[SECURITY AUDIT] No Content-Security-Policy — stored-HTML XSS defense rests entirely on sanitization #34

Open
opened 2026-07-04 22:01:47 +00:00 by wtclaude · 0 comments
Member

Severity: Low
Confidence: High (verified); this is a defense-in-depth gap, not an active exploit
Scope area: User input handling / XSS (audit area 3)

What

Helmet is initialized with CSP explicitly disabled:

// server/src/app.js:39-44
helmet({
  contentSecurityPolicy: false,
  crossOriginResourcePolicy: { policy: 'cross-origin' },
})

Meanwhile the app renders admin-authored HTML into the DOM via dangerouslySetInnerHTML in three public views:

  • client/src/routes/wiki/WikiArticle.jsx:124
  • client/src/routes/public/FiveOnFriday.jsx:40
  • client/src/routes/public/NewsletterIssue.jsx:56

Sanitization is done well and in two layers (server-side sanitizeHtml.cleanBody on save, client-side DOMPurify.sanitize on render), so there is no known bypass today. But with CSP off, if any sanitizer gap is ever found (a DOMPurify bypass, a config regression in the allowlist, a new render path that forgets to sanitize), injected script executes with nothing to stop it — no script-src restriction, no blocking of inline handlers or data:/remote script.

Why it matters

CSP is the standard second line of defense specifically for the "our sanitizer had a hole" case. The allowlist in server/src/utils/sanitizeHtml.js permits <span>, <a>, <img>, tables, etc. with attributes — a rich surface where one misconfiguration (or a future relaxation) turns into stored XSS on public pages, executing in visitors' browsers and, if it hits an authenticated admin, against a cookie session that (per issue #30) cannot be revoked.

Where

  • server/src/app.js:39-44 (CSP disabled)
  • client/src/routes/wiki/WikiArticle.jsx:124, client/src/routes/public/FiveOnFriday.jsx:40, client/src/routes/public/NewsletterIssue.jsx:56 (HTML injection sinks)

Suggested fix (not implemented)

The code comment says CSP was deferred to "the frontend phase" — that phase is now in place, so enable it. Define a restrictive policy for the SPA (default-src 'self', a controlled img-src including /uploads, script-src 'self' — avoid unsafe-inline, using hashes/nonces if inline is unavoidable). Roll out via Content-Security-Policy-Report-Only first to catch violations, then enforce.

**Severity:** Low **Confidence:** High (verified); this is a defense-in-depth gap, not an active exploit **Scope area:** User input handling / XSS (audit area 3) ### What Helmet is initialized with CSP explicitly disabled: ```js // server/src/app.js:39-44 helmet({ contentSecurityPolicy: false, crossOriginResourcePolicy: { policy: 'cross-origin' }, }) ``` Meanwhile the app renders admin-authored HTML into the DOM via `dangerouslySetInnerHTML` in three public views: - `client/src/routes/wiki/WikiArticle.jsx:124` - `client/src/routes/public/FiveOnFriday.jsx:40` - `client/src/routes/public/NewsletterIssue.jsx:56` Sanitization is done well and in two layers (server-side `sanitizeHtml.cleanBody` on save, client-side `DOMPurify.sanitize` on render), so there is no known bypass today. But with CSP off, **if** any sanitizer gap is ever found (a DOMPurify bypass, a config regression in the allowlist, a new render path that forgets to sanitize), injected script executes with nothing to stop it — no `script-src` restriction, no blocking of inline handlers or `data:`/remote script. ### Why it matters CSP is the standard second line of defense specifically for the "our sanitizer had a hole" case. The allowlist in `server/src/utils/sanitizeHtml.js` permits `<span>`, `<a>`, `<img>`, tables, etc. with attributes — a rich surface where one misconfiguration (or a future relaxation) turns into stored XSS on public pages, executing in visitors' browsers and, if it hits an authenticated admin, against a cookie session that (per issue #30) cannot be revoked. ### Where - `server/src/app.js:39-44` (CSP disabled) - `client/src/routes/wiki/WikiArticle.jsx:124`, `client/src/routes/public/FiveOnFriday.jsx:40`, `client/src/routes/public/NewsletterIssue.jsx:56` (HTML injection sinks) ### Suggested fix (not implemented) The code comment says CSP was deferred to "the frontend phase" — that phase is now in place, so enable it. Define a restrictive policy for the SPA (`default-src 'self'`, a controlled `img-src` including `/uploads`, `script-src 'self'` — avoid `unsafe-inline`, using hashes/nonces if inline is unavoidable). Roll out via `Content-Security-Policy-Report-Only` first to catch violations, then enforce.
wtclaude added the
severity:low
label 2026-07-04 22:01:47 +00:00
Sign in to join this conversation.
No description provided.