Every <script src="https://cdn..."> on your site is a standing grant of full JavaScript execution to whoever controls that file tomorrow. Not the file you reviewed, the file the CDN serves on the next request. Subresource Integrity (SRI) is the one browser feature that turns that open grant into a fixed contract: run this exact file, byte for byte, or run nothing.
I run PixelValut, a set of browser-side developer and security tools, and SRI is one of the cheapest controls I know with real teeth. I wrote this with help from AI tools and checked every command and claim myself. All outputs below are from commands I ran today in a terminal and in Chrome 156 driven by Playwright.
Direct answer: SRI makes the browser hash a downloaded script or stylesheet and compare it with the integrity value in your HTML. If one byte differs, the browser blocks the file. It protects against a compromised CDN or a hijacked file. It needs crossorigin="anonymous" and a CORS-enabled host, and it only works for static, versioned files.
A third-party file can change without any change to your code: a CDN account gets phished, a package maintainer's token leaks, or a domain changes hands. The best-known case is polyfill.io in 2024. After the domain and project changed ownership, researchers reported it serving injected code to sites that still loaded it. SRI would not have saved most of those sites, for a reason I cover below, but it does stop the simpler and more common case: a pinned, static file that gets swapped.
I used dayjs 1.11.13 from jsDelivr as the example. First, the hash. SRI uses base64 of the raw digest, not the hex string most tools print:
curl -s -o dayjs.min.js https://cdn.jsdelivr.net/npm/dayjs@1.11.13/dayjs.min.js
openssl dgst -sha384 -binary dayjs.min.js | openssl base64 -A
DpVxUeeBWjUvUV1czyIHJAjh+jYUZFu2lLakbdua5vbwOrBGi1UgaKCHjTC+x3Ky
Cross-check it against an independent source. jsDelivr's data API publishes a SHA-256 for each file, and my local SHA-256 matched it exactly (nP25Pzivzy0Har7NZtMr/TODzfGWdlTrwmomYF2vQXM=). If you only hash the file you just downloaded, you are trusting that download.
Then the headers, because SRI on a cross-origin file only works if the host sends CORS headers:
HTTP/2 200
access-control-allow-origin: *
timing-allow-origin: *
cache-control: public, max-age=31536000, s-maxage=31536000, immutable
cross-origin-resource-policy: cross-origin
content-type: application/javascript; charset=utf-8
What each one tells you:
| Header | Value | Why it matters for SRI |
|---|---|---|
access-control-allow-origin |
* |
Lets the browser read the bytes in CORS mode, which it must do to hash them |
cache-control |
immutable , 1 year |
A versioned URL that will not change, so the hash stays valid |
content-type |
application/javascript |
Correct MIME type; with nosniff a wrong type is blocked anyway |
cross-origin-resource-policy |
cross-origin |
The host allows embedding from other origins |
The tag to ship:
<script src="https://cdn.jsdelivr.net/npm/dayjs@1.11.13/dayjs.min.js"
integrity="sha384-DpVxUeeBWjUvUV1czyIHJAjh+jYUZFu2lLakbdua5vbwOrBGi1UgaKCHjTC+x3Ky"
crossorigin="anonymous"></script>
I loaded three local test pages in Chrome and captured the console.
Run 1: correct hash, crossorigin="anonymous".
[log] dayjs loaded: true
Run 2: tampered file. I served a copy of the same file from a second local origin with CORS enabled, with one harmless line appended (console.log("injected line")). The integrity attribute still held the original hash:
[error] Failed to find a valid digest in the 'integrity' attribute for resource
'http://127.0.0.1:8766/dayjs.min.js' with computed SHA-384 integrity
'jBOi9x7Z1ktN8H4qfSwk+1UiIyzSbIfXvjjA8wEFLuW81GktpRPSoc9kdCMHsca6'.
The resource has been blocked.
[log] dayjs loaded: false
The appended line never ran. Note that the browser prints the hash it computed, which is handy when you are debugging a legitimate update.
Run 3: correct hash, but no crossorigin attribute.
[error] Subresource Integrity: The resource 'https://cdn.jsdelivr.net/npm/dayjs@1.11.13/dayjs.min.js'
has an integrity attribute, but the resource requires the request to be CORS enabled to check
the integrity, and it is not. The resource has been blocked because the integrity cannot be enforced.
[log] dayjs loaded: false
This is the most common SRI bug I see: a correct hash with a missing attribute. It fails closed, which is good for security and bad if nobody checked the console before deploying.
| Resource type | SRI works? | Why | What to do instead |
|---|---|---|---|
Versioned library on a public CDN ( lib@1.2.3/file.js ) |
Yes | Bytes never change for that URL | Pin the version, add integrity andcrossorigin |
"Latest" URL ( lib@3/file.js ,/latest/ ) |
Breaks on every update | The bytes change while the hash does not | Pin an exact version |
| Dynamic polyfill or user-agent-tailored bundles | No | Each browser may get different bytes | Self-host a build you control |
| Tag managers, analytics and chat widgets | Usually no | Vendors update the in place | Review vendors, restrict with CSP, load in isolation where possible |
Your own hashed build assets ( app.3f9c1.js ) |
Yes | Bundlers can emit hashes at build time | Enable your bundler's SRI plugin, or rely on same-origin hosting |
| Web fonts and CSS from a CDN | Yes, for <link> stylesheets |
Same rules as scripts | Add integrity andcrossorigin to the<link> |
The dynamic case is why SRI would not have rescued most polyfill.io users: a service that tailors output per browser cannot be pinned to one hash. The lesson is to stop code you cannot pin.
npm view dayjs@1.11.13 dist.integrity returns:
sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==
It looks like an SRI value, and it is in the same format, but it is the hash of the package tarball, not of dayjs.min.js. Pasting it into a script tag blocks the file. Hash the exact file at the exact URL you serve.
integrity and crossorigin.[...document.querySelectorAll('script[src],link[rel=stylesheet]')].filter(e => !e.integrity).map(e => e.src || e.href) lists every external tag without SRI.immutable caching, no "latest".openssl as above, or the <script> and stylesheet uses an exact version URLcrossorigin="anonymous" is present on every tag with access-control-allow-origin
If you want the bigger picture on trusting files you did not build, see how to verify a download with SHA-256, GPG and Sigstore. It is the same idea at the file level.
Use sha384, the most common choice in CDN snippets, or sha512. Browsers also accept sha256. You can list several values in one attribute, separated by spaces, and the browser uses the strongest one it supports.
Either the URL is not pinned to an exact version, so the bytes changed, or the hash was generated from a different file. Pin the version and regenerate the hash from the URL you actually serve.
It is optional for same-origin files. The bigger risk is third-party hosts you do not control. Many bundlers can add SRI to their own output if you want defense in depth.
Hashing a typical library is cheap compared with down it. The real cost is operational: every upgrade needs a new hash, which is why it belongs in your build or CI.
Usually not, because vendors update those s in place, so a pinned hash breaks. Govern them with vendor review, a strict CSP and regular audits instead.
It makes the browser fetch the file in CORS mode without cookies. CORS mode lets the browser read the bytes and verify the hash. Without it, Chrome blocks a cross-origin file that has an integrity attribute.