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.
What does Subresource Integrity actually protect?
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.
The threat in one paragraph
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.
Generate the hash and read the headers
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>
Three browser runs: pass, tamper, missing CORS
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.
Where SRI works and where it cannot
| 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 and crossorigin
|
"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 loader 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 and crossorigin 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 loading code you cannot pin.
Watch out for the npm hash
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.
A browser-side workflow for auditing a page
- List third-party resources. In DevTools, open the Network panel, filter by JS and CSS, and sort by domain. Anything not on your origin goes on the list.
-
Check each tag for
integrityandcrossorigin. In the console,[...document.querySelectorAll('script[src],link[rel=stylesheet]')].filter(e => !e.integrity).map(e => e.src || e.href)lists every external tag without SRI. -
Confirm the URL is pinned. Exact version in the path,
immutablecaching, no "latest". -
Generate the hash from the exact URL. Use
opensslas above, or the PixelValut SRI Hash Generator, which hashes pasted code, a dropped file or a fetched URL in the browser and outputs the full tag withintegrityandcrossorigin. - Cross-check against a second source when the CDN or project publishes hashes.
-
Deploy, then read the console on a clean profile. Any SRI error means a hash mismatch or a missing
crossorigin. - Pair SRI with a Content-Security-Policy that only allows the script hosts you expect. SRI pins the bytes; CSP limits where code can come from. My HTTP security headers audit covers the CSP side.
Rollout checklist
- [ ] Every third-party
<script>and stylesheet uses an exact version URL - [ ]
integrityuses sha384 or sha512 and was generated from the served file - [ ]
crossorigin="anonymous"is present on every tag withintegrity - [ ] The host returns
access-control-allow-origin - [ ] Upgrades change the URL and the hash in the same commit
- [ ] A test or CI check fails if an external tag has no
integrity - [ ] Loaders you cannot pin are listed, owned by someone, and reviewed
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.
Frequently asked questions
Which hash algorithm should I use for SRI?
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.
Why does my script fail with an SRI error after a CDN update?
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.
Do I need SRI for scripts on my own domain?
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.
Does SRI slow down page loads?
Hashing a typical library is cheap compared with downloading it. The real cost is operational: every upgrade needs a new hash, which is why it belongs in your build or CI.
Can SRI protect Google Tag Manager or analytics scripts?
Usually not, because vendors update those loaders in place, so a pinned hash breaks. Govern them with vendor review, a strict CSP and regular audits instead.
What does crossorigin="anonymous" do?
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.
Follow & Connect
- YouTube: PixelValut (@pixel-valut)
- TikTok: PixelValut (@pixelvalut007)
- GitHub: Muhammad Ali Mughal (@ali111887)
- LinkedIn: Muhammad Ali Mughal

Top comments (0)