Every other boundary in this area constrains what a third-party script may do. This one constrains which script runs at all — and it addresses a risk the others cannot touch, because a sandbox faithfully isolates malicious code just as well as benign code.

The exposure is structural rather than hypothetical. A <script src="https://cdn.vendor.example/sdk.js"> is a standing instruction to execute whatever that URL returns, on every page load, forever. You reviewed it once; the vendor may change it hourly. Their build pipeline, their CDN, their dependency tree and every account with write access to any of those are, in effect, extensions of your own deployment process — with none of your review, and none of your rollback.

Subresource integrity closes the gap by making the content part of the contract rather than only the address. A script whose bytes no longer match the hash you recorded does not execute. That is a strong guarantee, and it comes with an equally strong operational obligation, which is most of what this guide is about.


When Integrity Is Applicable, and When It Is Not

Integrity requires a stable artefact. A hash pins exact bytes, so the technique fits a versioned, immutable URL and fights anything designed to change.

It applies well to versioned SDK builds (/sdk/3.4.1/client.js), self-hosted vendor copies, and any script you have pinned deliberately. It applies badly to bootstrap loaders that the vendor updates continuously, to tag-manager containers whose entire purpose is to change without a deploy, and to endpoints that vary the response by user agent or region.

That second list is not a reason to skip the technique — it is a reason to be precise about what you are protecting. Most vendor stacks have a small, stable core that can be pinned and a mutable loader that cannot, and pinning the core is worth doing even when the loader remains open. Where nothing can be pinned, the honest conclusion is that this vendor is an unreviewable dependency, which is itself useful information for the inventory.

Which vendor URLs can be pinned Three groups of vendor URL. Pinnable: a versioned immutable path, and a self-hosted copy you control, both of which return identical bytes indefinitely. Partly pinnable: a vendor SDK behind a stable major version, where the core can be pinned but the loader cannot. Not pinnable: a rolling latest path, a tag-manager container designed to change without a deploy, and any endpoint whose response varies by user agent or region. pin it /sdk/3.4.1/client.js /vendor/self-hosted.js identical bytes indefinitely an integrity mismatch means something is wrong pin the core only loader.js → cannot pin core-3.x.js → can pin partial coverage is still coverage record which half is unprotected cannot pin /latest/sdk.js /gtm.js?id=… designed to change without a deploy an unreviewable dependency — record it as one The right-hand column is not a failure of the technique. It is a finding about that vendor. Ask for a versioned URL. Many vendors publish one and simply do not document it prominently.

The Mechanic: How the Browser Checks

The integrity attribute carries one or more base64 digests prefixed by the algorithm. When the response arrives the browser hashes the bytes, compares against the list, and executes only on a match. A mismatch is a network-error-class failure: the script does not run, onerror fires, and the console reports the expected and computed digests.

Three properties of that mechanism have practical consequences. First, CORS is required: integrity only applies to responses the page is allowed to read, so a cross-origin script must carry crossorigin="anonymous" and the vendor must send permissive CORS headers. Without it, the check is skipped and you have a false sense of protection. Second, multiple digests are permitted, and the browser passes if any one matches — which is what makes a version rollover survivable. Third, the failure is total: there is no partial execution, so anything that depended on the script is also broken.

<!-- Both attributes are required. crossorigin is what makes the check possible;
     without it the browser cannot read the body and silently skips verification. -->
<script
  src="https://cdn.vendor.example/sdk/3.4.1/client.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
  crossorigin="anonymous"
  defer></script>
<!-- During a rollover, list both digests. The browser accepts either, so the
     deploy that changes the version and the deploy that removes the old hash
     can be separated in time. -->
<script
  src="https://cdn.vendor.example/sdk/3.5.0/client.js"
  integrity="sha384-<hash-for-3.5.0> sha384-<hash-for-3.4.1>"
  crossorigin="anonymous"
  defer></script>
What happens on a mismatch A response arrives and the browser computes its digest. On a match with any listed hash, the script executes normally. On a mismatch, the script is treated as a network error: it does not execute, the error handler fires, and anything depending on it is broken. A separate branch shows that a missing crossorigin attribute causes the check to be skipped entirely, so the script executes unverified with no warning. response arrives browser hashes the bytes match executes normally any listed digest is sufficient mismatch treated as a network error no execution at all — onerror fires dependants break with it no crossorigin attribute the check is skipped entirely executes unverified, no warning The right-hand box is the failure mode that matters: protection you believe you have and do not.

Implementation: Generate, Store, Verify

Step 1 — Generate the digest from the exact artefact

# Fetch and hash in one pass. Use the URL you will actually ship, including
# any query string — a different URL may return different bytes.
curl -sS https://cdn.vendor.example/sdk/3.4.1/client.js \
  | openssl dgst -sha384 -binary \
  | openssl base64 -A

Step 2 — Keep the hashes with the vendor registry, not in the markup

