ADR 024 — Frontend-tier security: CSP, cookies, CSRF, and response headers
Status
Accepted — the frontend half of the security work (backend half: ADR 019). The CSP baseline is committed: strict nonce-based script-src as the floor and a pragmatic style-src as the guaranteed baseline. A fully-strict style-src is an upgrade the Phase-0 spike (see Migration Plan) may unlock via SSR style extraction (ADR 013) — an upgrade, not a gate.
Context
The A3 security work was split: ADR 019 hardened the backend (rate limiting, session/token policy, audit logging, private backend), and the frontend-delivery controls were deferred to here because they live in the Nuxt tier and because the strongest control — a strict Content-Security-Policy — depends on a spike unknown: Ant Design Vue uses CSS-in-JS, so whether we can serve a nonce-based CSP without 'unsafe-inline' for styles is not yet known (ADR 013).
Two migration decisions set the stage. After the full BFF (ADR 022), Nuxt is the public tier and the owner of browser security, and auth is now cookie-based — which requires CSRF defense that bearer-in-header auth did not. And the security-header responsibility has moved from nginx-serving-a-static-bundle to the Nuxt SSR server.
Decision
Set a complete browser-security baseline at the Nuxt server (likely via the nuxt-security module, which provides nonce-based CSP, the header set, and CSRF helpers):
Content-Security-Policy. A per-request nonce-based policy from the Nuxt SSR server. Committed baseline: strict
script-src 'self' 'nonce-…'— no'unsafe-inline'for scripts, the security-critical control — with a pragmaticstyle-srcthat permits inline styles for Ant Design Vue's CSS-in-JS:default-src 'self'; script-src 'self' 'nonce-…'; style-src 'self' 'unsafe-inline'; frame-ancestors 'none'; object-src 'none'; base-uri 'self'Upgrade path: if the Phase-0 spike shows Ant Design Vue's styles can be extracted and nonced at SSR (
@ant-design/cssinjs), tightenstyle-srcto'self' 'nonce-…'and drop'unsafe-inline'. Roll out Report-Only first, then enforce. (Relaxing styles while keeping scripts strict is a deliberate, low-risk trade — script injection is the dangerous vector; CSS injection is comparatively minor.)Session cookie (from ADR 022):
Secure,httpOnly,SameSite,__Host-prefix, root path, noDomain.CSRF:
SameSiteplus a synchroniser / double-submit token on state-changing calls proxied through the BFF.Other response headers: HSTS (with preload),
X-Content-Type-Options: nosniff,Referrer-Policy: strict-origin-when-cross-origin, a locked-downPermissions-Policy, andframe-ancestors 'none'(via CSP) /X-Frame-Options: DENY. COOP/CORP as appropriate.
Consequences
Positive
- Strong XSS mitigation. A nonce-based CSP complements the BFF's tokens-out-of-JS posture (ADR 022) — defense in depth against script injection.
- Closes the cookie-auth gap. CSRF protection is added exactly where cookie-based auth needs it.
- A complete header baseline on the public tier, unified in intent with the backend hardening (ADR 019).
Negative
- The strict-
style-srcupgrade is spike-dependent. The committed baseline ships regardless (strict scripts, inline styles allowed); tighteningstyle-srcto nonce-only hinges on AntD SSR style extraction proving practical. Scripts are strict either way, so the security-meaningful part is not at risk. - Nonce plumbing through SSR and a Report-Only→enforce rollout add wiring and a transition window.
- Header and Permissions-Policy tuning is fiddly and easy to get subtly wrong.
Why this was deferred from ADR 019
The backend hardening was independent and unblocked; these controls require the Nuxt tier to exist, and the (optional) strict-style-src upgrade hinges on the Ant Design Vue SSR spike. Splitting let the backend proceed immediately without waiting on a frontend unknown.