fix(csp): blank page on statically prerendered routes

The nonce-based script-src is incompatible with Next's static prerendering.
The nonce is minted per request in middleware, but statically generated HTML
was produced at build time with no nonce on its inline scripts, so on "/" and
the marketing routes every inline hydration script was blocked: the page
painted from SSR HTML, React failed to hydrate, and the tree unmounted to a
blank screen a moment later. Dynamic routes (/sign-in, /dashboard) render per
request, receive the nonce, and worked — which is why this was missed.

Browsers that honour a nonce ignore 'unsafe-inline', so the two cannot be
combined. Drop the nonce and allow inline scripts; the policy still restricts
which origins may serve scripts, and frame-src/connect-src stay strict.
Restoring nonces would require every route to render dynamically.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leon Serfaty
2026-09-07 12:16:09 -04:00
co-authored by Claude Opus 5
parent fc00ad5b32
commit 68c0b1306d
+10 -1
View File
@@ -67,7 +67,16 @@ export function middleware(req: NextRequest) {
// proxied analytics tracker at /_a), and the Turnstile host-source actually // proxied analytics tracker at /_a), and the Turnstile host-source actually
// takes effect instead of being silently ignored. The nonce is kept, so // takes effect instead of being silently ignored. The nonce is kept, so
// dynamic routes and any inline script still get the stronger guarantee. // dynamic routes and any inline script still get the stronger guarantee.
`script-src 'self' 'nonce-${nonce}' ${TURNSTILE_ORIGIN}${devScriptSrc}`, // NOTE: nonce-based script-src is incompatible with Next's statically
// prerendered pages. The nonce is minted per request, but static HTML was
// baked at build time WITHOUT one, so every inline hydration script on those
// pages ("/", the marketing routes) got blocked: the page painted, React
// failed to hydrate, and the tree unmounted to a blank screen. Dynamic routes
// (/sign-in, /dashboard) were fine, which is what made this easy to miss.
// A browser that honours a nonce ignores 'unsafe-inline', so the two cannot
// be combined — this policy still restricts which ORIGINS may serve scripts.
// To go back to nonces, every page must render dynamically.
`script-src 'self' 'unsafe-inline' ${TURNSTILE_ORIGIN}${devScriptSrc}`,
"style-src 'self' 'unsafe-inline'", "style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https://oaidalleapiprodscus.blob.core.windows.net https://images.unsplash.com", "img-src 'self' data: https://oaidalleapiprodscus.blob.core.windows.net https://images.unsplash.com",
"media-src 'self'", "media-src 'self'",