From 28c5c228f7df9982deacbb20f4825df843a69d73 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Mon, 20 Jul 2026 23:02:57 -0500 Subject: [PATCH] docs(website): document the SPA Content-Security-Policy Replace the vague "helmet with a CSP suited to the SPA" line with the actual policy now implemented in server/src/app.js: per-directive sources and the rationale for each non-'self' allowance (Google Fonts, inline React styles, external/embedded images, same-origin REST+SSE), why upgrade-insecure-requests is omitted, the scoped looser CSP for the /api/docs Swagger UI route, and the X-Powered-By handling across the public and internal listeners. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01NgyHnrNa8WwG3doxvxjuCr --- website/BACKEND_DESIGN.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/website/BACKEND_DESIGN.md b/website/BACKEND_DESIGN.md index ace7b5d..9eb21c3 100644 --- a/website/BACKEND_DESIGN.md +++ b/website/BACKEND_DESIGN.md @@ -409,7 +409,19 @@ who"; `activity_log` provides the history feed. - **bcrypt** hashing (cost 10+); plaintext passwords never stored, logged, or returned. - **Rate limiting** (`express-rate-limit`) on `/auth/login` and `/public/contact`. - **Validation** (`express-validator`) on all writes; centralized error handler. -- **helmet** with a CSP suited to the SPA (self + inline styles as needed; image sources for uploads/hero). +- **helmet** with a Content-Security-Policy tuned for the built React SPA (see `server/src/app.js`): + `default-src 'self'`; `script-src 'self'` (the Vite build emits only external module chunks — the + inline module-preload polyfill is disabled in `client/vite.config.js` to keep this valid); + `style-src 'self' 'unsafe-inline' https://fonts.googleapis.com` (React's pervasive inline + `style={{…}}` attributes can't be nonce'd, plus the Google Fonts stylesheet); `font-src 'self' + https://fonts.gstatic.com` (Cinzel); `img-src 'self' data: https:` (same-origin uploads, plus + external https images embedded in wiki/news bodies or `BRAND_*` logo/hero/favicon); `connect-src + 'self'` (REST + SSE are same-origin); `frame-ancestors 'self'`; `object-src 'none'`; `base-uri + 'self'`. `upgrade-insecure-requests` is intentionally **not** set (TLS terminates at the proxy, there + are no mixed-content subresources, and it would break a local `npm start` over plain http). The + `/api/docs` Swagger UI route gets a **looser** policy that additionally allows inline script/style, + since swagger-ui-express injects an inline bootstrap. helmet also strips `X-Powered-By`; the two + internal-only listeners (`internalApp.js`, `bot/src/app.js`) disable it explicitly too. - **Admin not indexed**: `X-Robots-Tag: noindex, nofollow` on `/api/v1/admin` and the admin SPA routes; `robots.txt` disallows `/admin`. - **No directory browsing** (express.static doesn't list; no `serve-index`). - **No hardcoded credentials**: first admin via `seed.js` reading `ADMIN_USERNAME`/`ADMIN_PASSWORD` from env (created only if no users exist); `.env` git-ignored, `.env.example` committed. -- 2.49.1