CSP nonce mismatch causes script execution failure in production
0 reputation · 24 May 2020, 02:47 UTC
0 reputation · 24 May 2020, 02:47 UTC
Goal: Configure a Next.js application so that inline script elements execute successfully in both local development and production deployments by using a nonce‑based Content Security Policy.
In local development the Next.js dev server generates a fresh nonce for each request, adds it to the Content‑Security‑Policy header and to the nonce attribute of every script tag, so the browser allows the inline code. In production, when the application is exported as static HTML or served behind a cache that reuses the same CSP header, the nonce value does not change, causing the browser to reject the inline script with a CSP violation.
How can the nonce be regenerated for every response in a production Node.js server that serves Next.js pages?
What configuration ensures that the nonce value in the CSP header matches the nonce attribute on all script tags when using a static export or a CDN?
34775 reputation · 24 May 2020, 14:32 UTC
The nonce-… token in Content‑Security‑Policy is a request‑specific value. The browser will only allow an inline <script> whose nonce attribute matches the token in the header. If the header is cached and reused, the inline script’s nonce will never match and the script will be blocked.
// middleware.js (Next.js 13+)
import { randomBytes } from 'crypto'
export async function middleware(request) {
// Generate a 16‑byte base64 nonce per request
const nonce = randomBytes(16).toString('base64')
// Store it on the request so _document can read it
request.headers.set('x-nonce', nonce)
// Inject the CSP header
const csp = `script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'`
return NextResponse.next({ headers: { 'Content-Security-Policy': csp } })
}
_document.js// pages/_document.js
import Document, { Html, Head, Main, NextScript } from 'next/document'
export default class MyDocument extends Document {
render() {
const nonce = this.props.__NEXT_DATA__.nonce
return (
{/* other head tags */}
)
}
}
In this pattern the nonce is created once per HTTP request, added to the header, and made available to the page via __NEXT_DATA__ so that every inline script receives the same value.
When the app is exported to static HTML, there is no Node.js process to generate a nonce. Two practical solutions:
Content‑Security‑Policy header, and rewrites the HTML to replace a placeholder (e.g., __NONCE__) with the same value before the browser parses the page.Example Cloudflare Worker snippet:
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
async function handleRequest(request) {
const nonce = crypto.getRandomValues(new Uint8Array(16)).reduce((a,b)=>a+b,0).toString(36)
const response = await fetch(request)
const html = await response.text()
const rewritten = html.replace(/__NONCE__/g, nonce)
const newHeaders = new Headers(response.headers)
newHeaders.set('Content-Security-Policy', `script-src 'nonce-${nonce}' 'strict-dynamic'`)
return new Response(rewritten, { headers: newHeaders })
}
Ensure the CDN or reverse proxy does not cache the CSP header. Common tactics:
Cache‑Control: no-store, must‑revalidate for HTML responses.Vary: Cookie or Vary: Request‑ID so that the same URL can return different headers per request.After deploying, open the page, inspect the Content‑Security‑Policy header and the nonce attribute on every <script> tag. They must match exactly (case‑sensitive, no extra whitespace). If they differ, the browser will log a CSP violation and the script will never run.
Does your CDN add the CSP header after the HTML has already been cached and served? If it does, the nonce in the header will not match the one embedded in the page.
Use comments to ask for clarification. Post a solution as an answer.
34,775 reputation · 24 May 2020, 03:40 UTC
Static export writes HTML at build time, so a nonce embedded in the markup is fixed for all requests. A CSP nonce must be generated per response and mirrored exactly in the Content-Security-Policy header and in the nonce attribute of each inline script tag.
When the same HTML is served from a CDN or a cache that reuses a previously set header, the header nonce and the markup nonce diverge and the browser blocks execution. With Next.js, per-request nonce generation requires a running Node server or edge middleware that renders the page per request; a static export has no request-time rendering to create a fresh value.
Verify by comparing the response header script-src 'nonce-...' with the rendered script nonce attribute and checking the console for CSP violation reports.