fix(security): add SPA CSP, drop x-powered-by, strengthen dedupe hash #84
Reference in New Issue
Block a user
No description provided.
Delete Branch "fix/security-headers-csp"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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.jsReplaced
contentSecurityPolicy: falsewith a helmet CSP tuned for the built React SPA:style-src 'unsafe-inline'is required — React renders pervasive inlinestyle={{…}}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 externalBRAND_*logo/hero/favicon.upgrade-insecure-requestsintentionally omitted — TLS terminates at the proxy, there are no mixed-content subresources, and it would break a localnpm startover plain http./api/docsSwagger 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.jsDisabled the inline module-preload polyfill (
modulePreload: { polyfill: false }). The app code-splits (RichTextEditoris a separate chunk), which is exactly when Vite would otherwise inject an inline polyfill<script>. Verified the builtindex.htmlhas zero inline scripts.Fingerprinting —
bot/src/app.js,server/src/internalApp.jsapp.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.sqlshard_eventsdedupe key: SHA-1 → SHA-256 truncated to 40 hex chars. It's a content fingerprint for idempotentINSERT IGNORE, not a security value; truncation keeps it inside the existingCHAR(40)column so there's no schema migration and no risk to the live DB.Verification
/api/docs, and thatX-Powered-Byis absent.clientbuilds clean; no inline scripts in output.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-Bytrailer.🤖 Generated with Claude Code
https://claude.ai/code/session_01NgyHnrNa8WwG3doxvxjuCr