CSP Nonces & Hash-Based Policies

Permalink to "CSP Nonces & Hash-Based Policies"

This workflow is part of Runtime Policy Enforcement & Trusted Types. Subresource Integrity pins the bytes of the external files you reference, but it says nothing about the inline <script> blocks your own server emits — and those inline blocks are exactly what a cross-site scripting payload becomes once it lands in your markup. A Content Security Policy that still carries unsafe-inline treats an attacker’s injected <script>alert(document.cookie)</script> identically to your legitimate analytics snippet. Nonce-source and hash-source directives close that gap by requiring every inline script to prove it was authored by you, not injected by an attacker.

This page walks the full path: generating an unpredictable per-request nonce, injecting it into both the response header and the markup, adding 'strict-dynamic' so a small trusted loader can bootstrap the rest of your bundle, and using source hashes for content that is static at build time. It covers the configuration reference, the real console errors you will hit, the boundary of what this does and does not protect, and how the control sits alongside SRI.

Prerequisites

Permalink to "Prerequisites"

Conceptual Foundation: How Nonce-Source and Hash-Source Work

Permalink to "Conceptual Foundation: How Nonce-Source and Hash-Source Work"

The CSP Level 3 specification (W3C Working Draft, §6.6.4.5 Match nonces to source list and §6.6.4.6 Does a source list allow all inline behavior for type?) defines two ways to authorize an inline or external script without falling back to the blanket unsafe-inline keyword.

A nonce-source is the token 'nonce-<base64>' in a script-src or style-src directive. The browser will execute an inline script only if that script’s element carries a matching nonce attribute. Because the value is generated fresh on the server for each response and is unguessable, an attacker who injects markup into the page cannot know the nonce and therefore cannot author a script the policy will accept.

A hash-source is the token 'sha384-<base64>' (or sha256/sha512) in the same directive. Instead of matching an attribute, the browser computes the digest of the inline script’s exact text content and runs it only if the digest appears in the source list. No per-request server state is required, which makes hashes the right tool for static, cacheable content.

Two rules from the specification are load-bearing:

  • The presence of a valid nonce-source or hash-source cancels unsafe-inline. A browser that understands nonces ignores unsafe-inline whenever a nonce is present, which is what lets old browsers fall back gracefully while modern ones enforce strictly.
  • The nonce must be, in the spec’s words, “unique per response” and “generated using a cryptographically secure random number generator” with at least 128 bits of entropy.

'strict-dynamic' extends a nonce or hash: a script the browser already trusts (because it matched a nonce or hash) may create further scripts via document.createElement or import, and those inherit trust — while host allowlists and unsafe-inline are discarded for that directive. This is how a single nonced bootstrap script can load an entire application without enumerating every CDN origin.

The diagram below shows how one value threads through the response header and the markup, and how the browser reconciles them.

Per-request CSP nonce flow A flow diagram: the server generates a random nonce per request, writes the same value into the Content-Security-Policy response header and into the nonce attribute of an inline script tag, and the browser compares the two. A matching inline script is executed and allowed; an injected script with no nonce is blocked. Server CSPRNG generates 128-bit nonce Response header script-src 'nonce-r4Nd0m' HTML markup <script nonce="r4Nd0m"> Browser matches header vs attribute Match → Allow Injected <script> (no nonce) → Block

Step 1 — Generate a Per-Request Nonce

Permalink to "Step 1 — Generate a Per-Request Nonce"

Generate the nonce from a cryptographically secure source at the very start of request handling, before any markup is rendered, so the same value is available to both the header and the template. A 128-bit (16-byte) value base64-encoded is the specification minimum.

// Express middleware — runs first for every request
import { randomBytes } from 'node:crypto';

export function cspNonce(req, res, next) {
  // 16 bytes = 128 bits of entropy, base64-encoded
  res.locals.cspNonce = randomBytes(16).toString('base64');
  next();
}

Expected result — inspect the value on any request and confirm it is a ~24-character base64 string that changes on every reload:

res.locals.cspNonce → "8IBTHwOdC0k7v0Kf4hYou=="   (request 1)
res.locals.cspNonce → "Zm9vYmFyMTIzNDU2Nzg5MA=="   (request 2, different)

The full generation contract — entropy sizing, encoding, and the framework variants for Nginx and edge runtimes — is documented in Generating Per-Request CSP Nonces.


Step 2 — Inject the Nonce into Markup

Permalink to "Step 2 — Inject the Nonce into Markup"

Every first-party inline script and style the response emits must carry the nonce attribute. Never place the nonce on external <script src> tags as a substitute for SRI — use it on inline blocks and on the loader that bootstraps your app.

<!-- Server-rendered template; ${nonce} is res.locals.cspNonce -->
<script nonce="8IBTHwOdC0k7v0Kf4hYou=">
  window.__APP_CONFIG__ = { region: "eu-west-1" };
