Skip to content
DERKONLINE

Pin Third-Party Scripts With Subresource Integrity

The Polyfill.io attack hit 380,000 sites because nobody pinned the script. SRI plus CSP makes that class of supply-chain breach a non-event.

Derrick S. K. Siawor7 min read

On June 25, 2024, security researchers discovered that cdn.polyfill.io, a JavaScript service embedded on hundreds of thousands of websites, had been quietly turned into a weapon. The domain and its GitHub account had been bought by a new owner in February 2024. The new owner modified the script so it injected malicious code into every site that loaded it. By early July, over 380,000 hosts were still pulling the poisoned script, including properties tied to Warner Bros, Hulu, Mercedes-Benz, and Pearson.

The clever part, and the part that should scare any engineer, is how the payload behaved. It only activated on specific mobile devices. It avoided admin users. It delayed execution. It generated its payload dynamically based on HTTP headers so it would not look the same twice. Most of those 380,000 site owners had no idea anything was wrong, because nothing in their own code had changed. They were running exactly the <script src="https://cdn.polyfill.io/..."> tag they had shipped years earlier. The bytes behind that URL changed underneath them, and that was enough.

Why this keeps happening

Every third-party script tag is an open invitation. When you write <script src="https://some-cdn.com/widget.js">, you are telling every visitor's browser to download whatever bytes that URL returns, today, and run them with full access to your page. Cookies, the DOM, form inputs, the lot. You are trusting not just that the vendor is honest today, but that their CDN is uncompromised, their domain registration is current, their build pipeline is clean, and their account is not for sale.

That trust is transitive and it is brittle. The analytics snippet, the chat widget, the A/B testing tool, the font loader, the tag manager: each one is a remote code path into your site that you do not control, and the same scripts that open this hole are also the ones wrecking your page speed. When any one of them is compromised, the attacker is running inside your origin. This is supply-chain risk, and a script tag is the most direct form of it on the web. When the worst does happen, audit logs that actually help after a breach are what let you reconstruct which version of a poisoned script was live and when. The flip side is your own bundle: even a clean third-party graph means nothing if you are shipping API keys in your frontend bundle for anyone to read.

Subresource Integrity pins the bytes

Subresource Integrity (SRI) is a browser feature that solves exactly the Polyfill problem. You add an integrity attribute to the script or stylesheet tag containing a cryptographic hash of the exact bytes you expect. Before the browser executes the file, it hashes what it downloaded and compares. If the bytes do not match the hash, the browser refuses to run the file.

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

Now if that CDN is compromised and starts serving a malicious version, the hash will not match and the browser will block it. The Polyfill attack would have been neutralized on day one for any site using SRI, because the swapped-in malicious bytes could never produce the expected hash. The script simply would not run.

Generate the hash yourself from the file you reviewed. On the command line:

cat widget-3.4.1.js | openssl dgst -sha384 -binary | openssl base64 -A

Prefix the output with sha384- and that is your integrity value. The crossorigin="anonymous" attribute is required for SRI to work on cross-origin resources, because the browser needs CORS to be able to read and verify the response.

The one hard requirement: pin to a version

SRI only works on files that do not change. This is the trade-off, and it is a feature, not a limitation. If a vendor serves you https://cdn.example.com/widget/latest.js, you cannot pin it, because "latest" changes whenever they push an update. You must reference a specific, immutable, versioned URL, hash that exact version, review what it does, and only bump the hash when you deliberately upgrade to a new version you have reviewed.

This forces a discipline that is healthy anyway: you stop auto-running whatever a vendor pushes and start running only versions you have explicitly chosen. The Polyfill service was, by design, a dynamic endpoint that returned different code per request. That is precisely the pattern SRI cannot protect, and precisely the pattern you should not be running unpinned third-party code through in the first place.

Content Security Policy is the second lock

How CSP and SRI verify a script: allowlist the origin, then hash the bytes against the integrity attribute

SRI verifies the bytes of scripts you have explicitly tagged. Content Security Policy (CSP) controls which origins are allowed to load resources at all, and it catches the things SRI cannot. If an attacker manages to inject a brand-new script tag pointing at their own server, SRI never enters the picture, because there is no integrity attribute on a tag the attacker wrote. CSP stops it by refusing any script from an origin not on your allowlist.

A tight CSP is one piece of a broader header posture, alongside the rest of the security headers every Next.js app should ship. A tight CSP for a site with a couple of known third parties looks like this:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://cdn.example.com;
  connect-src 'self' https://api.example.com;
  object-src 'none';
  base-uri 'self';

There was once a require-sri-for CSP directive meant to reject any script tag without an integrity attribute, but it was experimental, never shipped widely, and has been dropped from browsers, so do not rely on it. The practical way to force the discipline is a build-time or lint check that fails your pipeline when an external script tag ships without an integrity attribute. The combination is what closes the gap: CSP says which origins may load code, SRI says exactly which bytes from those origins are allowed to run. One without the other leaves a hole. Together they make the Polyfill class of attack a non-event, and they belong on the short list of defenses a small team needs before its first security incident.

What to do this week

You do not need a breach to act. Audit every <script src> and <link rel="stylesheet"> on your site that points at a domain you do not own. For each one, ask three questions. Is it pinned to an immutable version, or a moving "latest" target? Does it carry an integrity hash? Is the origin on a CSP allowlist?

Then do the work:

  • Replace every "latest" or dynamic third-party URL with a pinned, versioned one. If a vendor only offers a dynamic endpoint, that is a signal to find a different vendor or self-host the file.
  • Add integrity and crossorigin="anonymous" to every external script and stylesheet.
  • Ship a CSP that allowlists only the origins you actually use, with object-src 'none' and base-uri 'self' to close common injection tricks.
  • Remove anything you no longer use. The cheapest third-party script to secure is the one you delete.

The Polyfill incident was not an exotic zero-day. It was a domain that got sold and a script tag that nobody had pinned. The defense has been built into browsers for years. The 380,000 sites that got caught were running the same trusting <script src> everyone writes, with nothing checking that the bytes coming back were the bytes they expected. It is exactly the kind of avoidable exposure that compounds when security gets skipped early, and the reason vetting a vendor's security before you hand over your data extends to every script you load.

This is the kind of exposure a proper review surfaces in minutes, and it is exactly what our security audits check for on every third-party dependency a site loads. If you want a fast first pass on your own site, run our free security scan and see which scripts are pinned and which are wide open. The fix is an attribute and a header. The cost of skipping it is someone else's code running as you.