1. Why the nonce must change per request
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.
2. Regenerating the nonce in a Node.js server
2.1. Middleware approach (recommended for server‑side rendered Next.js)
// 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 } })
}
2.2. Using the nonce in _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.
3. Handling static exports or CDN‑only deployments
When the app is exported to static HTML, there is no Node.js process to generate a nonce. Two practical solutions:
- Edge functions / serverless rewrites: Use a platform that can run code at the edge (e.g., Cloudflare Workers, Vercel Edge Functions). The worker generates a nonce, injects it into the
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.
- Cache‑busting CSP header: If you must rely on a CDN that only adds headers, configure the CDN to vary the header on a request‑specific value such as a cookie or a custom header that the worker adds. The CDN will then send a fresh header per request, matching the nonce embedded in the HTML by the worker.
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 })
}
4. Preventing caching of the CSP header
Ensure the CDN or reverse proxy does not cache the CSP header. Common tactics:
- Set
Cache‑Control: no-store, must‑revalidate for HTML responses.
- Use
Vary: Cookie or Vary: Request‑ID so that the same URL can return different headers per request.
- Turn off header caching in the CDN’s settings.
5. Quick sanity check
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.
Diagnostic question
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.