CSP Fail

My nonce is not working

Usually it is working perfectly. The problem is that it is not doing anything.

A nonce only carries trust if an attacker cannot know it. Every failure below produces a policy that looks correct in the header, passes a casual review, and stops nothing.

1. The same nonce on every response

Minting one nonce per session, per deploy, or per anything coarser than per-response means an attacker fetches any page, reads the nonce out of the HTML, and puts it on their own injected script.

script-src 'nonce-YvkYCgF4dXGyT8zKMFDqOO'   ← identical on every reload

CSP3 §8.4 is explicit: a nonce "SHOULD only be used for a single response, and SHOULD NOT be reused". Reload your page twice and diff the header. If the value is the same, this is your bug.

2. A nonce you can derive

Nonces built from the session id, the request path, a timestamp, or any of those run through a published hash are computable by anyone who can read your JavaScript. CSP3 §8.2 asks for "a cryptographically secure random value of at least 128 bits" — not a value that merely looks random.

$nonce = base64_encode(random_bytes(16));   // 128 bits, from the CSPRNG

3. A CDN caching the nonce

This one is nasty because the application is correct. The origin mints a fresh nonce per response, the CDN caches the HTML, and every visitor inside the TTL is served the same cached page with the same nonce. A per-response nonce has quietly become a per-cache-entry nonce.

Any HTML response carrying a nonce needs Cache-Control: no-store, or the nonce has to be injected at the edge rather than cached with the body. Cache your CSS and JS as hard as you like — they carry no nonce.

4. A library copying the nonce for you

jQuery's .html() parses the string you hand it, and for every <script> it finds it creates a real script element and copies across src, type and nonce. Anything that reaches .html() therefore inherits your page's nonce and runs. Bootstrap's popover with {html: true, sanitize: false} reaches it too.

The fix is not a CSP change — it is not passing untrusted markup to those APIs.

How to check yours in thirty seconds

curl -sI https://example.com/ | grep -i content-security-policy
curl -sI https://example.com/ | grep -i content-security-policy

Run it twice. Different nonce each time, and a Cache-Control that will not let a proxy hold on to the response? Then the nonce is real.

Worth knowing

If your policy contains both a nonce and 'unsafe-inline', browsers ignore the 'unsafe-inline' completely. That is usually good news — but it also means a broken nonce will not fail loudly, because the policy still looks like it has a fallback. It does not.

Check your policy →