How to check subresource integrity
Download the file the tag points at and hash it: openssl dgst -sha384 -binary jquery.min.js | openssl base64 -A. Prefix the result with the algorithm name and compare it to the integrity attribute on the tag. One differing byte changes the whole hash, and the browser then refuses to run the file.
Why check this
Subresource integrity is a hash written into the integrity attribute of a script or link tag. Before executing the file, the browser hashes the bytes it received and compares. A CDN account that gets taken over, a proxy that rewrites JavaScript, or a vendor that quietly republishes a version under the same URL all produce a different hash, and the browser drops the file instead of running it.
Run this check whenever a third-party script is added or its version is bumped, and during release sign-off on any page that loads code from a domain you do not control. The failure it prevents is concrete: an attacker replaces the analytics bundle on a CDN, every visitor executes it, and your payment page starts sending card fields to another host.
Prerequisites
- curl 7.0 or later. See the curl manual.
- openssl 1.1 or later. On macOS,
shasum -a 384 -b file | cut -d' ' -f1 | xxd -r -p | base64produces the same string. - The MDN page on Subresource Integrity for the attribute grammar.
- A published integrity value to compare against. The examples below use the value cdnjs publishes for jQuery 3.7.1.
Steps
- Step 1.
Read the integrity value the provider publishes for the exact file and version.
curl -s 'https://api.cdnjs.com/libraries/jquery/3.7.1?fields=sri' | tr ',' '\n' | grep '"jquery.min.js"'"jquery.min.js":"sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g=="The prefix before the dash is the algorithm. This one is
sha512, so step 3 has to use sha512 and not sha384. - Step 2.
Download the exact file the tag would load, and record its size.
curl -s -O https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js && wc -c jquery.min.js87533 jquery.min.js - Step 3.
Hash the file with the algorithm from step 1 and base64 encode the raw digest.
openssl dgst -sha512 -binary jquery.min.js | openssl base64 -Av2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g==Character for character the same as the published value in step 1, including the trailing padding. The file on the CDN is the file the provider signed.
- Step 4.
Compute the sha384 form, which is what most projects write into their own tags.
openssl dgst -sha384 -binary jquery.min.js | openssl base64 -A1H217gwSVyLSIfaLxHbE7dRb3v4mYCKbpQvzx0cegeju1MVsGrX5xXxAvs/HgeFsThe attribute value is the algorithm, a dash, then this string:
integrity="sha384-1H217gwSVyLSIfaLxHbE7dRb3v4mYCKbpQvzx0cegeju1MVsGrX5xXxAvs/HgeFs". - Step 5.
Change a single byte of the copy and hash it again, so you can see what the browser compares.
cp jquery.min.js jquery.tampered.js && printf '\x21' | dd of=jquery.tampered.js bs=1 seek=100 count=1 conv=notrunc status=none && cmp -l jquery.min.js jquery.tampered.js | wc -l && openssl dgst -sha384 -binary jquery.tampered.js | openssl base64 -A1 6uT1XVyPwt1WQSPgoagV8EK4keZ49nQTNU6huXd+I4yLirWYhRN26ccjGGk9Ch48The first line is the count of differing bytes: one. The file size is unchanged at 87533 bytes. Not one character of the hash survives, which is the property the check relies on.
- Step 6.
Confirm the host serving the file allows a cross origin read, because the browser needs that before it will check a hash.
curl -sI https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js | grep -iE '^HTTP|access-control-allow-origin'HTTP/2 200 access-control-allow-origin: *Without this header, a tag carrying
crossorigin="anonymous"fails to load at all, and the page breaks even though the hash is correct.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| Computed string equals the attribute | The served bytes are the bytes the tag expects | Nothing. Record the version you verified. |
| Strings differ, file size differs too | The URL now serves a different build | Pin an immutable, versioned URL and recompute the hash for it. |
| Strings differ, size identical | The content changed without the version changing | Treat as an incident until the provider explains it. |
| Attribute missing on a third-party tag | No integrity check happens at all | Add the attribute together with crossorigin="anonymous". |
| Attribute present, browser console reports a mismatch | The browser blocked the resource | Rehash the live file. A stale hash after a version bump is the usual cause. |
Common mistakes
What to check next
- How to check CSP header:
require-sri-forand a script source list are what stop a tag from being added without a hash. - How to check if mixed content exists on a page: a script loaded over HTTP can be rewritten before any hash is compared.
- How to check security headers: reads the header set that governs which third parties the page may reach.
- How to check X-Content-Type-Options: stops a response whose type is wrong from being executed as script.
- Security headers checker: paste a URL and read the policy headers for the same page.
FAQ
Which hash algorithm should I use?
sha384 is the common choice and sha256 and sha512 are equally valid. The algorithm is part of the attribute value, so the browser knows which one to run. Match whatever the provider published, as step 1 shows.
Can one tag carry more than one hash?
Yes. Space separated values are treated as alternatives, and the resource passes if it matches any of them with the strongest algorithm present. That is how a build publishes an old and a new hash during a rollout.
What does the browser do on a mismatch?
It discards the response and reports a console error naming the resource. The script never executes and the stylesheet never applies, so a wrong hash on a critical bundle takes the page down.
Does integrity work on images or iframes?
No. The attribute is honoured on script elements and on link elements with rel=stylesheet, rel=preload and rel=modulepreload. Writing it on an image tag has no effect and gives false confidence in a scan.
Verified
Verified by Maks Vernycurl 8.21.0openssl 3.1.1
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
intermediate6 minpublished updated Maks Verny