Subresource integrity: reject a script when its bytes do not match

Subresource integrity lets the browser compare a downloaded script or stylesheet with a digest in your HTML. Put an integrity value on the resource element. If the fetched bytes do not match an accepted digest, the browser refuses to use that resource. For a cross-origin file, the response also needs appropriate CORS approval.

A versioned third-party script is the practical case: your page should use the reviewed file, and a different response should fail visibly. SRI checks the file against the digest you supplied. You still need to decide whether the reviewed code itself is appropriate.

Generate the digest from the file you reviewed

Save the exact reviewed script as widget.js. With Node.js installed, run this in the folder containing it:

node -e "const fs=require('node:fs'),c=require('node:crypto');console.log('sha384-'+c.createHash('sha384').update(fs.readFileSync('widget.js')).digest('base64'))"

The result begins with sha384-. Copy the complete output into the attribute below, replacing the bracketed placeholder:

<script
  src="https://cdn.example.com/widget/1.2.3/widget.js"
  integrity="[COMPLETE sha384- OUTPUT]"
  crossorigin="anonymous"></script>

The address is an illustrative CDN URL, not a working library recommendation. Use the real pinned URL whose returned bytes you reviewed. The placeholder is deliberately not a valid integrity value; replace it before testing.

Hash the file bytes, including their line endings. Formatting or rebuilding the file can change its digest without changing its apparent behavior. A local prettified copy may therefore differ from the CDN response. MDN SRI reference

Make a match and mismatch visible locally

For a controlled rehearsal, create this exact widget.js through Node so its newline is explicit:

node -e "require('node:fs').writeFileSync('widget.js','globalThis.widgetReady = true;\n')"

Compute its digest with the earlier command. On an HTTP development server, use a page containing:

<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>SRI rehearsal</title>
<p id="status">Waiting for script</p>
<script src="./widget.js"
  integrity="[COMPLETE sha384- OUTPUT]"></script>
<script>
  document.querySelector('#status').textContent =
    globalThis.widgetReady === true ? 'Reviewed script ran' : 'Script did not run';
</script>
</html>

With the matching digest, the expected line is Reviewed script ran. Next append a comment to widget.js while keeping the old digest in HTML. On a fresh reload, the expected line is Script did not run, and the console should identify an integrity mismatch. This same-origin rehearsal isolates the digest check; it does not test CDN CORS settings.

Restore the reviewed file before continuing. If you intentionally approve a changed file, review it and regenerate the digest as part of that update. Replacing the digest merely to silence a surprise mismatch defeats the review boundary.

Diagnose the failed check before removing it

For a cross-origin script, inspect the actual response and its CORS headers. crossorigin="anonymous" does not make the CDN grant access; its server must approve the request. Also check for redirects, an HTML error page, a changed version, or transformed bytes.

Keep the failure visible. A fallback that inserts the same URL without integrity would run the unverified response. Plan a useful unavailable state for the page instead, and inspect the reason in DevTools.

If the script is missing because of a path or response-type problem, the JavaScript loading checklist covers those earlier checks. For a module-based page, HTML module scripts explains the entry file and dependencies; do not assume one classic script's digest verifies every file an application later requests.

Keep the pinned URL, reviewed file, digest, and match/mismatch result together. That gives the next maintainer a concrete basis for approving the next version.

Keep reading