Repository navigation
App Router: let the framework nonce be read from a configurable/non-stripped request header (the Content-Security-Policy request header is stripped by some production CDNs/WAFs, e.g. Vercel) #95259
Replies: 5 comments
|
This sounds like a real gap between the documented nonce flow and how production infrastructure may treat The current docs flow depends on two different things:
If a CDN/WAF strips A configurable nonce source seems reasonable, for example: // next.config.js
module.exports = {
experimental: {
nonceHeader: 'x-nonce',
},
}or some equivalent App Router config. Next could still default to parsing A couple of constraints seem important:
This would match the docs pattern where middleware already sets both |
|
This seems like the right place for a small framework escape hatch, but it should be designed as an internal transport header rather than “read any user-provided nonce header.” A config-driven option would cover this cleanly, for example something along these lines: // next.config.js
module.exports = {
experimental: {
cspNonceHeader: 'x-nonce',
},
}Then middleware could generate the nonce once, set One important security detail: the docs should explicitly say the configured nonce header must be overwritten by middleware and not trusted from the incoming client request. Otherwise It would also be useful if Next warned when it detects a nonce-based CSP flow but cannot resolve a nonce for its own inline framework scripts. The current silent failure mode makes this hard to diagnose because app-owned inline scripts can still work while If my answer solved your problem, you can click answered the question. I'm really here to help, and along the way I'm also collecting Galaxy Brain badges haha 😆 |
|
Good point about treating this as an internal transport header rather than trusting arbitrary user input. If a config like I also agree that a warning would help a lot. The confusing failure mode here is that app-owned inline scripts can be nonced correctly from |
|
This is a real production gap that a few teams have hit. The short version is that Content-Security-Policy is specified as a response header, and CDN layers that correctly implement the spec strip it from the request side before it reaches your renderer. Vercel production does exactly this. The x-nonce custom header survives because it is not a restricted header name. The practical workaround available today is to set a custom header in middleware alongside the Content-Security-Policy header, then read that custom header in your layout for any scripts you control directly. In middleware.ts: Set the nonce on a custom header such as x-nonce in addition to Content-Security-Policy. In your root layout: Read the nonce from x-nonce via headers() for your own inline scripts and the Script component. The gap this does not solve is the framework bootstrap scripts that Next auto-injects, because those are controlled internally and only read from Content-Security-Policy. For those you would need Next itself to support a configurable header name, which is what this RFC is requesting. Until that lands, one option is to use a hash-based CSP rather than a nonce-based one for the framework scripts specifically. You can obtain the sha256 of each framework bootstrap script from a local next build run and add those as script-src hashes in your CSP. The drawback is that you have to update the hashes after any Next upgrade. The nonce-based approach for your own scripts still works because those go through the x-nonce path. Opening an issue in the Next.js repo with a pointer to this discussion and the Vercel support confirmation would help get this prioritized, since it is a regression relative to local dev and preview deploys. |
|
Two things worth adding, both checkable against the repo. Next reads two request header names, not one. In const csp =
headers['content-security-policy'] ||
headers['content-security-policy-report-only']
The stripping looks like a deliberate control, not an oversight. GHSA-ffhc-5mcf-pf4q / CVE-2026-44581, "Cross-site scripting in App Router applications using CSP nonces" (patched in 15.5.16 and 16.2.5), ends with: "If you cannot upgrade immediately, strip inbound That makes the report-only name a diagnostic, not a fix. If it reaches the renderer, you've learned the edge rule matches an exact name rather than a header class, which is useful evidence for this RFC, and arguably a gap in that mitigation. It's the same attacker-settable input the advisory tells you to strip, so it isn't something to ship on. (If you do test it, On the RFC. The spoofing concern raised above is the load-bearing one and the advisory backs it. You're on 16.2.9, past the 16.2.5 patch, so malformed nonces are already rejected; what's left is provenance. A One alternative shape while it's still shapeable: make the option about the nonce value rather than the header name. Resolving the nonce at render time is what forces dynamic rendering and disables ISR and PPR. If Next could stamp a fixed sentinel instead, a proxy could mint a fresh value per response and rewrite body and header together, and the cached-page case falls out of the same hook. Not Next.js experience: I run this on nginx in front of a static site: the origin emits a constant placeholder in both the CSP header and the HTML, and a Cloudflare Worker caches that copy, minting a per-visitor nonce and marking the substituted response Finally, on the hash-based workaround suggested above: I don't think it holds. A |
Uh oh!
There was an error while loading. Please reload this page.
What's happening: App Router's automatic nonce injection (into self.__next_f and other framework/bootstrap inline scripts) reads the nonce only from the request Content-Security-Policy header that middleware sets (per the CSP guide). Content-Security-Policy is a response-only header per spec, and some production CDN/WAF layers strip it from the request pipeline before the renderer. When that happens, Next silently skips noncing its own scripts → an enforcing nonce CSP blocks them → broken hydration.
Concretely (Next 16.2.9, App Router, on Vercel):
Local (next build && next start) and Vercel Preview: framework scripts nonce correctly. ✅
Vercel Production (same build): framework scripts come back un-nonced, while:
a custom request header (x-nonce) set by the same middleware does reach the renderer (our own inline scripts, nonced via (await headers()).get('x-nonce'), are nonced),
the page renders per request (x-vercel-cache: MISS),
so it's specifically the Content-Security-Policy request header that doesn't make it to the renderer — and Next only reads that one.
Vercel support confirmed this: their production CDN/WAF strips the response-only Content-Security-Policy header from the request pipeline (custom headers like x-nonce are preserved), and "Next.js's framework-level auto-noncer is hardcoded to look specifically for the standard Content-Security-Policy request header." Their guidance was to raise it here.
Request: Allow App Router to read the framework nonce from a configurable, non-stripped request header (e.g. x-nonce, or a headers.nonceHeader config) in addition to the Content-Security-Policy request header — so per-request nonce CSP works behind CDNs/WAFs that strip response-only header names from requests. Today there's no workaround: the framework scripts are uncontrollable by app code, and the one header Next reads is stripped in production.
All reactions