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:

The same value must appear in the header and on the tag; a mismatch silently blocks the script.

Per-request nonce lifecycle For each request the server generates a fresh random nonce, injects it into both the CSP header and the inline script tag, and the browser executes the script only when the two values match. Server randomBytes(16) CSP header script-src 'nonce-X' Script tag <script nonce="X"> Browser match? execute

New random X on every response never cached, never reused

One nonce per response, written into both the header and every trusted inline script tag, then validated by the browser before execution.

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

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.