CSP with a nonce in Next.js: closing the XSS door without breaking Fast Refresh
A strong Content Security Policy with a per-request nonce neutralizes XSS even when something slips past sanitization. The catch that trips people up: making it coexist with hot reload in development.
A Content Security Policy (CSP) tells the browser where scripts, styles, and images may come from. Done well, it's the difference between a reflected XSS becoming an incident or a footnote: even if a malicious input escapes sanitization, the browser refuses to run a script that doesn't carry the right signature.
The signature here is a nonce — a random value generated on every response. Only the script that carries that nonce runs. Because the value changes on each request, an attacker has no way to predict which one to use.
Why strict-dynamic changes the game
The strict-dynamic directive tells modern browsers to trust only the nonce — and to ignore the allowlist of domains. A trusted script passes that trust along to the scripts it injects itself. This both simplifies the policy and strengthens it:
- You stop maintaining a host allowlist that ages badly.
- Third-party scripts only load if a script that's already trusted loads them.
- Older browsers ignore strict-dynamic and fall back to the host allowlist.
The detail that breaks in development
Next's Fast Refresh uses eval to swap components without reloading the page. A production CSP — without 'unsafe-eval' — kills that, and the dev server comes up with hot reload broken and an EvalError in the console.
The fix is to allow eval only in development, never in production:
const isDev = process.env.NODE_ENV !== 'production'
const devEval = isDev ? `'unsafe-eval'` : ''
const csp = [
`default-src 'self'`,
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic' ${devEval}`,
].join('; ')The real cost of this choice shows up in caching: because the nonce changes on every response, the HTML has to be rendered per request and ships with no-store. A stronger CSP costs a bit of TTFB. For most sites handling visitor data, it is a trade worth making.
More on the directive in the MDN documentation on CSP.