Headline: A Content Security Policy (CSP) is an HTTP response header that tells the browser which script, style, and connection sources a document is allowed to use, and in the Next.js App Router the only script policy that survives the framework's runtime chunk loading is a per-request nonce combined with .
The cost I did not budget for: generating that nonce in and reading it with opts every matched route out of static rendering.
Key takeaways A CSP nonce is a per-request random token that appears both in the directive and as a attribute on every allowed .
Because it must never repeat, HTML carrying a nonce cannot be cached — which is exactly why Next.js drops the route to dynamic rendering.
A host allowlist cannot secure a Next.js app.
The App Router emits an inline bootstrap payload () and then creates further elements at runtime, so both fails to allow the inline payload and fails to distinguish framework chunks from any other same-origin file. propagates trust from a nonce-allowed script to any script element that script creates programmatically.
It is what makes chunk loading work without enumerating chunk URLs.
Keeping and in is a deliberate fallback, not a hole.
A browser that understands nonces ignores , and a browser that understands ignores every host expression in the same directive.
Ship first.
The enforcing header turns a policy mistake into a blank page; the report-only header turns the same mistake into a log line.
What does a Content Security Policy actually stop?
A Content Security Policy does not stop injection.
It stops execution.
If an attacker gets into a comment field that I render with , the markup still lands in the DOM — but a policy without means the browser refuses to run it and emits a violation report instead.
Four directives paid for themselves in my apps before I touched at all, because none of them require a nonce and none of them break anything: removes the legacy plugin vector, stops an injected tag from silently repointing every relative URL on the page, stops an injected form from posting credentials to another origin, and is the modern replacement for .
Those four can go in under and stay fully cacheable.
The expensive directive is .
That is the one that requires the nonce.
Why does Next.js need a nonce instead of a domain allowlist?
A domain allowlist cannot express what the App Router does at runtime.
Next.js serializes the React Server Component payload into inline tags on the document, and its client runtime then creates additional script elements to fetch route chunks on demand. blocks the inline payload outright, and even if it did not, would happily execute any same-origin URL — including a user-uploaded file served from my own domain. is the CSP Level 3 keyword that fixes this.
It says: any script element created by an already-trusted script inherits that trust.
The inline bootstrap gets its trust from the nonce, and every chunk it loads afterwards inherits it.
No chunk hashes, no build-time URL enumeration.
The counterintuitive part is what switches off.
In a browser that implements it, all host-source expressions in the same directive — , , a literal CDN domain — are ignored.
That is why the recommended policy still lists them: they are a graceful degradation path for older browsers, not additional permission for modern ones.
How do I generate and propagate a CSP nonce in the App Router?
The nonce is generated once per request in , written to both the request headers and the response headers, and read back in a Server Component with .
Writing it onto the request matters: Next.js looks for a request header, extracts the nonce from it, and applies that nonce to the script tags it emits itself.
Skip that step and the framework's own bootstrap is blocked by my own policy.
Reading it in the root layout is two lines. returns a promise in Next.js 15 and later, so it must be awaited, and forwards a prop straight onto the emitted tag.
Why did my static pages turn dynamic after I added CSP?
Because a nonce is only a security control while it is unpredictable, and a cached HTML response hands the same nonce to every visitor.
A reused nonce is functionally identical to : an attacker who can read one page's markup learns the token that unlocks script execution on every other copy of it.
Next.js enforces the safe interpretation by treating as a dynamic API — the moment my root layout calls it, every route under that layout renders per request.
That is a real bill.
Marketing pages that were prerendered at build time became server-rendered on every hit, and Partial Prerendering could no longer treat the shell as static.
Strategy Script safety Rendering cost Nonce + everywhere Strongest — no inline injection executes Every matched route renders dynamically Nonce scoped to authenticated routes, static policy elsewhere Strong where user input is rendered Marketing and docs pages stay static No nonce, Weak — injected inline script still runs Fully static I landed on the middle row for content-heavy sites and the top row for anything behind a login.
Scoping is done through the middleware , and Next.js's documented example additionally excludes prefetch requests so a prefetched RSC payload does not burn a nonce it will never use.
What breaks in development and with CSS-in-JS?
Development needs in .
React Fast Refresh and eval-based source maps both compile strings at runtime, so a production-grade policy gives me a console full of the moment I run the dev server.
Gate it on rather than shipping it everywhere. is where I stopped fighting.
Next.js inlines critical CSS as elements, and CSS-in-JS libraries inject more at runtime, frequently from code paths that never see my nonce.
I keep in deliberately: style injection is a far weaker vector than script injection, and the alternative is a policy that breaks on every dependency upgrade.
If a threat model demands a style nonce, styled-components reads it from the global and Emotion accepts one via .
Three more that caught me: blur placeholders are URLs, so needs and usually ; analytics beacons need their host in , not , because the script is loaded but the beacon is a separate fetch; and embedded Stripe or YouTube iframes need , which does not inherit from once you start listing directives explicitly.
How should I roll out CSP without breaking production?
Send the identical policy under first and leave it there for a full traffic cycle — at minimum a week, so weekday and weekend behaviour both show up.
The report-only header instructs the browser to evaluate the policy and report violations without blocking anything, which converts an outage into a log stream.
Collecting the reports takes one Route Handler.
The legacy directive posts a single JSON object with content type ; the newer Reporting API uses a response header plus a directive and posts batched arrays as .
Browsers are split across both, so I send both directives and normalise on arrival.
Filter browser extensions before you alert on anything.
The first day I collected reports, the overwhelming majority came from and origins injecting scripts into pages — noise I cannot fix and should not page anyone about.
What is left after that filter is the actual list of things my policy would have broken.
FAQ Q: Does a CSP nonce protect me if I render untrusted HTML with ?
A: For script execution, yes.
An injected without the matching nonce will not run, and inline event handler attributes such as are blocked by the same absence of .
It does not stop the markup itself from rendering, so a CSP is not a substitute for sanitising with a library like DOMPurify.
Q: Can I use a nonce-based CSP with a statically exported Next.js site?
A: No.
A nonce must be generated per request, and a static export has no request-time compute.
The options are a hash-based — which requires extracting the hashes of the inline bootstrap after every build, since they change per build — or serving the export behind an edge function that injects the header