fix(security): add SPA CSP, drop x-powered-by, strengthen dedupe hash #84

Merged
whitlocktech merged 2 commits from fix/security-headers-csp into main 2026-07-21 04:13:43 +00:00
Member

Addresses the SonarQube Security Hotspots on the website (the ones that could be closed by a code change rather than triaged as "safe").

Changes

Content-Security-Policy (the main one)server/src/app.js
Replaced contentSecurityPolicy: false with a helmet CSP tuned for the built React SPA:

default-src 'self'; script-src 'self';
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src 'self' https://fonts.gstatic.com;
img-src 'self' data: https:; connect-src 'self';
frame-ancestors 'self'; object-src 'none'; base-uri 'self'
  • style-src 'unsafe-inline' is required — React renders pervasive inline style={{…}} attributes and CSP style attributes can't be nonce'd. Tracked as tech debt; tightening it is a client-side refactor, not a server change.
  • img-src https: covers uploads (same-origin), external images embedded in wiki/news bodies (the HTML sanitizer allows <img> over http/https), and external BRAND_* logo/hero/favicon.
  • upgrade-insecure-requests intentionally omitted — 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 scoped looser policy (inline script/style) because swagger-ui-express injects an inline bootstrap.

Keep script-src 'self' validclient/vite.config.js
Disabled the inline module-preload polyfill (modulePreload: { polyfill: false }). The app code-splits (RichTextEditor is a separate chunk), which is exactly when Vite would otherwise inject an inline polyfill <script>. Verified the built index.html has zero inline scripts.

Fingerprintingbot/src/app.js, server/src/internalApp.js
app.disable('x-powered-by') on the two internal-only listeners (the public app already strips it via helmet).

Weak hashserver/src/model/shardEvents/shardEvents.model.js + server/db/schema.sql
shard_events dedupe key: SHA-1 → SHA-256 truncated to 40 hex chars. It's a content fingerprint for idempotent INSERT IGNORE, not a security value; truncation keeps it inside the existing CHAR(40) column so there's no schema migration and no risk to the live DB.

Verification

  • Booted the app in-process and confirmed the rendered CSP header on a normal route vs. the looser one on /api/docs, and that X-Powered-By is absent.
  • client builds clean; no inline scripts in output.
  • Server tests: 284/284 pass.

Docs

Companion docs PR updates website/BACKEND_DESIGN.md: RunicGateway/docs#26.

AI disclosure

AI-assisted (Claude Code / Claude Opus 4.8). Commits carry a Co-Authored-By trailer.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NgyHnrNa8WwG3doxvxjuCr

Addresses the SonarQube **Security Hotspots** on the website (the ones that could be closed by a code change rather than triaged as "safe"). ## Changes **Content-Security-Policy (the main one)** — `server/src/app.js` Replaced `contentSecurityPolicy: false` with a helmet CSP tuned for the built React SPA: ``` default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self'; frame-ancestors 'self'; object-src 'none'; base-uri 'self' ``` - `style-src 'unsafe-inline'` is required — React renders pervasive inline `style={{…}}` attributes and CSP style *attributes* can't be nonce'd. Tracked as tech debt; tightening it is a client-side refactor, not a server change. - `img-src https:` covers uploads (same-origin), external images embedded in wiki/news bodies (the HTML sanitizer allows `<img>` over http/https), and external `BRAND_*` logo/hero/favicon. - `upgrade-insecure-requests` intentionally omitted — 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 **scoped looser policy** (inline script/style) because swagger-ui-express injects an inline bootstrap. **Keep `script-src 'self'` valid** — `client/vite.config.js` Disabled the inline module-preload polyfill (`modulePreload: { polyfill: false }`). The app code-splits (`RichTextEditor` is a separate chunk), which is exactly when Vite would otherwise inject an inline polyfill `<script>`. Verified the built `index.html` has zero inline scripts. **Fingerprinting** — `bot/src/app.js`, `server/src/internalApp.js` `app.disable('x-powered-by')` on the two internal-only listeners (the public app already strips it via helmet). **Weak hash** — `server/src/model/shardEvents/shardEvents.model.js` + `server/db/schema.sql` `shard_events` dedupe key: SHA-1 → SHA-256 truncated to 40 hex chars. It's a content fingerprint for idempotent `INSERT IGNORE`, not a security value; truncation keeps it inside the existing `CHAR(40)` column so there's **no schema migration** and no risk to the live DB. ## Verification - Booted the app in-process and confirmed the rendered CSP header on a normal route vs. the looser one on `/api/docs`, and that `X-Powered-By` is absent. - `client` builds clean; no inline scripts in output. - Server tests: **284/284 pass**. ## Docs Companion docs PR updates `website/BACKEND_DESIGN.md`: RunicGateway/docs#26. ## AI disclosure AI-assisted (Claude Code / Claude Opus 4.8). Commits carry a `Co-Authored-By` trailer. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01NgyHnrNa8WwG3doxvxjuCr
wtclaude added 2 commits 2026-07-21 04:03:26 +00:00
Address SonarQube security hotspots on the website:

- server/src/app.js: replace `contentSecurityPolicy: false` with a helmet CSP
  tuned for the built React SPA (script-src 'self'; style-src adds 'unsafe-inline'
  for React inline styles + the Google Fonts stylesheet; font-src gstatic; img-src
  allows data:/https: for uploads, embedded body images and BRAND_* assets;
  connect-src 'self' for REST+SSE). upgrade-insecure-requests is intentionally
  omitted (TLS terminates at the proxy; keeps local `npm start` over http working).
  The /api/docs Swagger UI route gets a scoped looser policy (inline script/style)
  since swagger-ui-express injects an inline bootstrap.
- client/vite.config.js: disable the inline module-preload polyfill so code-split
  builds keep `script-src 'self'` valid (RichTextEditor is a separate chunk).
- bot/src/app.js, server/src/internalApp.js: disable x-powered-by on the two
  internal-only listeners (the public app already strips it via helmet).
- shardEvents dedupe key: SHA-1 -> SHA-256 truncated to 40 hex chars (fits the
  existing CHAR(40) column, no migration; it is a content fingerprint, not a
  security value). schema.sql comment updated to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NgyHnrNa8WwG3doxvxjuCr
docs(readme): add SonarQube project badges
All checks were successful
PR Checks / bot-install (pull_request) Successful in 15s
PR Checks / client-build (pull_request) Successful in 9m23s
PR Checks / server-tests (pull_request) Successful in 10m10s
5e5e0d7a91
Bugs, code smells, duplicated lines, LOC, security hotspots, security rating,
and vulnerabilities badges linking to the project dashboard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NgyHnrNa8WwG3doxvxjuCr
whitlocktech approved these changes 2026-07-21 04:05:28 +00:00
whitlocktech scheduled this pull request to auto merge when all checks succeed 2026-07-21 04:05:34 +00:00
whitlocktech merged commit c73273f1bc into main 2026-07-21 04:13:43 +00:00
whitlocktech deleted branch fix/security-headers-csp 2026-07-21 04:13:44 +00:00
Sign in to join this conversation.
No description provided.