</script>

<!-- The bootstrap loader that pulls in the rest of the bundle -->
<script nonce="8IBTHwOdC0k7v0Kf4hYou=" src="/static/app.entry.js"></script>

Expected result — viewing source on the delivered page shows the identical nonce on every first-party inline block, and that value equals the token in the response header from Step 3.

Inline event-handler attributes (onclick="…") and javascript: URLs cannot carry a nonce and will be blocked. Refactor them into nonced script blocks that call addEventListener.


Step 3 — Set the Content-Security-Policy Header

Permalink to "Step 3 — Set the Content-Security-Policy Header"

Emit the header with the same nonce, plus 'strict-dynamic' so the nonced loader can pull in the rest of your scripts without a host allowlist. Keep object-src 'none' and base-uri 'none' — both are required to make a nonce policy actually strict.

// Express — set the header using the nonce from Step 1
app.use((req, res, next) => {
  const nonce = res.locals.cspNonce;
  res.setHeader(
    'Content-Security-Policy',
    [
      `script-src 'nonce-${nonce}' 'strict-dynamic' https: 'unsafe-inline'`,
      `object-src 'none'`,
      `base-uri 'none'`,
      `report-to csp-endpoint`,
    ].join('; ')
  );
  next();
});

The https: and 'unsafe-inline' tokens here are deliberate fallbacks for old browsers — a CSP3-aware browser sees the nonce and 'strict-dynamic' and ignores both, while a CSP1 browser that ignores 'strict-dynamic' still gets some protection from https:. Expected result — the response carries a header like this, with the nonce matching the markup:

Content-Security-Policy: script-src 'nonce-8IBTHwOdC0k7v0Kf4hYou=' 'strict-dynamic' https: 'unsafe-inline'; object-src 'none'; base-uri 'none'; report-to csp-endpoint

Step 4 — Verify Enforcement

Permalink to "Step 4 — Verify Enforcement"

Load the page with DevTools open. Your nonced inline scripts should execute normally. To prove the policy actually blocks injection, paste a script tag with no nonce into the Elements panel or via the console:

// Run in the DevTools console — simulates an injected inline script
const s = document.createElement('script');
s.textContent = 'window.__pwned = true';
document.body.appendChild(s);

Expected result — the injected script is refused and the console prints a violation naming the missing nonce (see Troubleshooting for the exact text). window.__pwned remains undefined.


Step 5 — Enforce and Remove Unsafe-Inline

Permalink to "Step 5 — Enforce and Remove Unsafe-Inline"

Once every first-party inline block carries a nonce (or a hash, for static content) and your report-to endpoint shows no legitimate violations across a full traffic cycle, you can tighten the policy. For static, cacheable routes where a per-request nonce is impossible, switch to source hashes as described in Migrating from unsafe-inline to Hash-Based CSP.

Content-Security-Policy: script-src 'nonce-8IBTHwOdC0k7v0Kf4hYou=' 'strict-dynamic'; object-src 'none'; base-uri 'none'; report-to csp-endpoint

Expected result — the page still functions, no legitimate script is blocked, and the policy no longer contains unsafe-inline. This is the enforcing end state.


Configuration Reference

Permalink to "Configuration Reference"
Directive / token Valid values Security implication
'nonce-<base64>' 128-bit+ CSPRNG value, base64 Must be unique per response; reuse or predictability defeats the control
'sha256-' / 'sha384-' / 'sha512-' base64 digest of inline content SHA-384 preferred; recompute whenever the inline text changes by one byte
'strict-dynamic' present / absent Propagates trust to dynamically inserted scripts; disables host allowlists and unsafe-inline for that directive
'unsafe-inline' present / absent Ignored when a nonce or hash is present; keep only as an old-browser fallback
object-src 'none' Required — without it, <object>/<embed> can bypass a nonce policy
base-uri 'none' or 'self' Required — blocks <base> tag injection that redirects relative script URLs
report-to endpoint group name Enables Report-Only rollout and production violation telemetry

Integration with Adjacent Tooling

Permalink to "Integration with Adjacent Tooling"

Nonce and hash policies are one layer of runtime script control and are strongest when combined with the fetch-time integrity guarantees that SRI provides. The two answer different questions: a nonce authorizes whether a script may run, while an integrity attribute verifies that an external file’s bytes are the ones you audited. For external <script src> references, keep Configuring Content Security Policy with SRI in force alongside the nonce so a compromised CDN cannot swap the bootstrap file even though it loaded from a nonced tag.

When a nonced or 'strict-dynamic' loader injects further scripts at runtime, those injected tags should still carry integrity and crossorigin; the browser’s SRI enforcement lifecycle is detailed in Browser Enforcement & Security Boundaries. And because a strict nonce policy will block any script that fails to load or is refused, pair it with the recovery patterns in Handling SRI Failures with onerror Handlers so a blocked resource degrades gracefully instead of white-screening the page.


