← The ten steps · Step 7 of 10 · 5 minutes
How nonces go wrong
A nonce is trivial to implement and easy to implement in a way that does nothing. All four failures below produce a policy that reads perfectly in the header.
1. Reused across responses
The most common one. A nonce minted once per session, per deploy, per boot, or cached in a config somewhere means an attacker fetches any page, reads the value out of the HTML, and puts it on their injected script.
The specification is explicit that a nonce should be used for a single response and not reused. Test it in ten seconds:
curl -sI https://example.com/ | grep -i content-security-policy curl -sI https://example.com/ | grep -i content-security-policy
Two different values, or you have this bug.
2. Predictable
Nonces derived from the session ID, the URL, a timestamp, a counter, or any of those passed through a
hash function are computable by whoever can read your JavaScript. "Looks random" is not the standard;
"came from the CSPRNG" is. Use the platform's secure random source, not Math.random(),
rand(), uniqid() or mt_rand().
3. Cached
This one catches careful teams, because the application is correct. The origin mints a fresh nonce per response. A CDN, a reverse proxy or a full-page cache stores that HTML and replays it. Every visitor inside the TTL now shares one nonce, and a per-response nonce has quietly become a per-cache-entry nonce.
Any HTML response carrying a nonce needs to be uncacheable:
Cache-Control: no-store
Your CSS, JS, fonts and images carry no nonce and should still be cached as aggressively as you like. If you genuinely need cached HTML, the nonce has to be injected at the edge — after the cache, not before it.
4. Copied by a library
jQuery's .html() parses the string you give it and, for every <script>
it finds, creates a real script element and copies across src, type and
nonce. Anything reaching that function therefore inherits your page's nonce
and executes. Bootstrap's popover with {html: true, sanitize: false} routes user-controlled
content into the same path.
No policy change fixes this, because from CSP's point of view nothing went wrong — a trusted script
created a script, which is exactly what 'strict-dynamic' permits. The fix is not passing
untrusted markup to those APIs.
Two things that are not bugs
'unsafe-inline' alongside a nonce
Browsers ignore 'unsafe-inline' entirely when a nonce or hash is present. Leaving it in
as a fallback for ancient browsers is harmless — but it also means a broken nonce will not fail
loudly, because the policy still looks like it has a safety net. It does not.
The nonce appearing in the DOM
You will notice the nonce is readable via document.querySelector('script').nonce. That is
fine: any script that can run that has already achieved execution, which is the thing the nonce was
protecting against. Modern browsers also hide the attribute from CSS attribute selectors and from
getAttribute specifically to stop nonce theft through markup injection.
You can do this now
Run the two-curl test above against your own site. If you have a CDN in front of it, run it against the CDN rather than the origin — that is where this bug lives, and the origin will look innocent.