If you’re choosing between CSP hashes and nonces, the short version is this:
- Use nonces when your page is rendered dynamically and you control the HTML response.
- Use hashes when the inline code is static and rarely changes.
- If you’re dealing with modern third-party loaders, nonce +
strict-dynamicis usually the cleanest option.
I’ve seen teams overcomplicate this. CSP gets much easier once you stop treating hashes and nonces as interchangeable. They solve similar problems, but they fit very different delivery models.
The problem both solve
CSP blocks inline JavaScript by default when you use a restrictive script-src. That’s good, because inline script is one of the easiest XSS execution paths.
These are blocked under a strict policy:
<script>
window.appConfig = { apiBase: "/api" };
</script>
<button onclick="doSomething()">Click</button>
To allow inline <script> safely, you generally choose one of these:
- Nonce-based allowlisting
- Hash-based allowlisting
They both let specific inline scripts run without falling back to 'unsafe-inline'.
Nonce-based CSP
A nonce is a random per-response token. You generate it on the server, add it to the CSP header, and also attach it to trusted <script> tags.
Example header
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-r4nd0mBase64Value'; object-src 'none'; base-uri 'self';
Matching script tag
<script nonce="r4nd0mBase64Value">
window.appConfig = {
apiBase: "/api",
env: "production"
};
</script>
If the nonce in the tag matches the nonce in the response header, the browser runs the script.
Why I like nonces
Nonces are great when:
- HTML is generated dynamically
- inline bootstrap code changes often
- you need to pass server-side data into the page
- you use a trusted script loader and want
strict-dynamic
This is the real advantage: you don’t need to recalculate a hash every time the inline content changes.
Express example
import crypto from "node:crypto";
import express from "express";
const app = express();
app.get("/", (req, res) => {
const nonce = crypto.randomBytes(16).toString("base64");
res.setHeader(
"Content-Security-Policy",
[
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}'`,
"object-src 'none'",
"base-uri 'self'"
].join("; ")
);
res.send(`
<!doctype html>
<html>
<head><title>Nonce CSP</title></head>
<body>
<script nonce="${nonce}">
window.appConfig = { apiBase: "/api", userId: 123 };
</script>
<script nonce="${nonce}" src="/app.js"></script>
</body>
</html>
`);
});
app.listen(3000);
Nonce rules that people get wrong
-
Generate a fresh nonce for every response Not per deploy. Not per process start. Per response.
-
Use cryptographically strong randomness Don’t invent your own token format.
-
Don’t expose the nonce in places attackers can easily reuse If you have an XSS that can read DOM attributes, a nonce is not magic. CSP reduces risk; it doesn’t erase insecure rendering.
-
Nonce only works for elements that support it Mostly
<script>and<style>.
Hash-based CSP
A hash is a digest of the exact inline script content. Instead of saying “scripts with this token may run,” you say “scripts with exactly this content may run.”
Example header
Content-Security-Policy: default-src 'self'; script-src 'self' 'sha256-2v0m0T4V8z1VwZ5m1YqY0j7YH7WQ9l8m4uH0F8xVYkQ='; object-src 'none'; base-uri 'self';
Matching script
<script>
console.log("hello from inline script");
</script>
The hash must be computed from the exact contents inside the <script> tag. Whitespace counts. Newlines count. A tiny formatting change breaks it.
Generate a SHA-256 hash
Node.js:
import crypto from "node:crypto";
const script = 'console.log("hello from inline script");';
const hash = crypto.createHash("sha256").update(script, "utf8").digest("base64");
console.log(`'sha256-${hash}'`);
OpenSSL:
printf 'console.log("hello from inline script");' \
| openssl dgst -sha256 -binary \
| openssl base64
Then put the result in your policy:
Content-Security-Policy: script-src 'self' 'sha256-...';
Why hashes are useful
Hashes are great when:
- inline code is static
- pages are cached heavily
- content is built once and served many times
- you want deterministic policies without server-side nonce generation
This is common for static sites, docs sites, and pages with one tiny bootstrap snippet.
Side-by-side comparison
Nonce-based
Pros
- Easy for dynamic inline code
- Works well with SSR apps
- Pairs nicely with
strict-dynamic - No need to recalculate hashes when content changes
Cons
- Requires server-side response customization
- Makes full-page caching trickier unless your edge injects the nonce
- You must plumb the nonce into templates correctly
Hash-based
Pros
- Great for static HTML
- Cache-friendly
- No per-request token generation
- Very explicit allowlist
Cons
- Fragile: whitespace or formatting changes break it
- Painful if inline content changes often
- Annoying in templates that embed dynamic values
My rule of thumb: if your inline script contains request-specific data, hashes are the wrong tool.
Real-world nonce example with strict-dynamic
Here’s the real CSP header from headertest.com:
content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-Y2QzMjI4ZjAtZGE1OC00M2FhLWExZmQtNWQ4ODA1NzlhNzZl' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://api.headertest.com https://tallycdn.com https://or.headertest.com wss://or.headertest.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com; frame-src 'self' https://consentcdn.cookiebot.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'
The interesting part is this:
script-src 'self' 'nonce-Y2QzMjI4ZjAtZGE1OC00M2FhLWExZmQtNWQ4ODA1NzlhNzZl' 'strict-dynamic' ...
That tells me they trust a nonce-bearing bootstrap script, and then allow scripts loaded by that trusted script. That’s usually a better fit for tag managers than trying to maintain a giant host allowlist forever.
A minimal version looks like this:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{{NONCE}}' 'strict-dynamic'; object-src 'none'; base-uri 'self';
HTML:
<script nonce="{{NONCE}}" src="/bootstrap.js"></script>
And /bootstrap.js can dynamically load other scripts:
const s = document.createElement("script");
s.src = "https://www.googletagmanager.com/gtm.js?id=GTM-XXXXXXX";
document.head.appendChild(s);
For modern browsers, strict-dynamic means the trust flows from the nonce-approved script. That’s powerful, and honestly one of the best reasons to prefer nonces in app-style deployments.
Copy-paste policies
1. Static site with one fixed inline script: use a hash
Content-Security-Policy: default-src 'self'; script-src 'self' 'sha256-REPLACE_WITH_HASH'; object-src 'none'; base-uri 'self'; frame-ancestors 'none';
Inline script:
<script>
document.documentElement.classList.add("js");
</script>
2. Server-rendered app with dynamic config: use a nonce
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{{NONCE}}'; object-src 'none'; base-uri 'self'; frame-ancestors 'none';
HTML:
<script nonce="{{NONCE}}">
window.__CONFIG__ = {
csrfToken: "{{csrfToken}}",
apiBase: "/api"
};
</script>
3. Nonce + strict-dynamic for trusted loaders
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{{NONCE}}' 'strict-dynamic'; object-src 'none'; base-uri 'self';
HTML:
<script nonce="{{NONCE}}" src="/loader.js"></script>
For more ready-to-use policy patterns, you can also browse https://csp-examples.com.
Common mistakes
Using 'unsafe-inline' with hashes or nonces
That usually defeats the point for scripts. If you’re trying to harden CSP, remove it from script-src.
Hashing the wrong content
You hash only the text inside the <script> tag, not the tag itself.
Forgetting that templating changes hashes
This breaks constantly:
<script>
window.userId = "{{ user.id }}";
</script>
If user.id changes, the hash changes. That’s a nonce use case.
Reusing one nonce everywhere forever
That turns a nonce into a static secret, which is pointless.
Which one should you choose?
Pick hashes if all of this is true:
- your HTML is mostly static
- inline code rarely changes
- you want easy CDN caching
- you don’t want per-request CSP generation
Pick nonces if any of this is true:
- you render HTML dynamically
- inline script contains runtime data
- you use SSR frameworks
- you want
strict-dynamic - you load third-party scripts through a trusted bootstrap script
If I had to give one opinionated default for most modern web apps: use nonces. Hashes are excellent, but mostly for static content and very small controlled snippets. In app backends, nonces fit the workflow better and scale with fewer weird edge cases.
If you want the browser’s exact behavior and directive details, the official reference is the MDN CSP documentation: https://developer.mozilla.org/docs/Web/HTTP/CSP.