Subresource Integrity (SRI) lets a page pin a script or stylesheet to a cryptographic hash. You add an integrity attribute to the tag; if the file the CDN actually serves does not hash to that value, the browser refuses to run it. SRI protects you from a compromised or hijacked CDN. It does not protect you from bugs in the library you chose.

What SRI protects against

Every <script src="https://cdn.example/lib.js"> tag is a statement of complete trust: whatever that host returns runs with full access to your page, your users' sessions and every form they fill in. That trust is usually justified, and occasionally catastrophically not. A CDN account can be phished, a maintainer's npm token can leak, a domain can expire or be sold to a new owner with different intentions, and a DNS or BGP hijack can redirect the hostname for an afternoon. The 2024 polyfill.io incident is the reference case: a widely embedded script host changed hands and began serving altered code to some visitors, and every page that had embedded it without a hash executed whatever came back.

Note what is and is not in that list. SRI stops a third party from changing the bytes you load. It does nothing about cross-site scripting in your own templates, nothing about a library that was vulnerable on the day you pinned it, and nothing about a script that legitimately fetches further code at runtime. If the pinned file itself calls eval on a remote response, the hash has verified the loader and not the payload.

SRI turns that open-ended trust into a specific promise: "run this file only if it is byte-for-byte the file I reviewed." It is a narrow defence, and that is its strength. It does not care who owns the domain today, whether the TLS certificate was reissued, or what the CDN's security team has been doing. The hash either matches or it does not.

SRI is not a substitute for choosing well-maintained libraries. If you pin jQuery 1.8 with a perfect hash, you have pinned its known vulnerabilities perfectly too. Pair SRI with the checks in Outdated JavaScript Libraries.

How the browser checks integrity

When a tag carries an integrity attribute, the browser fetches the resource as normal, computes a digest of the response body with the algorithm named in the attribute, and compares it with the base64 value you supplied. On a match the script executes or the stylesheet applies. On a mismatch the browser discards the response, fires the element's error event and logs a console message such as "Failed to find a valid digest in the 'integrity' attribute". Nothing from the bad response reaches the page.

The specification supports sha256, sha384 and sha512. You can list several hashes separated by spaces; the browser uses the strongest algorithm it recognises and accepts the resource if any hash of that algorithm matches. That is how you rotate a library version without a flag day: ship both the old and new hash, update the URL, then drop the old hash. SRI applies to <script>, to <link rel="stylesheet">, to <link rel="modulepreload"> and to fetch() through its integrity option. It does not apply to images, fonts loaded from CSS, or iframes.

A copy-ready example

This is a complete, correct tag. The hash below is illustrative; you must compute your own for the exact file you deploy.

<script
  src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js"
  integrity="sha384-1H217gwSVyLSIfaLxHbE7dRb3v4mYCKbpQvzx0cegeju1MVsGrX5xXxAvs/HgeFs"
  crossorigin="anonymous"></script>

<link
  rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"
  integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
  crossorigin="anonymous">

Generating the hash

Download the exact file you intend to reference, then hash it. The -binary flag matters: hashing the hex representation instead of the raw digest produces a value the browser will never match.

curl -sSLo jquery.min.js https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js
openssl dgst -sha384 -binary jquery.min.js | openssl base64 -A
# prefix the output with "sha384-"

Compare the output with the hash the CDN publishes on its file page. If they differ, you have downloaded a different file from the one the CDN lists, which is itself worth investigating before you ship anything.

Most bundlers can do this at build time (webpack-subresource-integrity, Vite plugins, the Rails asset pipeline), and the major CDNs display SRI hashes next to each file. Trust a CDN's published hash only as a convenience: the hash you should ship is the one you computed from a file you actually reviewed.

Why crossorigin="anonymous" is required

Scripts from another origin are normally loaded as opaque responses: the browser executes them but does not let the page inspect their bytes, because the same-origin policy forbids reading cross-origin content. An integrity check is a read of the bytes, so the browser will only perform it when the response was fetched with CORS and the server permitted it. That is what crossorigin="anonymous" does: it sends the request in CORS mode without credentials, and the CDN must answer with Access-Control-Allow-Origin: * (or your specific origin). If the header is missing, the load fails even when the hash is correct. Every mainstream public CDN sends it; a private CDN or an old S3 bucket may not, and this is the most common reason a freshly added SRI tag breaks a site.

