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.