Skip to main content
Saved
Pattern
Difficulty Advanced

Subresource Integrity

Use integrity hashes on external scripts and stylesheets to ensure resources have not been tampered with.

Den Odell
By Den Odell Added

Subresource Integrity

Problem

Loading a script from a CDN is a one-line convenience that hides a permanent commitment: you are trusting that host to serve the exact same file, unchanged, to every one of your users forever. A <script src="https://cdn.example.com/lib.js"> tag tells the browser to download whatever lives at that URL right now and execute it with full access to your page. There is no comparison against what you tested, no signature check, no second opinion. Whatever arrives, runs.

That trust is misplaced more often than people assume. CDNs get breached. Insiders push malicious builds. A misconfigured cache or a hijacked DNS record routes the request somewhere else. On a coffee-shop network, an attacker performing a man-in-the-middle swap can replace the response in transit before it reaches the browser. Any one of these turns a trusted dependency into hostile code running on every page that loads it, and supply-chain attackers target popular CDNs precisely because a single compromise fans out to thousands of sites and millions of users at once.

The damage is total. A tampered script can read passwords straight out of form fields, exfiltrate session tokens, inject a cryptocurrency miner, or quietly redirect users to a phishing clone. Your authentication, your validation, your careful server-side checks all run inside the same page as the attacker’s code, so none of them help. Worst of all, you have no way to detect the swap. The modified file already executed in the user’s browser; by the time anyone notices, the data is gone.

Solution

Subresource Integrity (SRI) closes the trust gap with a cryptographic checksum. You add an integrity attribute to the <script> or <link> tag containing a base64-encoded hash of the file you expect. Before executing or applying the resource, the browser hashes the bytes it actually downloaded and compares them against your value. On a match, the resource runs as normal. On any mismatch, the browser refuses the file entirely and reports an error. A single flipped byte is enough to block it.

The hash is prefixed with its algorithm, like sha384- or sha512-. Use SHA-384 or SHA-512; SHA-256 is permitted but the stronger variants are the practical standard. Because verifying a cross-origin response requires the browser to read its full body, SRI depends on CORS: you must add crossorigin="anonymous" (or use-credentials) so the CDN serves the file with the appropriate CORS headers. Without it, the integrity check cannot run and the resource is blocked.

You can make SRI mandatory rather than optional by pairing it with a Content Security Policy require-sri-for script style directive, which tells the browser to reject any script or stylesheet that arrives without an integrity attribute at all. That prevents a forgotten tag from quietly bypassing your protection.

Generating a hash is straightforward, and most CDN documentation now publishes integrity values alongside the version number. For your own bundled assets, hashing belongs in the build: a plugin computes the digest of each output file and injects the attribute automatically, so the hash and the file can never drift apart. The catch is mutable URLs. SRI pins one exact file, so it only works against immutable, versioned URLs like .../react@18.2.0/.... A “latest” or auto-updating URL will change its bytes out from under your hash and break the page, which is why you always pin a specific version and treat every hash update as a deliberate review of the new release.

Example

These examples walk from a hand-written tag through hash generation, build automation, and a graceful fallback.

Adding integrity to a CDN script

Both <script> and <link rel="stylesheet"> support the integrity and crossorigin attributes. The hash describes the exact file; the crossorigin attribute is what makes verification possible.

<!-- External script, pinned to an exact version -->
<script
  src="https://cdn.example.com/react@18.2.0/react.min.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
  crossorigin="anonymous"
></script>

<!-- External stylesheet -->
<link
  rel="stylesheet"
  href="https://cdn.example.com/bootstrap@5.3.0/bootstrap.min.css"
  integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM"
  crossorigin="anonymous"
/>

If the bytes at either URL change by even one character, the browser blocks the resource and logs an integrity error to the console.

Generating the hash

Generate the hash from the exact file you intend to ship. The pipeline computes a SHA-384 digest as raw binary, base64-encodes it, and you prefix the result with the algorithm name.

# Hash a local file
cat react.min.js | openssl dgst -sha384 -binary | openssl base64 -A
# -> oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC

# Hash a file straight from the CDN
curl -s https://cdn.example.com/react@18.2.0/react.min.js \
  | openssl dgst -sha384 -binary | openssl base64 -A

Prefix the output with sha384- to build the attribute value: integrity="sha384-oqVuAfXRKap7...". The web-based SRI Hash Generator does the same thing in a browser when you only need a value or two.

