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

Steps

  1. 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.

  2. 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.js
    
    87533 jquery.min.js
  3. 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 -A
    
    v2CJ7UaYy4JwqLDIrZUI/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.

  4. 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 -A
    
    1H217gwSVyLSIfaLxHbE7dRb3v4mYCKbpQvzx0cegeju1MVsGrX5xXxAvs/HgeFs

    The attribute value is the algorithm, a dash, then this string: integrity="sha384-1H217gwSVyLSIfaLxHbE7dRb3v4mYCKbpQvzx0cegeju1MVsGrX5xXxAvs/HgeFs".

  5. 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 -A
    
    1
    6uT1XVyPwt1WQSPgoagV8EK4keZ49nQTNU6huXd+I4yLirWYhRN26ccjGGk9Ch48

    The 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.

  6. 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

Sign: The hash matches on your machine and the browser still blocks the script.Cause: The tag is missing crossorigin equals anonymous. For a cross origin file the browser fetches in no-cors mode by default, cannot read the body, and refuses the resource rather than checking it. Step 6 covers the server side of the same requirement.
Sign: The computed hash never matches and the file looks correct in an editor.Cause: The hash was taken over compressed bytes. The browser hashes the payload after content encoding is removed, so a file saved straight from a gzip or brotli response hashes to something else. Save the decoded body, which is what curl writes by default.
Sign: Integrity works for weeks, then breaks for every user at once.Cause: The URL points at a floating version such as a major-version alias. The provider published a patch, the bytes changed, and the pinned hash no longer applies. Reference an exact version in the URL.
Sign: A scanner reports integrity present on every tag and a self-hosted bundle is still unprotected.Cause: Attributes on same origin scripts add little, and build tools often add them only to vendor tags. Check which tags carry the attribute, not how many do.

What to check next

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.

intermediate6 minpublished updated Maks Verny