Scattering digests through templates makes a version bump a search-and-replace exercise. Keep them in the same registry that drives your consent categories and CSP allowlist, and render the attribute from it:

// vendors.js — one record per vendor, consumed by templates, CSP and CI alike.
export const VENDORS = {
  acmeSdk: {
    src: 'https://cdn.vendor.example/sdk/3.4.1/client.js',
    integrity: 'sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC',
    category: 'analytics',       // drives the consent gate
    origin: 'https://cdn.vendor.example',  // drives the CSP allowlist
  },
};

Step 3 — Fail loudly, and only where it is safe to

A mismatch stops the script. Whether that is acceptable depends entirely on the vendor: for an analytics tag, silence is the correct outcome and a monitoring alert is the response. For anything the page’s function depends on, a hard stop is worse than the risk it prevents — which is an argument for not putting such a dependency on an unpinned third-party URL in the first place.

// Report a mismatch as a distinct signal. Without this, an integrity failure
// looks identical to a vendor outage in every dashboard you own.
script.addEventListener('error', () => {
  report('vendor.script.failed', {
    vendor: 'acmeSdk',
    src: script.src,
    // The console names the digest mismatch; this only records that it failed.
    hint: 'integrity mismatch or network failure — check the console detail',
  });
  fallbackTier(); // the tiered degradation from the fallback-chain guide
});

Step 4 — Detect the change before the browser does

The browser tells you at the worst possible moment: in production, in a visitor’s session. A scheduled job that fetches each pinned URL and compares its digest against the registry moves the detection to somewhere you can act on it.

// ci/verify-vendor-hashes.js — run on a schedule, not only on deploy.
import { VENDORS } from '../vendors.js';
import { createHash } from 'node:crypto';

let failed = false;
for (const [name, v] of Object.entries(VENDORS)) {
  const bytes = Buffer.from(await (await fetch(v.src)).arrayBuffer());
  const actual = 'sha384-' + createHash('sha384').update(bytes).digest('base64');
  if (actual !== v.integrity.split(' ')[0]) {
    console.error(`${name}: expected ${v.integrity.split(' ')[0]}, got ${actual}`);
    failed = true;
  }
}
process.exit(failed ? 1 : 0);
Detect the change before your visitors do A vendor publishes a changed script. Without scheduled verification, the change is first detected when a visitor's browser fails the integrity check in production, which appears as a vendor outage and is diagnosed under pressure. With a scheduled verification job, the change is detected within the job's interval, in a build log, where it can be reviewed and the hash updated deliberately before any visitor is affected. vendor publishes a change scheduled job notices in a build log, within the interval review the diff, update the hash deliberately a visitor's browser notices in production, mid-session looks like a vendor outage, diagnosed under pressure Same event, two ways to learn about it. The job costs one request per vendor per run.

Operating the Hashes Without Blocking Releases

The technique fails in practice not because the mechanism is hard but because the process around it is. A hash is a promise that the artefact will not change, and vendors do not read your promises — so the operational question is how a legitimate vendor update becomes a reviewed change rather than an outage.

Separate the version bump from the hash removal. Because the browser accepts any listed digest, a rollover can be split across two deploys: the first adds the new hash alongside the old one and switches the URL, the second removes the old hash once the new version has been live long enough to trust. Neither deploy has a window in which the page is broken, and either can be reverted independently. Teams that treat the attribute as a single value have to change URL and hash atomically, which makes every vendor update a small risk.

Keep the update mechanical. The scheduled verification job already computes the current digest; have it open a pull request with the new value rather than merely reporting a mismatch. The review then happens on a diff — old hash, new hash, and a link to the vendor’s changelog — which is a two-minute task rather than an investigation. Automating the detection while leaving the decision to a human is the right split: you want the change noticed automatically and accepted deliberately.

Decide the failure policy per vendor, in advance. For each pinned script, record whether an integrity failure should degrade silently, degrade with a visible notice, or page someone. Analytics degrades silently; a payment SDK does not. Writing this down before the first mismatch is what prevents the decision being made at speed by whoever is on call, whose incentive is always to restore service by removing the attribute.

Watch for the pin that quietly stopped applying. The most dangerous outcome is not a mismatch — it is an integrity attribute that has silently stopped being checked, because a crossorigin attribute was dropped in a refactor or the vendor’s CORS headers changed. The attribute is still in the markup, the script still runs, and nothing anywhere reports that verification is no longer happening. Include a deliberate mismatch in a staging build as a recurring test, so the absence of a failure is itself verified.

Taken together these turn integrity from a one-off hardening exercise into a standing control. The distinction matters because the risk it addresses is standing too: a vendor’s build pipeline is exactly as trustworthy on the day you add the hash as it is two years later, and the only thing that changes in between is how much you have stopped thinking about it.