Automating SRI in the build

Hand-maintaining hashes for your own output is a losing game, since every build changes the bytes. A build plugin computes the digest of each emitted asset and injects the attribute into the generated HTML automatically. With Vite and rollup, vite-plugin-sri (or the underlying rollup-plugin-sri) handles this:

// vite.config.js
import { defineConfig } from 'vite';
import sri from 'vite-plugin-sri';

export default defineConfig({
  plugins: [
    sri({ algorithm: 'sha384' }),
  ],
});

Every <script> and <link> the build emits now carries a correct integrity attribute, regenerated on each build so the hash and the file are guaranteed to stay in sync. The equivalent in a webpack project is webpack-subresource-integrity.

Providing a fallback

When SRI blocks a resource, the file simply does not load, so a critical library can leave your page broken. Guard against it by detecting the failure and loading a self-hosted copy. For scripts, check whether the expected global appeared and inject a local fallback if it did not.

<script
  src="https://cdn.example.com/jquery@3.7.1/jquery.min.js"
  integrity="sha384-1H217gwSVyLSIfaLxHbE7dRb3v4mYCKbpQvzx0cegeju1MVsGrX5xXxAvs/HgeFs"
  crossorigin="anonymous"
></script>
<script>
  // If integrity failed (or the CDN is down), window.jQuery is undefined.
  window.jQuery || document.write(
    '<script src="/vendor/jquery-3.7.1.min.js"><\/script>'
  );
</script>

For stylesheets, listen for the error event on the <link> and swap in a local href:

const link = document.querySelector('link[data-cdn]');
link.addEventListener('error', () => {
  link.href = '/vendor/bootstrap-5.3.0.min.css';
});

The local copy should match the pinned version so behavior stays consistent whether the CDN or the fallback wins.

Benefits

  • The browser cryptographically verifies every external script and stylesheet before it runs, turning blind trust into a byte-for-byte guarantee.
  • A compromised CDN, malicious insider build, or in-transit man-in-the-middle swap is blocked automatically instead of executing on your page.
  • Supply-chain attacks that depend on silently altering a popular library stop working, because the altered bytes no longer match your hash.
  • Protection is declarative: a single attribute, no runtime library, no extra request, and negligible performance cost.
  • Pinning an exact hash forces you to pin an exact version, which eliminates surprise behavior changes from automatic CDN updates.
  • A CSP require-sri-for directive lets you enforce SRI across the whole page so no unverified script or style can slip through.
  • It composes cleanly with self-hosted fallbacks, so a blocked or unreachable resource can degrade gracefully rather than breaking the page.
  • Build-tool automation keeps hashes for your own assets correct without any manual maintenance.

Tradeoffs

  • Any legitimate change to the file breaks loading until you regenerate the hash, including routine CDN updates, minifier changes, or re-compression.
  • It cannot be used with mutable or “latest” URLs, since a file that updates itself will inevitably fail a fixed hash.
  • Every external resource needs its hash generated, recorded, and updated in lockstep with version bumps, which is real maintenance overhead.
  • Verification requires CORS, so a CDN that does not serve the correct crossorigin headers cannot be protected at all.
  • Protection is all-or-nothing per file; there is no partial verification, and a blocked critical resource takes the page down unless you build a fallback.
  • It is a poor fit for frequently rotated resources like rapidly iterating analytics or ad scripts, where you cannot keep hashes current.
  • Updating a library becomes a deliberate, verified step rather than a passive pull, which is safer but slower than letting the CDN serve whatever it likes.
  • A wrong or stale hash fails silently from the user’s perspective: the resource just does not load, so you need monitoring to catch it in production.
  • It protects integrity, not availability or privacy; SRI does nothing about a CDN going offline or about what the CDN learns from the request.

Summary

Subresource Integrity pins an external script or stylesheet to a cryptographic hash, so the browser refuses to run anything that does not match the exact file you approved. It defends against compromised CDNs and in-transit tampering at the cost of regenerating hashes whenever a file legitimately changes, which means pinning exact versions and automating hashes for your own assets in the build. Pair it with CSP and self-hosted fallbacks to make verification mandatory while keeping the page resilient when a resource is blocked.

Newsletter

A Monthly Email
from Den Odell

Behind-the-scenes thinking on frontend patterns, site updates, and more

No spam. Unsubscribe anytime.