AttributeValueEffect
integritysha384-<base64>Digest the response must match; several may be listed
crossoriginanonymousCORS request without cookies; required for any cross-origin resource with integrity
crossoriginuse-credentialsCORS request with cookies; only when the CDN needs them, and it must then echo your exact origin
(same-origin resource)no crossorigin neededIntegrity is checked directly because the page may read its own origin

Where SRI does not fit

SRI assumes the resource is immutable. Several classes of third-party script are deliberately not: Google Tag Manager and gtag.js, most A/B testing snippets, chat widgets, consent-management loaders and payment-provider scripts such as those served from js.stripe.com all update in place, by design, from a stable URL. Pinning them breaks the moment the vendor ships a change, which will be within days. For these you have three honest choices: accept the trust relationship and limit its blast radius with a strict Content Security Policy; self-host a reviewed copy and take on the update burden; or remove the script. Pretending SRI covers them is not one of the choices.

Mutable "latest" URLs are a related trap. https://code.jquery.com/jquery-latest.js style endpoints and unpinned @3 semver ranges on jsDelivr or unpkg will change under you. Pin the full version in the URL, and the hash will stay valid until you deliberately upgrade.

If you change the URL of a pinned resource without regenerating the hash, the browser blocks the file and your page loads without it. A stale SRI hash on a framework bundle is a blank site. Put hash generation in the build, never in a human's memory.

SRI and CSP together

Content Security Policy and Subresource Integrity answer different questions. CSP's script-src says which origins may supply scripts; SRI says which exact bytes are acceptable from those origins. A CSP that allows https://cdn.example still executes anything that host serves, so a compromised CDN walks straight through it. SRI closes that gap, but only for the tags you annotate; CSP is what stops an injected <script src="https://evil.example"> that has no integrity attribute at all. Use both. The old require-sri-for CSP directive, which would have made integrity mandatory, was removed from browsers and should not be relied on.

There is one useful interaction: CSP Level 3 allows hash-based script-src entries to match external scripts that carry a matching integrity attribute, which lets you drop the CDN origin from the allow-list entirely. Support is uneven across browsers, so treat it as a bonus rather than a foundation.

Rolling SRI out without breaking anything

  1. Inventory every cross-origin script and stylesheet. A scan's third-party script inventory is a fast start; view-source and your bundler's output finish the job.
  2. Classify each one as static (versioned library) or mutable (tag manager, widget). Only static resources get hashes.
  3. Pin versions in every URL, replacing ranges and "latest" aliases.
  4. Generate hashes in the build step and inject them into the templates, so a version bump regenerates the hash automatically.
  5. Add crossorigin="anonymous" to every annotated cross-origin tag and confirm the CDN sends Access-Control-Allow-Origin.
  6. Test in staging with the console open; an SRI failure is loud and immediate.
  7. Re-scan after deployment and keep the finding count at zero from then on.

How a scanner reports it

The Scan.now website scanner fetches your page, lists every cross-origin <script> and stylesheet, and reports those without an integrity attribute under Third-party scripts without Subresource Integrity. It also verifies that tags which do carry a hash use a supported algorithm. The check is passive: it reads the HTML you already serve and never fetches the scripts with altered content or attempts to inject anything. The default severity is low because a missing hash is an unrealised risk rather than an active weakness, but it moves up our triage order when the same scan also shows an outdated library, since that combination means you have neither chosen the code carefully nor pinned it. The JavaScript library scanner covers the version side of that pairing.

Our position: every versioned third-party script and stylesheet should carry an integrity hash, and the ones that cannot should be self-hosted or justified in writing. The cost is one build-step plugin. The alternative is trusting that no CDN you use will ever have a bad day. For the broader picture of what an external scan examines, start at the website security hub; for the mechanics of the header that complements SRI, see HTTP Security Headers Explained.