Rolling a version over in two safe steps Three states across two deploys. Initially the attribute lists only the old hash and the URL points at the old version. The first deploy switches the URL to the new version and lists both hashes, so the new bytes verify against the new hash and a revert still verifies against the old one. The second deploy, made after the new version has been live long enough to trust, removes the old hash. Neither deploy has a window in which no listed hash matches. before src → 3.4.1 integrity: [old] deploy 1 both hashes listed src → 3.5.0 integrity: [new old] deploy 2 after src → 3.5.0 integrity: [new] at no point is there a state where no listed hash matches Deploy 1 is also revertible: rolling the URL back still verifies, because the old hash is still listed. Leave a week between the two so a bad vendor release is caught while the escape hatch is still open.

Verification Checklist


One further point on scope: integrity protects the artefact, not the vendor’s intentions. A hash confirms that the bytes you reviewed are the bytes that ran; it says nothing about whether those bytes were doing something you would object to when you first pinned them. Pinning is therefore a complement to reviewing rather than a substitute — and for a vendor whose script you have never actually read, the honest description is that you have pinned an unknown quantity rather than verified a known one.

Interaction Matrix

Pattern Interaction
Strict CSP Complementary and independent: CSP says which origins may be contacted, integrity says which bytes may run. require-sri-for can enforce that scripts carry a digest at all.
Preload and prefetch The integrity attribute is part of the cache key match. A preload without it, paired with a script that has it, double-fetches.
Consent gating Same registry, different field. Injected scripts must carry the digest too — set integrity on the element before appending.
Fallback chains An integrity failure is a Tier-2 condition, not a Tier-3 one: it is an availability problem, so a retry on the next navigation is legitimate.
Script inventory The registry that holds the digests is the same artefact the inventory consumes; keeping them together prevents them diverging.

Where a vendor cannot supply an immutable URL at all, ask anyway and record the answer. A written “we do not offer versioned builds” is a fact your risk register can hold, and it occasionally prompts the vendor to publish one.

Troubleshooting

The script fails after a vendor deploy, with an integrity error in the console. The vendor changed the artefact at a URL you assumed was immutable. Verify, review the new file, and update the hash — do not remove the attribute to restore service, which discards the protection permanently in exchange for a few minutes.

The check appears to pass but the script is not verified. Confirm crossorigin is present and that the response carries Access-Control-Allow-Origin. Without both, the browser skips verification silently.

Two requests for the same script. A <link rel="preload"> without a matching integrity value does not satisfy the consuming element’s cache lookup. Put the same attributes on both.

A dynamically injected script is unverified. The property is integrity on the element and must be set before appendChild, exactly like fetchPriority. Injection paths written before the policy existed are the usual gap.

The verification job fails on a vendor that never changes. Some CDNs vary whitespace or a build banner per edge node. Confirm by fetching from two regions; where it is genuinely non-deterministic, the URL is not pinnable and belongs in the third column.


Frequently Asked Questions

Does an integrity failure break the whole page?

It breaks whatever depended on that script, which for a well-isolated third party should be nothing. That is the practical argument for combining pinning with the other boundaries in this area: a vendor whose absence takes the page down is a vendor you cannot safely pin, and also a vendor you cannot safely have.

Where a dependency is unavoidable, define the degraded state explicitly rather than discovering it during an incident. The tiered fallback model applies unchanged: the integrity failure is the trigger, and the tier below is what the visitor sees.

Is self-hosting the vendor script better than pinning theirs?

It is stronger and more work. Self-hosting means the artefact cannot change without passing through your own pipeline, so the review happens before deployment rather than after publication — and you get the connection saving of serving from your own origin. What you take on is the obligation to update, and the risk of running a version the vendor no longer supports.

The pragmatic middle ground is to self-host the stable core and leave genuinely dynamic pieces on the vendor’s CDN, pinned where possible. Check the vendor’s terms first: some prohibit redistribution, which makes this a contractual question rather than a technical one.

How often should the verification job run?

Often enough that the window between a vendor’s change and your knowledge of it is shorter than your tolerance for surprise — for most teams, hourly is comfortable and daily is defensible. The job costs one conditional request per vendor, so frequency is rarely the constraint.

Run it against production URLs rather than a cached copy, and from an environment whose geography matches most of your traffic. A vendor rolling a change out region by region will otherwise show as passing for hours after your visitors have started receiving the new bytes.

Does this protect against a compromised vendor account?

Against one that changes the pinned artefact, yes — the modified script will not execute, which is precisely the scenario the technique exists for. It does not protect against a compromise that operates through channels you have not pinned: a tag-manager container, a configuration API the SDK fetches at runtime, or a second script the pinned one loads dynamically.

That last case deserves attention. A pinned loader that fetches unpinned modules provides verification of the loader and nothing else, and strict-dynamic in your CSP will happily extend trust to whatever it loads. Where the vendor’s architecture works that way, the pin is a smaller guarantee than it appears, and the inventory should say so.


Start with one vendor. The first pinned script is where the process gets built — the digest generation, the registry field, the scheduled job and the failure policy — and every vendor after it is a one-line addition to a system that already exists.

Up: Third-Party Isolation & Sandboxing Strategies