← All posts

Why Subresource Integrity protects your CDN scripts

CentralCSP Team

CentralCSP ·

Last update:

A <script src="https://cdn.example.com/lib.js"> tag runs someone else's code with the full rights of your page: it can read form fields, change the DOM, and send data anywhere your policy allows. You chose the file once, but whoever controls the CDN decides what bytes arrive on every visit. Subresource Integrity (SRI) closes that gap. You write the hash of the file you reviewed into the tag, and the browser refuses anything else.

The short version: add an integrity hash and crossorigin="anonymous" to every third-party script and stylesheet with a fixed version. Generate the value with the free SRI hash generator, and see the SRI reference for every attribute and algorithm detail. This post is about why it matters: three real compromises of widely loaded scripts, what SRI would have done in each, and the cases where it cannot help.

Three incidents that show the threat

Browsealoud, February 2018

On 11 February 2018, the Browsealoud accessibility script from Texthelp was altered to load the Coinhive cryptocurrency miner. Because thousands of sites embedded the same file from Texthelp's servers, one change reached over 4,000 of them at once, including the UK Information Commissioner's Office, NHS Inform, the Student Loans Company and United States Courts, as Infosecurity Magazine reported. The security researcher Scott Helme, who raised the alarm, recommended exactly this fix: an SRI attribute on the script tag, so the browser detects the modified file and refuses it.

This is the textbook SRI case. The file had a stable URL and stable content, so a pinned hash would have blocked the miner on every site that carried it, without any action from the site owner during the incident.

cdnjs, 2021

In July 2021 the researcher RyotaK published a vulnerability in cdnjs, the Cloudflare-run library CDN that he measured at 12.7% of all websites. A flaw in the service that pulls library updates let him execute arbitrary commands on cdnjs infrastructure. In his words, "cdnjs could be completely compromised", and leaked credentials could have been used to tamper with "arbitrary libraries on the cdnjs". It was reported on 6 April 2021 and fully fixed on 3 June 2021; there is no sign it was exploited.

The lesson is that a reputable CDN is still a server with bugs. Sites that pinned their cdnjs files with SRI would have been safe even if an attacker had swapped a library, because the altered file would no longer match the hash in their HTML.

polyfill.io, June 2024

On 25 June 2024 the e-commerce security firm Sansec reported that cdn.polyfill.io, embedded by more than 100,000 sites, was injecting malware. A company had bought the domain and its GitHub account in February 2024. The injected code only activated on specific mobile devices at specific hours, skipped admin users, and redirected visitors to a sports betting site through a fake Google Analytics domain.

polyfill.io is also the case that shows SRI's limit. The service built a different file for each browser: its own GitHub page said it "sends a different content per User-Agent (and that's the point of the service)", as Terence Eden quoted in 2018 when he explained why he could not put a single integrity hash on it. A dynamic response cannot be pinned with one hash, so most sites never had the option. The safe pattern was the one he recommended: serve a static copy yourself.

What SRI actually checks

When the browser fetches a resource that carries an integrity attribute, it hashes the bytes it received and compares them with your value. On a match the file runs. On a mismatch the fetch is treated as a network error, so the script never executes and the stylesheet is never applied.

<script
  src="https://cdn.example.com/lib.js"
  integrity="sha384-Li9vy3DqF8tnTXuiaAJuML3ky+er10rcgNR/VqsVpcw+ThHmYcwiB1pbOxEbzJr7"
  crossorigin="anonymous"></script>

Three rules cover almost every deployment:

  • Use sha384 or sha512. Browsers must support SHA-256, SHA-384 and SHA-512. If you list several algorithms, the browser only checks the strongest one, so a weaker extra hash does not weaken the check. Several hashes of the same algorithm are all accepted, which lets you allow two known-good versions.
  • Add crossorigin on cross-origin files. Without it the browser fetches the file in no-cors mode, the response is opaque, and the SRI check fails. crossorigin="anonymous" requests the file through CORS without cookies, which is what a public CDN expects.
  • Hash the exact file you serve. The SRI hash generator fetches the URL and returns the SHA-256, SHA-384 and SHA-512 values. In a build, automate SRI hashes with webpack or Vite.

The full syntax, the <link> variants (stylesheet, preload, modulepreload) and the CORS details live in the SRI reference.

When the hash does not match

A mismatch is loud in the console. Chromium logs a message of the form "Failed to find a valid digest in the 'integrity' attribute for resource '...' with computed SHA-384 integrity '...'. The resource has been blocked." The file is missing from the page, and whatever depended on it breaks.

In practice there are three causes, in order of likelihood:

  1. The upstream file changed legitimately. You pinned a URL like /lib@latest/lib.js that moves with each release. Pin an exact version, then regenerate the hash when you upgrade.
  2. The file is built per request. Polyfill services such as polyfill.io return different bytes per browser, and some loaders change on every release. SRI is the wrong tool there: self-host a fixed copy or drop the dependency.
  3. The file was tampered with. This is the case SRI exists for. Do not "fix" it by removing the attribute; find out what changed first.

A scanner finding about missing SRI is a different problem with its own fix, covered in how to fix an SRI security finding.

Where SRI cannot help

SRI pins bytes. It does not judge them, and it only covers the tags you put it on.

  • A vulnerable version stays vulnerable. The hash proves the file is the one you chose, not that the version is safe. Upgrade first, then pin; see which jQuery versions are vulnerable for a common case.
  • Scripts loaded by scripts are not covered. A tag manager or loader that injects more scripts at runtime adds files with no integrity attribute. Each of those is a new dependency you have not pinned.
  • First-party files are a different risk. If an attacker can change the JavaScript on your own server, they can usually change the HTML that carries the hash too.

That is why SRI sits next to two other controls. A Content Security Policy decides which origins may load scripts at all. The Integrity-Policy header can block scripts that arrive without integrity metadata and report each one as an integrity-violation. The old CSP require-sri-for directive never shipped to stable browsers, so do not rely on it. SRI hashes are also distinct from CSP hash sources, which allow inline code; see SRI vs CSP hash.

A quick checklist

  • Pin an exact version of every third-party script and stylesheet, then add integrity with sha384 or sha512.
  • Pair every cross-origin integrity with crossorigin="anonymous".
  • Self-host anything whose bytes change per request instead of trying to hash it.
  • Regenerate hashes as part of the upgrade, ideally in the build.
  • Keep an inventory of the scripts that actually run, including the ones injected at runtime.

SRI protects the files you know about. CentralCSP builds an inventory of the scripts your visitors' browsers report running on your pages, so a new or changed third-party script shows up before it becomes the next incident. Start free to map your client-side dependencies, or generate your first hash with the SRI hash generator.

Sources