You have decided to pin a vendor script and need the mechanics: where the digest comes from, where it lives, and how a version bump happens without an outage in the middle of it.
Triage: Confirm the URL Is Actually Immutable
Pinning a URL that changes produces an outage on the vendor’s schedule. Establish immutability before generating anything.
- Fetch the URL and record its digest. Wait an hour, fetch again, and compare. Any difference disqualifies it.
- Fetch from a second region — a different network, or a proxy — and compare again. Some CDNs vary a build banner per edge.
- Check the URL shape. A version segment such as
/3.4.1/is a good sign;/latest/or a query parameter is not. - Confirm the response carries
Access-Control-Allow-Origin, without which the browser cannot verify at all.
Root Cause: A Hash Is a Promise About Bytes
The integrity attribute states that the resource at this URL will hash to this value. The browser enforces it by hashing the response and refusing to execute on a mismatch — so the attribute is only as good as the assumption that the bytes are fixed, and everything operational about the technique follows from managing that assumption.
Two properties of the mechanism make the workflow tractable. The browser accepts a list of digests and executes if any one matches, which is what allows a version change to be split across two deploys with no broken state in between. And the check applies only to responses the page may read, which is why CORS is a hard prerequisite rather than a detail — without it the attribute is present and inert, which is worse than absent because it looks like protection.
The failure mode that matters is not a mismatch, which is loud. It is the pin that has silently stopped applying: a crossorigin attribute dropped in a refactor, or a vendor changing their CORS headers, leaving the attribute in the markup with nothing enforcing it. That state produces no error and no warning, and it can persist indefinitely.
Resolution: Generate, Store, Roll
# Generate from the exact URL you will ship, including any query string.
curl -sS https://cdn.vendor.example/sdk/3.4.1/client.js \
| openssl dgst -sha384 -binary | openssl base64 -A
// vendors.js — one record per vendor, consumed by templates, CSP and CI.
export const VENDORS = {
acme: {
src: 'https://cdn.vendor.example/sdk/3.4.1/client.js',
// A list. During a rollover this holds two values; otherwise one.
integrity: ['sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC'],
origin: 'https://cdn.vendor.example',
category: 'analytics',
},
};
<script
src="{{ vendor.src }}"
integrity="{{ vendor.integrity | join(' ') }}"
crossorigin="anonymous"
defer></script>
The rollover is then two ordinary deploys. The first changes src to the new version and appends the new digest to the list, so both the new bytes and a revert to the old ones verify. The second, once the new version has run long enough to trust, removes the old digest. At no point is there a state in which nothing in the list matches, which is what makes the change revertible.
For scripts injected at runtime, set the property before appending, exactly as with fetchPriority:
const s = document.createElement('script');
s.src = v.src;
s.integrity = v.integrity.join(' '); // before appendChild, not after
s.crossOrigin = 'anonymous';
document.head.appendChild(s);
Verification: Break It Deliberately
Change one character of the digest in a staging build and confirm the script does not execute and the console names the mismatch. Restore it and confirm the script runs. A build in which both cases execute has an inert attribute — check crossorigin and the response headers.
Which Vendors to Pin First
Pinning everything at once is unnecessary and slows adoption. Rank candidates by two properties and start at the top: how much damage a compromised version could do, and how stable the URL is.
The highest-value pins are scripts with broad access and stable URLs — a payment SDK, a session recorder, an authentication widget. These run with full access to your document, so a hostile version is a serious event, and they are usually versioned properly because their vendors think about this. The lowest-value are cosmetic embeds behind rolling URLs, where the pin is impossible and the consequence of compromise is smaller.
Working down that list also builds the process incrementally. The first vendor is where the digest generation, the registry field, the scheduled verification and the failure policy get established; every subsequent one is a single record. Teams that try to pin twelve vendors in one change usually discover that four of the URLs are not immutable, and abandon the whole exercise rather than the four.
Common Pitfalls
-
Omitting
crossorigin. The attribute is present, the check never runs, and nothing reports it. -
Pinning a rolling URL. The first vendor deploy takes your script offline, and the fastest fix under pressure is to delete the attribute permanently.
-
Hand-editing digests in templates. The URL and the digest diverge the first time someone updates one of them.
-
Setting
integrityafterappendChild. The request is already in flight; the property has no effect. -
Generating the digest from a local copy. Hash what the URL returns, not what is in your repository. A vendor’s CDN may serve a differently minified build from the artefact you downloaded once, and the two will not agree.
-
Forgetting the resource hints. A
preloadfor the same URL must carry the sameintegrityvalue, or it occupies a different cache entry and the browser fetches the script twice.
Frequently Asked Questions
Which hash algorithm should we use?
sha384 is the sensible default: it is supported everywhere the attribute is, and it is comfortably strong for this purpose. sha256 is equally acceptable and produces a shorter attribute; sha512 is available and adds length without adding meaningful security for a resource whose contents are public.
What matters more than the choice is consistency. Generating some digests with one algorithm and some with another makes the registry harder to verify programmatically, and a scheduled job comparing digests has to know which algorithm produced each. Pick one, record it in the registry, and use it everywhere.
Can the same approach cover stylesheets?
Yes — integrity applies to <link rel="stylesheet"> as well, with the same CORS requirement and the same behaviour on mismatch, which for a stylesheet means the page renders unstyled. That consequence is usually more visible to a visitor than a missing script, so the failure policy deserves more thought.
Third-party stylesheets are worth pinning for the same reason as scripts. A compromised stylesheet can reposition and disguise interface elements, overlay content, and exfiltrate limited information through selective resource loading — less powerful than script execution, and far from harmless.
What if the vendor serves different bytes per browser?
Then the URL cannot be pinned, and the two-network check above is designed to catch it. Some vendors serve differentiated builds by user agent from a single URL, which is invisible until you compare digests from two clients rather than two networks.
Ask for a build-specific URL. Most vendors that do this also publish per-build paths for exactly this reason, and where they do not, the request is a reasonable one to make. Until then, record the vendor as unpinnable in the inventory rather than pinning the digest your own browser happened to receive.
How do we handle a vendor with dozens of files?
Pin the entry points rather than the whole tree. A vendor shipping fifty modules typically has one or two files your page references directly, and everything else is loaded by those — so pinning the entry gives you a verified root, and 'strict-dynamic' in your policy then extends trust to what it loads without a digest for each.
That is a smaller guarantee than pinning everything, and it is worth being honest about in the inventory: you have verified the loader, not the payload. For a vendor where that distinction matters, self-hosting the whole bundle is the stronger option, because then every file passes through your own pipeline and the question of pinning individual modules disappears.
Should the digest be committed or generated at build time?
Committed. A digest generated at build time from a live fetch is not a pin at all — it records whatever the vendor happened to be serving when the build ran, so a compromised version is faithfully hashed and shipped as approved. The whole point is that a human looked at a specific artefact and recorded its identity.
Generate it once, by hand or by a tool, and commit the value alongside the URL. Let the scheduled verification job compare the live resource against the committed value and open a pull request when they differ, so the change is proposed automatically and accepted deliberately.