Subresource Integrity and Supply Chain
What is Subresource Integrity?
Subresource Integrity (SRI) is a W3C security feature that lets browsers verify that resources loaded from third-party origins haven’t been tampered with. You attach a cryptographic hash to an HTML element, and the browser recalculates that hash on the downloaded resource — blocking it when the values do not match.
SRI guards against supply chain attacks. If a CDN gets compromised and a malicious version of a library gets served, sites using SRI will refuse to load it.
How the integrity attribute works
The integrity attribute holds a base64-encoded cryptographic hash prefixed by the algorithm:
<script
src="https://cdn.example.com/library.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxyZrx..."
crossorigin="anonymous"
></script>
Format: <algorithm>-<base64-hash>
Supported algorithms: sha256, sha384, sha512.
Here’s the step-by-step flow:
- Browser encounters
<script src="..." integrity="sha256-..."> - Browser fetches the resource from the CDN
- Browser reads the
integrityvalue and decodes the base64 hash - Browser computes the hash of the downloaded bytes using the specified algorithm
- Computed hash matches stored hash? The resource executes. Mismatch? The resource is blocked.
Why crossorigin is required
The crossorigin="anonymous" attribute is not optional for cross-origin resources. Without it, the browser may receive an opaque response — one where the body cannot be read. SRI validation requires reading the raw response body to compute the hash.
Even for publicly accessible CDN files, crossorigin="anonymous" triggers a CORS-aware fetch, which is what enables hash verification.
<!-- SRI validation may silently fail without crossorigin -->
<script src="https://cdn.example.com/lib.js" integrity="sha384-..."></script>
<!-- Correct — enables body reading for hash verification -->
<script src="https://cdn.example.com/lib.js" integrity="sha384-..." crossorigin="anonymous"></script>
Generating SRI hashes
SHA-256, SHA-384, and SHA-512
| Algorithm | Hash Length | Security Level |
|---|---|---|
sha256 | 256 bits | Minimum viable |
sha384 | 384 bits | Recommended |
sha512 | 512 bits | Strongest |
SHA-384 is the recommended balance of security and performance.
CLI (Linux/macOS)
# SHA-384
shasum -a 384 library.js | awk '{print "sha384-" $1}' | base64
The shasum command computes the hash of a local file. The output is piped through awk to prepend the algorithm name and then base64-encoded. This works for files you have already downloaded to your machine.
Node.js
const crypto = require('crypto');
const fs = require('fs');
function generateSRIHash(filePath, algorithm = 'sha384') {
const fileBuffer = fs.readFileSync(filePath);
const hash = crypto.createHash(algorithm).update(fileBuffer).digest('base64');
return `${algorithm}-${hash}`;
}
const sri = generateSRIHash('./node_modules/lodash/lodash.min.js', 'sha384');
console.log(sri);
// Output: sha384-RXVoVUXHKy5JVeEymv...
Fetch a URL and generate the hash
When the file you need is only available at a URL and you have not downloaded it locally, you can fetch it from Node.js and compute the hash in one pass. This is useful for quick verification during development or for scripting SRI hash generation in CI:
// fetch-and-hash.js
const crypto = require('crypto');
const https = require('https');
const url = 'https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js';
const algorithm = 'sha384';
https.get(url, (res) => {
const chunks = [];
res.on('data', chunk => chunks.push(chunk));
res.on('end', () => {
const buffer = Buffer.concat(chunks);
const hash = crypto.createHash(algorithm).update(buffer).digest('base64');
console.log(`${algorithm}-${hash}`);
});
}).on('error', err => console.error('Fetch error:', err));
Online Tools
- srihash.org fetches a URL and returns the full
integrityattribute ready to paste into your HTML - Other online SRI generators can compute hashes from pasted content or URLs
Verifying a file against an SRI hash
// verify-sri.js
const crypto = require('crypto');
const fs = require('fs');
function verifySRI(filePath, expectedSRI) {
const [algorithm, expectedBase64] = expectedSRI.split('-');
const fileBuffer = fs.readFileSync(filePath);
const actualHash = crypto.createHash(algorithm).update(fileBuffer).digest('base64');
const matches = crypto.timingSafeEqual(
Buffer.from(expectedBase64, 'base64'),
Buffer.from(actualHash, 'base64')
);
console.log(`Expected: ${expectedSRI}`);
console.log(`Computed: ${algorithm}-${actualHash}`);
console.log(`Match: ${matches ? 'YES — integrity verified' : 'NO — file tampered!'}`);
return matches;
}
verifySRI('./vendor/library.min.js', 'sha384-RXVoVUXHKy5JVeEymv...');
Use crypto.timingSafeEqual to prevent timing attacks when comparing hashes.
Applying SRI to different elements
The verification script compares a stored hash against a downloaded file. Passing both the file path and the expected SRI string lets you confirm integrity independently of the browser. Here is how you would embed the verified hash in an HTML document:
<script> (most common)
<script
src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"
integrity="sha384-HI0WcJGdMuDmv4qPFHl4VqDkmCU6RcEFcA=="
crossorigin="anonymous"
></script>
The script element is the most frequent use of SRI, since third-party JavaScript has the greatest potential for damage if compromised. SRI also applies to stylesheets, which can inject CSS-based attacks if tampered with:
<link> (stylesheet)
<link
rel="stylesheet"
href="https://cdn.example.com/styles.css"
integrity="sha384-HASH_HERE"
crossorigin="anonymous"
/>
A compromised stylesheet can redirect users, overlay phishing elements, or steal data through CSS selectors. SRI on <link> elements prevents these attacks by ensuring the delivered stylesheet matches the expected hash. The same protection extends to media elements:
<img> and other elements
SRI also works on <img>, <video>, <audio>, <source>, and <track>:
<img
src="https://cdn.example.com/image.png"
integrity="sha384-HASH_HERE"
crossorigin="anonymous"
alt="..."
/>
A failed integrity check on an image results in the image not rendering — no broken icon placeholder, just an empty space where the image should appear.
What happens when an integrity check fails
SRI is fail-closed. When the computed hash doesn’t match:
- The browser does not execute the resource (for
<script>) or does not apply it (for<link>stylesheets) - A console error is thrown:
Failed to find a valid digest in the 'integrity' attribute for resource 'https://cdn.example.com/lib.js' The resource has been blocked. - The page continues loading normally — SRI failures don’t crash the page
- If a local fallback exists (no
integrityon a local copy), that loads instead
Important: SRI failures happen at the network layer. JavaScript cannot catch them with try/catch — there’s no DOM exception thrown that you can intercept.
Real-world supply chain incidents
The event-stream incident (2018)
A malicious actor created the event-stream npm package and added a dependency on flatmap-stream, which contained code designed to exfiltrate cryptocurrency wallet private keys. The attacker specifically targeted the copay Bitcoin wallet app.
Applications loading event-stream from a CDN without an integrity hash had no defense. Projects using SRI were protected because the tampered version’s hash would not match.
The npm package hijacking (2022)
Attackers compromised npm accounts of maintainers for widely-used packages (colors, faker, web-resource-inliner and others) via phishing. Malicious updates printed obscene ASCII art or bricked applications. These packages had billions of weekly downloads.
SRI on CDN-served copies would have blocked the malicious versions from executing.
Common SRI mistakes to avoid
Do NOT use MD5 or SHA-1. These are cryptographically broken for integrity purposes. Only sha256, sha384, and sha512 are valid per the SRI spec.
Do NOT omit crossorigin="anonymous" on cross-origin resources. The integrity check will silently fail or behave inconsistently across browsers.
Do NOT include query strings in the URL if the CDN ignores them. A ?v=1.0 may be stripped, changing the hash. Pin to specific versioned URLs instead (e.g., @4.17.21).
Do NOT use integrity with data: URIs. Browsers block these combinations for security reasons.
Do NOT assume SRI works for all resource types in all browsers. While <script>, <link>, and <img> have solid support, some elements like <use> for SVG sprites have inconsistent behavior.
Do NOT use SRI as your only defense. It is defense-in-depth. Combine it with CDN access controls and Content Security Policy require-sri-for directives.
SRI and content security policy
You can enforce SRI site-wide using CSP:
<meta http-equiv="Content-Security-Policy" content="require-sri-for script;">
This tells the browser to refuse loading any script without a valid integrity attribute. You can also extend it to stylesheets: require-sri-for script style;
CSP require-sri-for combined with SRI hashes gives you layered protection against supply chain attacks.
Build a hash update routine
One practical part of SRI is maintenance. When a library version changes, the hash must change with it, so teams need a routine for updating both at the same time. Keep the version pin, the file URL, and the integrity value close together in source control so reviewers can see the whole change. That makes updates less error prone and helps the team notice when a CDN asset changed for reasons that were not planned. A tiny checklist can save a lot of time later.
Prefer predictable sources
SRI is easiest to manage when the asset source is stable. If a CDN rewrites paths, changes headers, or serves different content behind the same URL, the hash workflow becomes fragile. Pinning exact versions is a good habit because it gives you a fixed target to verify. If you cannot guarantee a stable URL, host the file yourself or move the dependency into your own build pipeline. The more predictable the source, the less likely you are to spend time chasing hash mismatches.
Use SRI with other controls
SRI is strong, but it works best as part of a wider policy. Content Security Policy, careful dependency pinning, and a small trusted surface all make the browser’s job easier. If your app loads many third-party files, review whether each one is truly needed. Every extra asset adds another place where verification matters. That mindset keeps the security story practical: verify what you can, reduce what you load, and make the remaining dependencies easy to audit.
Watch for operational drift
Security settings can drift when a project grows fast. A script tag that was once pinned and hashed may later get copied into a second page without the integrity attribute, or a new asset may be added without the same review. Periodic checks help catch that drift. Treat the hash list like any other security-sensitive configuration and review it when you add, remove, or replace third-party code. Small discipline at the edges keeps the protection useful over time.
See Also
- Content Security Policy — enforce security headers across your site
- Browser Fetch and XHR — understand how the browser loads resources
- Web Workers — load scripts in isolated worker contexts