From a86629e24e282bf46391cea25ae3788982880b8d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 23 Sep 2026 15:16:17 +0000 Subject: [PATCH] Harden Express runtime and add Pages security headers - Set trust proxy (TRUST_PROXY env, defaults to 1 in production) so express-rate-limit keys on the real client IP behind a proxy - Add helmet with a CSP matching the Vite/Tailwind bundle - Default NODE_ENV to production so an unset env never leaks stacks or skips SPA serving - Map body-parser errors to 400/413/415 instead of generic 500 - Run CORS before rate limiting so preflights are not counted - Drop unused urlencoded parser, duplicate unhandledRejection handler, and dead health-check skip - Add Cloudflare Pages _headers (CSP, HSTS, nosniff, frame deny, etc.) Co-Authored-By: Jason Maycock --- .env.example | 4 ++ package-lock.json | 13 ++++++ server/.gitignore | 2 + server/client/public/_headers | 15 +++++++ server/package.json | 1 + server/src/config/index.ts | 17 +++++++- server/src/index.ts | 57 ++++++++++++++++----------- server/src/middleware/errorHandler.ts | 27 ++++++++++++- server/src/middleware/rateLimiting.ts | 4 -- 9 files changed, 110 insertions(+), 30 deletions(-) create mode 100644 server/client/public/_headers diff --git a/.env.example b/.env.example index 18ee550..ac7ac13 100644 --- a/.env.example +++ b/.env.example @@ -8,6 +8,10 @@ NODE_ENV=development # Security CORS_ORIGIN=http://localhost:5173 +# Express `trust proxy` value: hop count (1), true/false, or address list ("loopback, 10.0.0.0/8"). +# Defaults to 1 in production (one TLS-terminating proxy in front) and false otherwise. +# TRUST_PROXY=1 + # API Rate Limiting (Optional - defaults are secure) # General API rate limiting (15 minutes window) GENERAL_RATE_LIMIT_WINDOW_MS=900000 diff --git a/package-lock.json b/package-lock.json index 5a99dc4..0184bcf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3905,6 +3905,18 @@ "node": ">= 0.4" } }, + "node_modules/helmet": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.3.0.tgz", + "integrity": "sha512-Qgpiaws3Sm30Av8Eah6sjMCZZwjlBu+E68rhpCWBshY1lb09HtLwj5GviX0OyQIn+ulUS0iX0AxN5n3tLZzz1w==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/EvanHahn" + } + }, "node_modules/hookified": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", @@ -6610,6 +6622,7 @@ "express": "^5.2.1", "express-rate-limit": "^8.7.0", "express-slow-down": "^3.1.1", + "helmet": "^8.3.0", "openai": "^7.17.0", "zod": "^4.6.5" }, diff --git a/server/.gitignore b/server/.gitignore index c7ab0ea..4e3ff58 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -87,6 +87,8 @@ dist # Gatsby files .cache/ public +# Vite static assets (copied verbatim into client/dist) +!client/public # vuepress build output .vuepress/dist diff --git a/server/client/public/_headers b/server/client/public/_headers new file mode 100644 index 0000000..6805594 --- /dev/null +++ b/server/client/public/_headers @@ -0,0 +1,15 @@ +# Cloudflare Pages response headers (applied to the static site and /api/* functions). +# CSP: the SPA injects a