Nonces And Subresource Integrity For Scripts You Cant Avoid

A nonce protects a script you wrote. Subresource Integrity protects a script someone else hosts. Neither claims to be a perfect wall, both just shrink what's left to attack.

July 25, 20266 min read3 / 3

The last post closed on the durable fix: move inline scripts into real files, served from a trusted origin, so a strict policy doesn't need a growing list of exceptions. Sometimes that refactor genuinely isn't finished yet, or isn't even possible, and this post is for that situation.

When You Can't Just Refactor It Away

A bundler like Webpack or Vite sometimes injects a small inline bootstrap script to get the rest of your app loading. Maybe your team just hasn't finished pulling every inline script out yet.

Either way, default-src 'self' with no exceptions blocks that inline script too, bundler-generated or not. There are two real mechanisms for allowing it back in, and they're a genuine set of trade-offs, not one obviously correct answer.

Option One: The Nonce

A nonce, short for "number used once," works conceptually the same way as the CSRF token covered earlier in this course. It's an unpredictable, per-request value that has to show up in two places at once: the server's CSP header, and the specific inline script tag it's meant to allow.

HTML
<!-- header: Content-Security-Policy: script-src 'nonce-a8f3c9d2' --> <script nonce="a8f3c9d2"> window.__APP_CONFIG__ = { theme: "dark" }; </script>

The browser only runs that script if the two values actually match. Anything else, an injected script that doesn't know the current nonce, gets refused silently, exactly the way a forged request without the right CSRF token gets refused.

Scripts dynamically loaded by an already-trusted, nonce'd script inherit that trust too. That detail matters for how bundlers actually work in practice: a small bootstrap script carries the nonce, and the larger bundle it loads afterward is allowed through as a consequence of that initial trust, not because it carries its own copy of the value.

Two related directives are worth knowing alongside the nonce itself.

  • base-uri stops an injected script from changing where the page's own resources are allowed to load from, closing a loophole that would otherwise let a compromise route around the rest of the policy.
  • Blocking mixed content refuses anything loaded over plain HTTP once the page itself is served over HTTPS. This one is close to free to turn on now that HTTPS is trivial to get, through something like Let's Encrypt or a host's built-in certificate handling, not the multi-week infrastructure project it used to be.

The Real Trade-Off With Nonces

A nonce regenerated on every single request is the most secure version, since an attacker genuinely cannot predict what the next one will be. It also means the HTML itself has to be different on every request to carry that fresh value.

That makes the page effectively uncacheable. A team has to make a real, honest choice about where they land: full per-request freshness, or something cheaper like regenerating the nonce once per build or deploy instead.

Any value that doesn't change per request has a longer window where a captured value could theoretically get reused. That's the cost of the cheaper option, worth knowing rather than assuming away.

Option Two: Subresource Integrity

The second option skips unpredictability entirely and hashes the exact contents of a script instead. Formally, this is Subresource Integrity, or SRI, and it's the clearer fit for a third-party, CDN-hosted library.

HTML
<script src="https://cdn.example.com/analytics.min.js" integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC" crossorigin="anonymous" ></script>

You hash the file, put that hash in the tag, and the browser refuses to execute the script unless what it actually downloads hashes to exactly that value.

If a compromised CDN serves modified code, even a single changed byte, the hash won't match and the browser won't run it. This specifically protects against a supply-chain compromise of a resource you don't control the origin of.

Nonce matching a header value against a script tag versus SRI hashing downloaded content against a pinned hash, both refusing on a mismatch. ExpandNonce matching a header value against a script tag versus SRI hashing downloaded content against a pinned hash, both refusing on a mismatch.

The Trade-Off With SRI

SRI is genuinely resilient to malicious tampering, but it's just as strict about any change, malicious or not. Upgrade a minifier, bump a dependency version, or touch a self-hosted bundled file during a routine build, and the hash breaks along with it, requiring a regenerated value.

That makes SRI fit a resource that changes rarely, a pinned third-party library version, far better than a file your own build process touches constantly. Pin the exact version you tested against, and don't expect to reuse the hash across an upgrade.

Some teams sidestep this trade-off entirely by self-hosting third-party resources instead of relying on a CDN. The traditional argument for the CDN copy, that every site sharing the same public jQuery build gets a free cache hit, is weaker than it used to be.

Modern browsers now partition their cache per origin rather than sharing one cross-site cache the way they once did. The caching benefit some teams assume they're getting from a public CDN may not actually be there anymore. Worth measuring on your own traffic, not assuming.

What All of This Adds Up To

Sanitization, CSP, nonces, SRI, none of it claims to be a perfect, unbreakable wall. The actual goal across all four layers is shrinking the realistic attack surface as far down as practical, and making a successful attack expensive and unlikely rather than promising it's impossible.

That's a more honest way to think about security than any single control ever gets credit for.

The next section of this course moves to a different family of attacks entirely, clickjacking, postMessage abuse, tabnabbing, and JWT security, where the browser itself becomes the thing being tricked rather than the page's own script execution.

The Essentials

  1. A nonce is conceptually the same as a CSRF token, an unpredictable per-request value that has to match between the CSP header and the script tag before the browser runs it.
  2. Scripts loaded by an already-trusted, nonce'd script inherit that trust, which is how a small bootstrap script can vouch for the larger bundle it loads.
  3. base-uri and blocking mixed content are cheap, related wins. base-uri stops an injected script from redirecting where resources load from; blocking mixed content is nearly free now that HTTPS is trivial to get.
  4. A per-request nonce is the most secure but makes the page uncacheable. Regenerating per build or deploy is cheaper but leaves a longer window if a value gets captured.
  5. Subresource Integrity hashes a script's exact contents, protecting against a compromised CDN serving modified code, but it breaks on any change, malicious or not.
  6. None of these layers, sanitization, CSP, nonces, or SRI, claim to be unbreakable. The goal is shrinking the realistic attack surface and making an attack expensive, not promising it's impossible.

Further Reading and Watching