feat(security): nonce-based CSP script-src via middleware, report-uri to Sentry - #616
Open
Codesmith313 wants to merge 1 commit into
Open
feat(security): nonce-based CSP script-src via middleware, report-uri to Sentry#616Codesmith313 wants to merge 1 commit into
Codesmith313 wants to merge 1 commit into
Conversation
… 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
|
@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. |
|
@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! 🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #480, assigned to me.
The app already had a
Content-Security-Policydefined innext.config.mjs'sheaders()— the issue's summary ("no CSP headers configured") is out of date. What was actually still missing were the other two acceptance criteria:script-srcused'unsafe-inline'rather than a nonce, and there was noreport-uri.script-src. Moved CSP generation into a newmiddleware.tsso it can mint a fresh nonce per request —next.config.mjs'sheaders()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 innext.config.mjssince they don't need per-request generation. Also removed the old static CSP entry fromnext.config.mjs— leaving both would have given the browser twoContent-Security-Policyheaders, which it intersects rather than overrides, silently breaking the new policy.script-srcis'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 noheaders()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 fromNEXT_PUBLIC_SENTRY_DSN— Sentry's CSP report endpoint has the shapehttps://<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.script-srcalso allows'unsafe-eval'. Next.js Fast Refresh wraps modules witheval()for hot-reload — I found this wasn't hypothetical: running a real dev server and checking the browser console showed a genuineEvalErroron every edit before this exception, which would've broken local development for the whole team. That code path doesn't ship innext 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 oneContent-Security-Policyheader with a different nonce on every request, plus all the other security headers intact/loginand/— no CSP violations, no errorsconnect-src) all load correctlyTest plan
npx jest— 40/40 passing, including 8 new middleware tests (nonce uniqueness per request, nounsafe-inline, nonce forwarded via request header,report-uripresent/absent based on DSN,unsafe-evalpresent in dev only / absent otherwise, rest of the policy intact) — and the pre-push hook's own full runnpx eslint— clean (one pre-existingdefaultRuntimeCachingunused-import error innext.config.mjspredates this PR, confirmed viagit stash, not touched here)npx tsc --noEmit— no new errors