How to generate per-request CSP nonces
This guide is part of the Content-Security-Policy (CSP) reference and covers generating per-request nonces so you can allow specific inline scripts without 'unsafe-inline'. A nonce (“number used once”) is a random token the server emits in the CSP header and on each trusted <script nonce="…"> tag in the same response. The browser executes only the inline scripts whose nonce attribute matches the value in the header. The whole scheme depends on one rule: the token must be unpredictable and freshly minted for every single response. A nonce that an attacker can guess, or one that survives across responses because a cache served stale HTML, provides no protection at all.
Configuration Syntax & Exact Values
The CSP carries the nonce inside script-src (and optionally style-src), and the same value appears on every inline script you trust:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-r4nd0mBase64ValueHere'; object-src 'none'; base-uri 'self'
<script nonce="r4nd0mBase64ValueHere">
// trusted inline bootstrap
</script>
Annotated breakdown and the non-negotiable properties of the token:
'nonce-…'— the literal prefixnonce-followed by the base64 token, wrapped in single quotes insidescript-src. The browser strips thenonce-prefix and compares the remainder against thenonceattribute on each inline<script>.- Cryptographically random — generate it with a CSPRNG (
crypto.randomBytes,secrets.token_*,/dev/urandom), neverMath.random, a timestamp, a counter, or a request hash. The spec calls for at least 128 bits of entropy; 16 random bytes is the practical floor. - base64-encoded — the token must be a valid base64 string (the value byte-for-byte; standard or URL-safe alphabet both work). Do not add whitespace or line breaks.
- Regenerated every response — a new value per HTTP response, including redirects and error pages that contain inline script. Reusing a nonce across responses lets an injected script reuse a leaked value.
- Never cached — because the token is per-response, any cache layer (CDN, reverse proxy, browser) that stores the HTML must not store it under a key that serves the same body — and therefore the same stale nonce — to a later request. Mark noncing HTML
Cache-Control: no-storeor vary per request.
The same value must appear in the header and on the tag; a mismatch silently blocks the script.
Server-Side Configuration
Nonces are inherently an application concern: the same random value has to reach the header and the template in one render pass. A pure web-server config can only fake it, so most of the real work lives in the app tier.
Nginx
Nginx can generate a per-request variable and inject it into the header, but it cannot rewrite your inline <script> tags unless the body is plain HTML you control. The $request_id is per-request and high-entropy but is hex, not the secret the spec wants; use it only as a stopgap and prefer app-generated nonces.
# Per-request token injected into the header.
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-$request_id'; object-src 'none'; base-uri 'self'" always;
# To stamp the SAME value onto inline scripts you need sub_filter on the body:
sub_filter_once off;
sub_filter 'NONCE_PLACEHOLDER' '$request_id';
proxy_no_cache 1; # do not cache responses that carry a per-request nonce
proxy_cache_bypass 1;
always emits the header on error responses too, so inline scripts on a 404/500 page are still covered. sub_filter only works on uncompressed, text bodies your origin renders with the literal NONCE_PLACEHOLDER; it breaks on gzip/brotli from upstream and on binary or streamed content. This is a limitation, not a recommendation — the server cannot mint a fresh secret and rewrite a templated app’s markup reliably, so the application must generate the nonce.
Node/Express (Helmet)
Generate a fresh crypto.randomBytes nonce in middleware, expose it to templates, and reference it from Helmet’s CSP via a function:
const crypto = require('crypto');
const helmet = require('helmet');
app.use((req, res, next) => {
res.locals.nonce = crypto.randomBytes(16).toString('base64');
next();
});
app.use(helmet.contentSecurityPolicy({
useDefaults: false,
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.nonce}'`],
objectSrc: ["'none'"],
baseUri: ["'self'"],
},
}));
<!-- in the template (EJS/Pug/etc.) -->
<script nonce="<%= nonce %>">window.__BOOT__ = true;</script>
The directive value is a function so Helmet evaluates it per response, reading the same res.locals.nonce the template uses. Set Cache-Control: no-store on these HTML routes so a proxy never serves one user’s nonce to the next request.
Django/Python
Generate the nonce in middleware with secrets, attach it to the request, and rewrite the CSP header on the way out:
import secrets
class CSPNonceMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
request.csp_nonce = secrets.token_urlsafe(16)
response = self.get_response(request)
response["Content-Security-Policy"] = (
"default-src 'self'; "
f"script-src 'self' 'nonce-{request.csp_nonce}'; "
"object-src 'none'; base-uri 'self'"
)
response["Cache-Control"] = "no-store"
return response
<script nonce="">/* trusted inline */</script>
secrets.token_urlsafe(16) yields a URL-safe base64 token with 128 bits of entropy. Reading request.csp_nonce in the template guarantees the tag and header carry the identical value generated for this request.
Next.js
For the App Router and edge runtime, generate the nonce in middleware.ts, pass it through a request header, and read it in the layout. The full, framework-specific recipe — including reading the nonce from headers() and propagating it to the Next.js script loader — is covered in setting CSP with nonces in Next.js middleware. The core is the same: crypto.randomUUID()/crypto.getRandomValues per request, written into both the CSP and the rendered scripts, with the route excluded from caching.
Diagnostic & Verification Steps
The defining test is that the nonce changes on every request. Hit the page twice and confirm the token differs.
curl -sI https://example.com/ | grep -i content-security-policy
curl -sI https://example.com/ | grep -i content-security-policy
Expected output — two different nonce- values:
content-security-policy: default-src 'self'; script-src 'self' 'nonce-Yz2bQk9...'; object-src 'none'; base-uri 'self'
content-security-policy: default-src 'self'; script-src 'self' 'nonce-Pm7vR1d...'; object-src 'none'; base-uri 'self'
If the two values are identical, a cache is serving a stale nonce — that is a defect, not a passing test.
Confirm the header value matches the tag in the same response:
curl -s https://example.com/ -D - -o body.html | grep -i content-security-policy
grep -o 'nonce="[^"]*"' body.html | head -1
The base64 string after nonce- in the header must equal the value in the nonce="…" attribute. In the browser, open DevTools → Console: a trusted inline script with the correct nonce runs silently, while any inline script lacking it raises Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self' 'nonce-…'". The Network panel → Response Headers shows the live CSP for the document; reload and watch the nonce change.
Edge Cases, Security Implications & Safe Rollback
- Caching breaks nonces. Any full-page cache (CDN, Varnish,
proxy_cache, browserCache-Control: public) that stores noncing HTML will serve the same nonce to many requests, or serve a body whose embedded nonce no longer matches a regenerated header — both break script execution. Mark noncing documentsCache-Control: no-store(orprivate, no-cache). For static, cacheable HTML you cannot nonce per request at all; use hash-based CSP instead. - Nonce reuse defeats the control. Generating the token once at boot, deriving it from the URL/session, or copying it across responses lets an injected script present a known nonce. Each response must call the CSPRNG afresh.
- Static CDN HTML can’t be noncing. Pages served straight from object storage or a static export have no per-request render step, so there is nowhere to mint a fresh value. Either render those pages at the edge (a worker/function that injects a nonce) or switch them to hashes.
- Combine with
strict-dynamic. A nonce only authorizes the scripts you tag directly; scripts those scripts inject still need help. Adding'strict-dynamic'propagates the nonce’s trust to dynamically loaded scripts and lets you drop fragile host allowlists — see CSP strict-dynamic explained.
Rollback (reversible): if noncing breaks legitimate scripts in production, revert script-src to your prior known-good source list (temporarily including 'unsafe-inline' only if you must, accepting the weaker posture) and reload the service. Because the change is header-only and not destructive, removing the 'nonce-…' token and the nonce attributes restores the previous behavior immediately.
# Revert: drop the 'nonce-$request_id' token from script-src, restore the prior list, then:
# nginx -t && systemctl reload nginx
Frequently Asked Questions
How much entropy does a CSP nonce need?
At least 128 bits from a cryptographically secure RNG. In practice generate 16 random bytes (crypto.randomBytes(16), secrets.token_urlsafe(16)) and base64-encode them. Never use Math.random, timestamps, counters, or anything an attacker could predict or reuse.
Can I cache pages that use a nonce?
Not with a shared, per-request nonce. A cache that stores the HTML will replay one nonce to many users or mismatch a regenerated header. Send Cache-Control: no-store on noncing documents, or use hash-based CSP for content you need to cache.
Why does my inline script get blocked even though I set a nonce?
The most common cause is a mismatch: the header and the <script nonce="…"> attribute carry different values (regenerated separately, or the cache served a stale body). They must be the identical value from the same response. A nonce on an external <script src> is also ignored unless the resource is same-origin or covered another way.
Do nonces work for inline styles too?
Yes — style-src 'nonce-…' authorizes inline <style nonce="…"> and style attributes the same way. The same freshness and no-cache rules apply.
Conclusion
Roll nonces out incrementally. Start in Content-Security-Policy-Report-Only in staging so blocked inline scripts surface as reports rather than breakage, confirm via curl that the nonce changes on every request and matches the tag, then promote to enforcing mode on production with Cache-Control: no-store on the noncing routes. Once stable, layer 'strict-dynamic' to drop host allowlists.
Related
- Content-Security-Policy (CSP) reference — the parent header reference and Report-Only rollout.
- Hash-based CSP for inline scripts — the alternative for static, cacheable inline scripts.
- CSP strict-dynamic explained — propagate nonce trust to dynamically loaded scripts.