Chapter 2 · Practice 1 of 3
Use a strict, nonce-based CSP instead of a domain allowlist
Why it matters
Older CSPs listed trusted domains: script-src 'self' cdn.example.com analytics.example.net. Those are easy to bypass when any allowed domain hosts user content or an old library with an injectable endpoint. OWASP's CSP Cheat Sheet recommends a strict policy based on nonces:
Content-Security-Policy: script-src 'nonce-{RANDOM}' 'strict-dynamic'; object-src 'none'; base-uri 'none';
| Directive | What it does |
|---|---|
'nonce-{RANDOM}' | only script tags carrying this response's nonce run |
'strict-dynamic' | scripts loaded by a trusted script are trusted too, so bundlers and loaders keep working |
object-src 'none' | blocks plugin content |
base-uri 'none' | stops an injected <base> tag redirecting relative script URLs |
Nonce rules from OWASP:
- generate a new nonce for every response, from a secure random generator
- add it in your templating, on the script tags you wrote
- never use middleware that rewrites every script tag to add a nonce, because injected tags would get one too
Many frameworks support this directly, for example Next.js reading a nonce from a request header set in middleware.
Example
import { randomBytes } from 'node:crypto';
export function cspHeaders() {
const nonce = randomBytes(16).toString('base64'); // new for every response
const csp = [
`script-src 'nonce-${nonce}' 'strict-dynamic'`,
"object-src 'none'",
"base-uri 'none'",
"frame-ancestors 'none'",
].join('; ');
return { nonce, header: csp }; // pass nonce to the template's script tags
}