CSP nonces vs hashes: which should you use?
Nonces need a fresh random value on every response, hashes need stable script content. Which strict CSP strategy fits your stack: server-rendered apps, static sites, CDNs, and the caching traps in between.
Both nonces and hashes solve the same problem: allowing the inline and dynamic scripts you meant to ship while blocking everything an attacker injects, so you can finally delete 'unsafe-inline' from your policy. They differ in one operational detail that decides everything else. A nonce must change on every response. A hash must not change at all.
That gives you the short answer. Server-rendered pages where you can generate a random value per request: use nonces. Static sites, aggressively cached pages, anything served identically to every visitor: use hashes. Either way, pair the result with 'strict-dynamic' if your scripts load other scripts.
The rest of this article is the reasoning, because the failure modes hide in the details.
How nonces work, and where they break
A nonce is a random value you generate for each response, put in the header, and repeat on every legitimate script tag:
Content-Security-Policy: script-src 'nonce-8IBTHwOdqNKAWeKl7plt8g'
<script nonce="8IBTHwOdqNKAWeKl7plt8g">/* runs */</script>
<script>/* blocked: no nonce */</script>
The browser executes only the scripts carrying the current nonce. An attacker who injects markup does not know the value, so their script stays dead. For this to hold, the nonce needs at least 128 bits from a cryptographic random source, base64-encoded, and it must never repeat.
That last requirement is where real deployments fail. Full-page caching at a CDN freezes one nonce into the cached HTML and serves it to thousands of visitors. The policy still “works” in the sense that pages render, but the nonce is now a constant that any attacker can read from view-source and reuse. We’ve seen this exact setup pass a security review because nobody looked at two responses in a row.
So the honest prerequisite for nonces is: every layer between your template engine and the visitor either regenerates the nonce or doesn’t cache the HTML. Server-rendered frameworks handle this well (the main CentralCSP site has a Next.js nonce walkthrough). Static exports and edge-cached marketing pages do not.
How hashes work, and where they break
A hash source authorizes one exact script body:
Content-Security-Policy: script-src 'sha256-B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF8='
Any inline script whose SHA-256 digest matches runs. Everything else is blocked. No server-side generation, no caching constraints. The header is as static as the page, which is exactly what a CDN wants.
The trade-off is brittleness. The hash covers the script byte for byte, whitespace included. A reformatted file, a template that interpolates a user ID, an A/B test injecting a variant: each produces a different digest and a blocked script. Hashes suit pages whose inline scripts are finite and stable, which describes most static sites and very few personalized apps. Multi-tenant products sit right on that line, and CSP for SaaS platforms works through what changes once every page is rendered for a logged-in customer.
Two extensions worth knowing. 'unsafe-hashes' lets hash matching cover inline event handlers (onclick="..."), a concession to legacy markup. And in CSP Level 3 a hash can authorize an external script whose tag carries a matching integrity attribute, which quietly combines CSP and Subresource Integrity into one mechanism. Chromium supports this, other engines have trailed, test before depending on it.
strict-dynamic changes the question
If your entry scripts load further scripts (every tag manager, most bundlers’ chunk loading), listing all of them is a treadmill. 'strict-dynamic' fixes this: scripts you authorized via nonce or hash may load additional scripts, and those inherit trust. Host allowlists in the directive are ignored in modern browsers when it’s present.
Which means the nonce-or-hash decision only has to cover your entry points. Authorize the three script tags in your HTML and let trust propagate, instead of maintaining a fifty-line allowlist that drifts out of date. Our take: if you’re going through the pain of removing 'unsafe-inline', go directly to nonce-plus-strict-dynamic or hash-plus-strict-dynamic. The half-measure of a giant host allowlist costs the same effort and ages worse.
Deciding in one table
| Your situation | Use |
|---|---|
| Server-rendered app (per-request HTML) | Nonces + 'strict-dynamic' |
| Static site, JAMstack, full-page CDN cache | Hashes + 'strict-dynamic' |
| Mixed: cached shell, dynamic fragments | Hashes for the shell, and keep fragments free of inline scripts |
Legacy app full of onclick= attributes | Hashes + 'unsafe-hashes' while you migrate handlers out |
| Third-party scripts you don’t control | 'strict-dynamic' propagation, or host sources as a fallback |
Finding out what you’d actually have to hash
The step everyone skips: before choosing, inventory the inline scripts you really serve. A report-only policy without 'unsafe-inline' makes every browser list them for you, violation by violation, sample included. CentralCSP aggregates those reports and its Builder turns the observed traffic into a reviewable policy, with the report-sha256 mechanism capturing the digest of each script the browser saw. That inventory usually settles the nonce-vs-hash debate on its own: a dozen stable inline scripts point to hashes, an unpredictable stream of them points to nonces and a talk with whoever keeps injecting scripts.
Whichever you choose, the destination is the same policy shape: no 'unsafe-inline', entry points authorized explicitly, trust propagated with 'strict-dynamic', violations reported. The hashes and nonces reference covers the syntax details when you get there.
Frequently asked questions
Can I use CSP nonces on a static site or behind a CDN?
Not safely. A nonce must be a fresh random value on every response, and static hosting or full-page CDN caching serves the same bytes to everyone, so the nonce repeats. A repeated nonce is a password taped to the door. Static and cached pages should use hashes.
Why is my nonce not working?
The three usual causes: the nonce in the header does not exactly match the nonce attribute in the script tag, the page is cached so the header and the HTML carry nonces from different responses, or a proxy strips or rewrites one of the two. Compare the header and the DOM value on a single response before debugging anything else.
Do hashes work for external scripts?
In CSP Level 3, yes: a hash source can authorize an external script when the script tag carries a matching integrity attribute. Chromium supports it. Coverage in other engines has lagged, so test before relying on it and keep a host source or nonce fallback.
What is unsafe-hashes for?
It extends hash matching to inline event handler attributes like onclick. It exists as a migration aid for legacy markup you cannot rewrite yet. Prefer moving handlers into scripts with addEventListener. Each unsafe-hashes entry is a small hole you have to justify.