Troubleshooting

Permalink to "Troubleshooting"

Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'nonce-…'". Either the 'unsafe-inline' keyword, a hash ('sha256-…'), or a nonce ('nonce-…') is required to enable inline execution.

The inline <script> has no nonce attribute, or its nonce does not match the header. Confirm the template writes the same res.locals value into both the tag and the header, and that no proxy is rewriting one but not the other. This is the most common failure and usually means the header and markup were generated from two different nonce values.

Refused to execute inline script … a hash … or a nonce … is required on a page that worked yesterday

A shared cache stored a response whose body carried a nonce, and it is now serving that stale nonce with a freshly generated header (or vice versa). Mark nonce-bearing HTML Cache-Control: no-store, or move that route to hash-based sources. Caching a nonce is covered in depth in Generating Per-Request CSP Nonces.

Refused to execute inline event handler because it violates … script-src

An onclick, onload, or other inline handler attribute is present. Nonces cannot be applied to attribute-level handlers. Move the logic into a nonced <script> block using addEventListener. This surfaces most often after enabling a nonce policy on legacy templates.

Injected third-party script runs even though it has no nonce

You have 'strict-dynamic' and a trusted first-party script is programmatically appending the third-party tag, so it inherits trust. That is 'strict-dynamic' working as designed — trust flows through createElement-inserted scripts. If you need to stop that, the third-party must be loaded by markup that the policy evaluates directly, not injected by trusted JavaScript.

Content-Security-Policy-Report-Only shows violations but nothing is blocked

The header name is Content-Security-Policy-Report-Only, which only reports. This is expected during rollout. Switch to the enforcing Content-Security-Policy header name once the report stream is clean.

Nonce works in Chrome but inline styles are still blocked

nonce- on script-src does not cover styles. Add a matching 'nonce-<value>' to style-src (or hash your inline styles) if you have inline <style> blocks. Nonce sources are per-directive.


Security Boundary

Permalink to "Security Boundary"

A nonce or hash policy enforces that only inline and external scripts you authorized may execute. It does not:

  • Verify the bytes of an external script. A nonced <script src> tag will run whatever the origin returns. Byte-level integrity is SRI’s job — see Configuring Content Security Policy with SRI.
  • Stop DOM-based XSS that flows into a non-script sink. An injection that writes into innerHTML without creating a <script> may still cause damage; that class of bug needs Trusted Types, not a nonce.
  • Protect against a compromised server. If the attacker controls the response, they also control the nonce and can author a valid script. CSP defends against injection into an otherwise-trusted response, not against origin compromise.
  • Help if the nonce is guessable or reused. A nonce generated with Math.random(), taken from a timestamp, or cached and served to many users provides no protection — an injected script can reproduce it.

Frequently asked questions

Permalink to "Frequently asked questions"
Do I need a new CSP nonce on every request?

Yes. A nonce is, literally, a number used once. It must be regenerated from a cryptographically secure random source on every HTTP response and must never be reused across requests or stored in a shared cache. A predictable or reused nonce lets an injected script guess the value and satisfy the policy, which defeats the control entirely.

Should I use a nonce or a hash for inline scripts?

Use a nonce when your server renders HTML dynamically and can inject a fresh value per response. Use a hash when the inline content is static and known at build time — a pre-rendered or fully cached page where a per-request nonce is impossible. Hashes need no server-side randomness but must be recomputed whenever the inline content changes by even one byte.

What does 'strict-dynamic' actually do?

It propagates the trust granted by a nonce or hash to any script that a trusted script loads programmatically, and it tells the browser to ignore host allowlists and unsafe-inline for script-src. This lets a small nonced loader bootstrap the rest of your bundle without enumerating every CDN host, while still blocking parser-inserted injected markup.

Can I cache a page that contains a CSP nonce?

Not in a shared cache. If a CDN or reverse proxy stores a response whose body and header share one nonce, every visitor receives the same value and it is no longer unpredictable. Either mark nonce-bearing responses Cache-Control: no-store, or switch that route to hash-based sources so the cached body carries no per-request secret.

How does this relate to Subresource Integrity?

SRI verifies the bytes of externally fetched scripts against a hash in the integrity attribute; CSP nonces and hashes authorize which inline and external scripts may run at all. SRI answers “is this file the one I audited”; CSP answers “is this script allowed to execute”. A hardened page uses both, which is why this workflow links out to the SRI-focused pages throughout.


Permalink to "Related"

Articles in This Topic

Generating Per-Request CSP Nonces
Migrating from unsafe-inline to Hash-Based CSP
Back to Runtime Policy Enforcement & Trusted Types