Supply chainJavaScriptHeaders

Subresource integrity after polyfill.io: what SRI stops and what it can't

Subresource integrity (SRI) is an integrity attribute holding a cryptographic hash of a file you load from another server. If the file that arrives doesn't match the hash, the browser refuses to use it. That makes SRI a strong defence for static, versioned files on a CDN, and no defence at all for a service whose response changes from one request to the next, which is exactly what polyfill.io was.

This guide explains how SRI works, how to add it, why it wouldn't have saved the sites hit by the polyfill.io attack in 2024, and what to combine it with.

What happened with polyfill.io

Polyfill.io was a popular service: you added one script tag, and it sent each browser the polyfills it needed for missing JavaScript features. In February 2024 the domain changed hands. Cloudflare later named the buyer as Funnull, a relatively unknown company.

In June 2024, the security firm Sansec reported that the CDN had started injecting malicious code into what it served. More than 100,000 sites embedded the script from cdn.polyfill.io, with JSTOR, Intuit and the World Economic Forum among the notable users. The injected code was written to stay out of sight. According to Sansec, it:

  • redirected mobile visitors to a sports betting site
  • only activated on specific mobile devices at specific hours
  • stayed inactive when it detected an admin user
  • delayed itself when it found a web analytics service
  • loaded through a fake Google Analytics domain, www.googie-anaiytics.com

The response came fast once the report was out. Cloudflare and Fastly published safe mirrors, Cloudflare began rewriting polyfill.io links on the sites it proxies (automatically on the free plan), Google started blocking ads for e-commerce sites that used the script, and Namecheap put the domain on hold. A later Sansec update tied the same actor to other CDN domains, including bootcdn.net, bootcss.com, staticfile.org and staticfile.net, used to spread malware since at least June 2023.

None of the affected sites had changed a line of code. A script tag added years earlier was enough.

How subresource integrity works

You give the browser the expected hash of the file. It downloads the file, computes the hash itself, and compares:

<script
  src="https://cdn.example.com/lib@4.2.1/lib.min.js"
  integrity="sha384-REPLACE_WITH_REAL_HASH"
  crossorigin="anonymous"></script>

MDN's reference spells out the rules that matter in practice:

  • Where it works: <script> elements, and <link> elements with rel="stylesheet", rel="preload" or rel="modulepreload".
  • Algorithms: the allowed prefixes are sha256, sha384 and sha512.
  • On a mismatch: the browser refuses to load the resource and returns a network error. The script simply doesn't run.
  • Several hashes: you can list more than one value with the same algorithm, and the file loads if any of them matches.
  • CORS: cross-origin requests with integrity checks must use CORS, which is why the crossorigin attribute is required. The CDN has to send the matching CORS headers, which the large public CDNs do.

Generating an SRI hash

Download the exact file you will reference, check it's the version you expect, and hash it. MDN gives this command:

cat lib.min.js | openssl dgst -sha384 -binary | openssl base64 -A

Prefix the output with sha384- and put it in the integrity attribute. On a machine without OpenSSL, shasum does the same job:

shasum -b -a 384 lib.min.js | awk '{ print $1 }' | xxd -r -p | base64

The URL must point to a fixed version. A path like lib@latest or a URL without a version will change under you, the hash will stop matching, and the script will silently disappear from your site.

Why SRI couldn't have protected polyfill.io users

The project's README describes the core feature: the service reads the User-Agent header of each request and returns polyfills suited to that browser. Chrome, Safari and an old Android browser each received a different file.

A hash describes one exact file. A response that varies by browser has no single correct hash, so sites using polyfill.io had no practical way to pin it with SRI in the first place. And the attackers' behaviour made the point stronger still: code that changes by device and time of day is the opposite of what SRI can describe.

SRI protects you against a file being swapped behind a URL that should never change. It doesn't help with services designed to change, such as dynamic polyfills, tag managers, analytics loaders or A/B testing snippets.

What does protect a third-party script

1. Self-host or bundle what you can. If a library doesn't need to be served by someone else, install it as a dependency and ship it from your own origin. A domain sale on the other side of the world can't touch a file you serve yourself. For polyfills, Sansec relayed the original author's advice that modern browsers no longer need the service at all; if you still support older browsers, let your build tool include polyfills based on your browser targets.

2. SRI on what stays external and static. A charting library pinned to an exact version on a public CDN, or a widget published under a versioned URL, should carry a hash.

3. A Content-Security-Policy allowlist. A policy such as script-src 'self' https://js.stripe.com limits which origins can run scripts on your pages. It wouldn't have blocked polyfill.io itself if you had allowed that host, but the injected code pulled a second script from a lookalike domain, and an allowlist without that domain blocks the second stage.

4. Integrity-Policy, where supported. A newer response header, Integrity-Policy: blocked-destinations=(script), tells the browser to block any script loaded without integrity metadata. MDN marks it as limited availability, so treat it as an extra layer for now, and remember that it applies to your own bundles too.

Scripts you can't self-host

Some providers require loading from their domain. Stripe's documentation says Stripe.js should always be loaded directly from https://js.stripe.com, not bundled or hosted yourself, and recommends including it on every page so its fraud detection can see how customers browse. Stripe's URL is versioned, but the provider controls the file.

For scripts like this, the decision is about trust rather than hashes. Keep the list short, allow those exact hosts in your CSP, and know which vendor can change code on your checkout page.

Audit the external scripts on your own site

Open your site, then the browser console, and run:

[...document.scripts]
  .filter(s => s.src && new URL(s.src).origin !== location.origin)
  .map(s => ({ src: s.src, integrity: s.integrity || '(none)' }))

Each line is code you don't control. For every entry, answer three questions: does it have to be external, is the version pinned in the URL, and does it carry an integrity value? Repeat on a few pages, because checkout and marketing pages often load different scripts. Scripts injected later by a tag manager won't show up in this list, which is one more reason to review what the tag manager loads.

FAQ

What is subresource integrity in simple terms?

It is a hash you put in a <script> or <link> tag so the browser can check that a file from a CDN is exactly the one you tested. If a single byte differs, the browser refuses to load it.

Can I use SRI with Google Analytics or a tag manager?

Not in a useful way. Those scripts are updated by the provider without a version change in the URL, so a hash would break the moment they ship a new release. Restrict them with a CSP allowlist instead.

Why do I need crossorigin="anonymous" with SRI?

Browsers only run integrity checks on cross-origin files fetched with CORS. Without the crossorigin attribute, the request isn't made in CORS mode and the check can't pass, so the file won't load.

Is polyfill.io still a risk?

Sansec reported that Namecheap put the domain on hold in June 2024, and Cloudflare turned on automatic link rewriting for sites on its free plan. Neither is a reason to keep the tag: any page that still references polyfill.io should remove it or switch to a bundled alternative.

Sources