← The ten steps · Step 6 of 10 · 7 minutes
Nonces and strict-dynamic
The answer to step 5 is to stop describing where scripts come from and start describing which scripts you meant.
The nonce
Generate a random value on every response. Put it on your script tags and in your policy. They have to match, so only script tags that were in the HTML you generated will run.
Content-Security-Policy: script-src 'nonce-r4nd0mV4lu3'
<script nonce="r4nd0mV4lu3" src="/app.js"></script> <script nonce="r4nd0mV4lu3"> // inline is fine too, as long as it carries the nonce </script>
An attacker injecting a script tag cannot supply the nonce, because they do not know it — it is different on every response and it is not in the DOM anywhere they can read it before their payload runs.
The generation rule matters and there are only two parts to it: cryptographically secure randomness, and at least 128 bits. In practice:
PHP base64_encode(random_bytes(16))
Node crypto.randomBytes(16).toString('base64')
Python secrets.token_urlsafe(16)
Go rand.Read on 16 bytes, then base64
The problem the nonce alone does not solve
Real applications load scripts that load other scripts. A tag manager injects vendor tags. A bundler loads chunks on demand. Those child scripts are created by JavaScript at runtime, and nothing gives them a nonce — so a nonce-only policy breaks them.
The old fix was to add every one of those origins to the allowlist, which puts you straight back into step 5.
'strict-dynamic'
Add it and the rule changes fundamentally: a script loaded by an already-trusted script is itself trusted, regardless of where it comes from. Trust propagates by mechanism rather than by origin.
script-src 'nonce-r4nd0mV4lu3' 'strict-dynamic'
Your nonced bootstrap can now load whatever it legitimately needs. An injected
<script> tag still cannot run, because markup injected into the page is parser-inserted
and parser-inserted scripts do not inherit trust — only scripts created programmatically by trusted code
do.
The part that surprises everyone
When 'strict-dynamic' is present, the browser ignores
'self', 'unsafe-inline', and every host expression in that directive. All of
them. This policy:
script-src 'nonce-abc' 'strict-dynamic' 'self' https://cdn.example.com
is exactly equivalent to:
script-src 'nonce-abc' 'strict-dynamic'
That is not a bug, it is the entire point — the allowlist is what was failing, so
'strict-dynamic' switches it off. Those extra sources are there purely as a fallback for
browsers too old to understand 'strict-dynamic', and in current ones they do nothing.
It also means that if you add 'strict-dynamic' to an existing allowlist policy and
something breaks, the fix is not to add more hosts. The fix is to find the script that should
have carried a nonce and give it one.
The target policy
default-src 'self';
script-src 'nonce-{RANDOM}' 'strict-dynamic';
style-src 'self';
img-src 'self';
connect-src 'self';
object-src 'none';
base-uri 'none';
form-action 'self';
frame-ancestors 'none';
report-uri <your endpoint>
No host allowlist for script, so no JSONP endpoint, no gadget, no upload host and no forgotten subdomain can be used against you. This is what a CSP is supposed to look like in 2026, and it is the policy this site serves itself.
You can do this now
Find where your application renders its <head>. That is where the nonce has to be
generated and where it has to reach the template. Knowing which file that is, and whether your
templating layer can pass a value into every script tag, is most of the work of adopting this.