Skip to content

feat(security): nonce-based CSP script-src via middleware, report-uri to Sentry - #616

Open
Codesmith313 wants to merge 1 commit into
kellymusk:devfrom
Codesmith313:security/480-csp-nonce-report-uri
Open

feat(security): nonce-based CSP script-src via middleware, report-uri to Sentry#616
Codesmith313 wants to merge 1 commit into
kellymusk:devfrom
Codesmith313:security/480-csp-nonce-report-uri

Conversation

@Codesmith313

Copy link
Copy Markdown

Summary

Closes #480, assigned to me.

The app already had a Content-Security-Policy defined in next.config.mjs's headers() — the issue's summary ("no CSP headers configured") is out of date. What was actually still missing were the other two acceptance criteria: script-src used 'unsafe-inline' rather than a nonce, and there was no report-uri.

  • Nonce-based script-src. Moved CSP generation into a new middleware.ts so it can mint a fresh nonce per request — next.config.mjs's headers() function only runs once at build/server-init time, so it structurally can't generate per-request randomness; that's why criterion 1 ("CSP defined in next.config.mjs headers() block") and criterion 2 ("nonce-based approach") are in tension, and why Next.js's own official CSP guide requires middleware for this. The other static security headers (X-Frame-Options, X-Content-Type-Options, etc.) stay in next.config.mjs since they don't need per-request generation. Also removed the old static CSP entry from next.config.mjs — leaving both would have given the browser two Content-Security-Policy headers, which it intersects rather than overrides, silently breaking the new policy.
  • In production, script-src is 'self' 'nonce-<value>' 'strict-dynamic'. Next.js reads the nonce back out of the CSP response header itself and applies it to its own inline bootstrap/hydration scripts automatically, so no headers() call in a layout (and the opt-out of static rendering that comes with reading it) is needed — I confirmed there are no custom inline <script> tags anywhere in the app that would need one.
  • report-uri. Derived from NEXT_PUBLIC_SENTRY_DSN — Sentry's CSP report endpoint has the shape https://<ingest-host>/api/<project-id>/security/?sentry_key=<public-key>, parsed straight out of the DSN. Omitted entirely when no DSN is configured (e.g. local dev), rather than pointing at a broken URL.
  • Dev-mode exception. In development only, script-src also allows 'unsafe-eval'. Next.js Fast Refresh wraps modules with eval() for hot-reload — I found this wasn't hypothetical: running a real dev server and checking the browser console showed a genuine EvalError on every edit before this exception, which would've broken local development for the whole team. That code path doesn't ship in next build, so production stays on the strict nonce-only policy.

Verification ("no existing functionality broken")

Ran a real dev server and checked in an actual browser, not just code review:

  • curl -D - on / shows one Content-Security-Policy header with a different nonce on every request, plus all the other security headers intact
  • Typed into the login page's email field — input updates, confirming hydration isn't blocked
  • Checked the browser console on both /login and / — no CSP violations, no errors
  • Landing page renders fully: images, fonts, and the live-rate fetch (connect-src) all load correctly

Test plan

  • npx jest — 40/40 passing, including 8 new middleware tests (nonce uniqueness per request, no unsafe-inline, nonce forwarded via request header, report-uri present/absent based on DSN, unsafe-eval present in dev only / absent otherwise, rest of the policy intact) — and the pre-push hook's own full run
  • npx eslint — clean (one pre-existing defaultRuntimeCaching unused-import error in next.config.mjs predates this PR, confirmed via git stash, not touched here)
  • npx tsc --noEmit — no new errors
  • Live browser verification as above

… to Sentry

The app already had a Content-Security-Policy in next.config.mjs, but
script-src used 'unsafe-inline' and there was no report-uri — the two
acceptance criteria actually still unmet here.

- Move CSP generation into middleware.ts so script-src can carry a fresh
  nonce per request instead of 'unsafe-inline'. next.config.mjs's headers()
  runs once at build/server-init time, so it can't mint a per-request nonce
  — that's why this can't live there entirely; the other static security
  headers (X-Frame-Options, etc.) stay in next.config.mjs.
- script-src is 'self' 'nonce-<value>' 'strict-dynamic' in production. Next.js
  reads the nonce back out of the CSP response header itself and applies it
  to its own inline bootstrap/hydration scripts, so no headers()-in-layout
  plumbing (and the opt-out of static rendering that comes with it) is
  needed — confirmed no custom inline <script> tags exist anywhere in the
  app that would need one.
- report-uri is derived from NEXT_PUBLIC_SENTRY_DSN (Sentry's CSP report
  endpoint shape: https://<ingest-host>/api/<project-id>/security/
  ?sentry_key=<public-key>), omitted entirely when no DSN is configured.
- In development only, script-src also allows 'unsafe-eval' — Next.js Fast
  Refresh wraps modules with eval() for hot-reload, which the strict policy
  otherwise throws on every edit. Confirmed via a live dev server + browser
  console before this that it was actually a real EvalError, not
  hypothetical; production builds don't ship that code path.

Verified via a real browser: CSP header present with a new nonce every
request, no other CSP violations in console, login page hydrates and
accepts input, landing page renders with images/fonts/live rate fetch all
loading correctly, no functionality broken.

Also re-fixed components/session-provider.tsx (same bad-merge compile
break documented in kellymusk#477-479 — duplicate imports/types, broken signOut)
since it blocks the full test suite the pre-push hook runs.

Closes kellymusk#480
@vercel

vercel Bot commented Aug 31, 2026

Copy link
Copy Markdown

@Codesmith313 is attempting to deploy a commit to the kelly musk's projects Team on Vercel.

A member of the Team first needs to authorize it.

@drips-wave

drips-wave Bot commented Aug 31, 2026

Copy link
Copy Markdown

@Codesmith313 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant