All posts

How to set up a CSP nonce, per request, in Express, Next.js, and nginx

CentralCSP Team ·

Last update:

A nonce lets you allow specific inline scripts under a Content Security Policy (CSP) without re-enabling all of them. A CSP is an HTTP response header that tells the browser which scripts it may run, and by default it blocks inline scripts. A nonce is a fresh random token you put both in the header and on each inline <script> you trust; the browser runs only the inline scripts whose nonce attribute matches the header value.

The short version: generate a unique, crypto-random nonce on every response, put it in script-src as 'nonce-...', and repeat it in the nonce attribute of each trusted inline script. The catch is that the nonce must change per response and must come from app code that renders the HTML, which is why a static web server like nginx cannot do it alone.

New to CSP? Get started with Content Security Policy covers the basics, and why you should never use unsafe-inline in CSP explains why a nonce beats 'unsafe-inline'.

What a nonce is and the two rules

A nonce ("number used once") is a short random value the server generates per response. It appears in the policy header and on each trusted inline script. Because the value is unpredictable and changes every time, an attacker who injects an inline script cannot supply the right nonce, so the injected script stays blocked while yours runs.

Two rules make a nonce safe:

  • Unique per response. Generate a new nonce on every HTTP response, never reuse one across requests. A reused nonce can be read from one page and replayed by an attacker, which defeats the point.
  • Cryptographically random. Generate it from a secure source with enough entropy (16 bytes, 128 bits, is the common recommendation). Never use Math.random(); it is predictable.

Generate the nonce

In Node.js the built-in crypto module gives you a secure value. Sixteen random bytes, base64-encoded, is the standard:

const crypto = require("crypto");
const nonce = crypto.randomBytes(16).toString("base64");

Generate this once per request, then use the same value in both the header and the template for that response.

Pair the nonce with 'strict-dynamic'

A bare nonce trusts only the exact tags you put it on. The moment one trusted script injects another (an analytics loader appending a tracker, a tag manager writing more script tags), the injected script has no nonce and is blocked. Adding 'strict-dynamic' tells the browser to propagate trust from a nonced script to the scripts it loads, so a trusted loader can pull in what it needs without a host allowlist.

Content-Security-Policy: script-src 'nonce-r4nd0m' 'strict-dynamic'; object-src 'none'; base-uri 'none'

Use a nonce with 'strict-dynamic' as the default shape for a strict policy.

Express

In Express, generate the nonce in middleware, stash it on res.locals so your templates can read it, then set the header. Helmet accepts a function for a directive value, which runs per response and is the clean place to read the nonce:

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: {
      directives: {
        "script-src": [
          (req, res) => `'nonce-${res.locals.nonce}'`,
          "'strict-dynamic'",
        ],
        "object-src": ["'none'"],
        "base-uri": ["'none'"],
      },
    },
  })
);

Then emit the same nonce in the template for each trusted inline script:

<script nonce="<%= nonce %>">
  // your trusted inline script
</script>

Because Helmet calls the function on every response and the middleware sets a fresh res.locals.nonce each time, the header and the markup always carry the same per-request value.

Next.js

Next.js generates the nonce in middleware and forwards it to the rendering layer, so the App Router setup has a few moving parts (reading the nonce in a Server Component, propagating it to the framework's own inline scripts). It is enough that it gets its own walkthrough.

For the full App Router setup end to end, see how to set up a CSP nonce in Next.js. The same per-response and crypto-random rules above apply; the article covers wiring the nonce through middleware and into the rendered output.

nginx

nginx serves responses but does not render your HTML, so on its own it cannot put a matching nonce into both the header and each <script> tag for the same request. A nonce only works when the same code that writes the markup also writes the header value. If nginx reverse-proxies an app you control, the right answer is to generate the nonce in that app (Express, Next.js, your backend framework) and let nginx pass the response through unchanged.

The exception is a purely static site with no backend to do that. There you can put an unguessable placeholder in your HTML and have nginx mint a fresh $request_id per request and swap the placeholder for it with sub_filter, so the header and the tags carry the same value. It comes with real caveats (the placeholder must be unguessable, sub_filter cannot touch a compressed body, and add_header inheritance is easy to break), so it has its own walkthrough: how to add a fresh CSP nonce to a static site with nginx. If your inline scripts never change, a CSP hash is simpler still at the nginx layer, because a hash does not need per-request generation.

For setting the header itself across stacks, see how to set a CSP header in every framework.

See it in your reports

A nonce mismatch (a stale value, a script you forgot to tag) shows up as a blocked inline script. Roll the policy out in Report-Only first so the browser reports those blocks instead of breaking the page. CentralCSP collects those reports from real traffic and groups them, so you can confirm every trusted inline script is tagged before you enforce. You can start a free trial and point a Report-Only header at it.

For the full directive and keyword reference, see the CSP policy reference.

Violation rows whose blocked origin reads inline rather than a host

Frequently asked questions

Can nginx generate a CSP nonce?

Not while it is proxying an app, because nginx does not render your HTML and cannot put a matching nonce into both the header and each inline script for that request. Generate the nonce in the app instead. For a purely static site, you can use an unguessable placeholder plus $request_id and sub_filter, covered in how to add a fresh CSP nonce to a static site with nginx.

How long should a CSP nonce be?

Generate it from a secure random source with at least 128 bits (16 bytes) of entropy, base64-encoded. The exact length matters less than it being unpredictable and unique per response.

Do I need 'strict-dynamic' with a nonce?

You need it when a trusted script injects further scripts (a tag manager or analytics loader). 'strict-dynamic' propagates trust from a nonced script to the scripts it loads, so they are not blocked for lacking a nonce.

Sources