Subresource Integrity (SRI)
This is the implementation reference for Subresource Integrity (SRI), part of the Web Security Headers Fundamentals reference. SRI is not a response header — it is an HTML attribute, integrity, that pins a <script> or <link> element to the exact cryptographic hash of the bytes it expects to receive. When the browser fetches the resource it hashes the delivered body, compares it against the pinned digest, and refuses to execute or apply anything that does not match. This converts a compromised CDN, a hijacked package registry, or a man-in-the-middle rewrite from silent remote code execution into a hard, fail-closed load error. This page covers the attribute grammar, the SHA-256/384/512 hash set, why crossorigin="anonymous" is mandatory for cross-origin resources, the interaction with Content-Security-Policy, and how to generate and deliver hashes across Nginx, Apache, Cloudflare, and Node/Express.
Threat Model & Protocol Mechanics
SRI defends the integrity of resources you load but do not host. The classic case is a JavaScript library or stylesheet pulled from a third-party CDN: your origin serves the HTML, but the <script src> points at a domain you do not control. Three attack classes are in scope:
- CDN or origin compromise. An attacker who gains write access to the CDN — a leaked API key, a poisoned build pipeline, a subdomain takeover of the asset host — replaces
library.min.jswith a version that also exfiltrates form fields or session tokens. Every site embedding that file executes the payload. SRI blocks it: the modified bytes hash to a different digest than the one pinned in your HTML, so the browser aborts the load. - In-transit tampering. A network attacker on a resource loaded over plaintext, or through a misconfigured intermediary, rewrites the body. SRI catches the rewrite because it validates bytes, not transport. It complements HTTP Strict Transport Security (HSTS): HSTS guarantees the fetch happens over TLS, SRI guarantees the delivered bytes are the ones you signed off on.
- Supply-chain drift. A CDN silently ships a new minor version at a “latest” URL, or a package is republished under the same path. Without SRI the change is invisible until it breaks; with SRI the load fails loudly the moment the bytes change, turning an unnoticed dependency mutation into an actionable incident.
SRI is specified by the W3C in the Subresource Integrity Recommendation. It applies to <script> and <link rel="stylesheet"> today (the spec reserves room for more element types). The enforcement model is entirely browser-side and per-element: the browser reads the integrity attribute, fetches the resource, computes the digest of the response body with the named algorithm, and performs a constant-time comparison against the pinned value. A match applies the resource; a mismatch is treated as a network error — the script never executes, the stylesheet never applies, and the element’s error event fires.
There is one protocol subtlety that trips up nearly every first deployment: integrity checks are gated on the resource being CORS-eligible. The browser will not expose the bytes of an opaque cross-origin response to the integrity algorithm, because doing so would leak information about resources the page is not otherwise permitted to read. So any cross-origin resource carrying integrity must be requested in CORS mode via crossorigin="anonymous", and the responding server must return an Access-Control-Allow-Origin header that permits the request. Without both halves the browser discards the response as an integrity failure — even when the bytes are correct.
Directive Syntax & Spec
SRI lives in the integrity attribute. Its value is one or more hash expressions separated by whitespace; each expression is an algorithm prefix, a hyphen, and the base64-encoded digest of the resource. Options after the hash list (introduced by a ?) are reserved by the spec and ignored by current browsers.
<!-- Script pinned to a single SHA-384 digest, CORS-enabled -->
<script src="https://cdn.example.com/lib.min.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"></script>
<!-- Stylesheet with two acceptable digests (e.g. during a rollover) -->
<link rel="stylesheet" href="https://cdn.example.com/app.css"
integrity="sha384-BASE64_A sha512-BASE64_B"
crossorigin="anonymous">
| Attribute / token | Applies to | Required | Security impact | When to deviate |
|---|---|---|---|---|
integrity="sha384-…" |
<script>, <link rel=stylesheet> |
Yes, to enable SRI | Pins the exact bytes; any change fails the load | Omit only for same-origin assets you fully control and version |
sha256- prefix |
hash expression | One of 256/384/512 | Weakest of the three; acceptable but shorter margin | Prefer sha384; use sha256 only for parity with an existing pipeline |
sha384- prefix |
hash expression | — | Spec-recommended default balance of strength and size | This is the baseline; deviate upward for the highest-value assets |
sha512- prefix |
hash expression | — | Strongest digest, longest attribute | High-value or long-lived pins where attribute size is irrelevant |
| Multiple space-separated hashes | one element | No | Browser accepts the resource if it matches any listed digest | Use during a resource rollover so old and new bytes both validate |
crossorigin="anonymous" |
element | Yes for cross-origin | Enables CORS mode so the browser may read and hash the body | Drop only for genuinely same-origin resources |
crossorigin="use-credentials" |
element | Situational | Sends cookies/auth with the CORS fetch | Only when the asset host requires credentials and echoes them |
Directive-level notes that decide whether a deployment works:
- The algorithm must be
sha256,sha384, orsha512. No other names are valid;md5andsha1are explicitly excluded because they are not collision-resistant. An unrecognized prefix makes the entire expression invalid and the browser ignores it. - When multiple hashes are listed, the browser picks the strongest algorithm present and validates against every digest of that algorithm. It does not require all of them to match — a resource passes if it matches any one hash of the strongest algorithm you supplied. This is what makes rollovers safe: list both the outgoing and incoming digest and either byte-set validates.
- Base64 must be standard, not URL-safe, and correctly padded. A digest computed with a URL-safe alphabet (
-/_instead of+//) will never match. Generate withopensslpiped throughopenssl base64, never a hand-rolled encoder. - The hash covers the raw response body after content-decoding. If the CDN serves the file gzipped, the browser hashes the decompressed bytes — so generate the digest from the uncompressed original, which is exactly what
opensslon the source file produces.
SRI interlocks with CSP at two points. First, CSP can make SRI mandatory rather than opt-in: the require-sri-for script style directive instructs the browser to refuse any <script> or <link rel=stylesheet> that lacks an integrity attribute, closing the gap where a developer forgets to pin a new dependency. (This is a separate mechanism from require-trusted-types-for, which governs DOM XSS sinks, not resource integrity — do not conflate the two.) Second, a nonce- or hash-based Content-Security-Policy governs whether a script origin is allowed to load at all, while SRI governs whether the delivered bytes are authentic. They are complementary layers: CSP says “you may load from this CDN,” SRI says “and only if the file is exactly this.”
Platform-Specific Implementation
SRI itself ships in HTML, so the “config” is twofold: generate the correct digest, and ensure the asset host returns CORS headers so cross-origin integrity checks can read the body. The server blocks below set the Access-Control-Allow-Origin header on the asset responses — the half of the requirement developers most often miss. Compute the digest once per asset build:
openssl dgst -sha384 -binary lib.min.js | openssl base64 -A
# → prints the base64 digest; prefix it with "sha384-" in the integrity attribute
Nginx
# On the server/location that SERVES the asset (the CDN origin), expose it to CORS
location ~* \.(js|css)$ {
add_header Access-Control-Allow-Origin "https://your-site.example" always;
add_header Cross-Origin-Resource-Policy "cross-origin" always;
}
- Security Impact: The
alwaysflag emits the CORS header on every status including 304/4xx, so a conditional or error response does not silently strip the header and turn a valid load into an integrity failure. ScopeAccess-Control-Allow-Originto your exact site, not*, unless the asset is genuinely public. - Verification Command:
curl -sI -H "Origin: https://your-site.example" https://cdn.example.com/lib.min.js | grep -i access-control-allow-origin
Apache
# mod_headers on the asset host / vhost
<FilesMatch "\.(js|css)$">
Header always set Access-Control-Allow-Origin "https://your-site.example"
Header always set Cross-Origin-Resource-Policy "cross-origin"
</FilesMatch>
- Security Impact:
Header always setattaches the CORS header across all response codes, so cached-revalidation (304 Not Modified) responses still satisfy the browser’s CORS-mode requirement for the integrity check. Requiresmod_headers. - Verification Command:
apachectl -t && curl -sI -H "Origin: https://your-site.example" https://cdn.example.com/lib.min.js | grep -i access-control-allow-origin
Cloudflare
// Cloudflare Worker on the asset route — adds CORS so SRI can read the body
export default {
async fetch(request, env, ctx) {
const response = await fetch(request);
const headers = new Headers(response.headers);
headers.set("Access-Control-Allow-Origin", "https://your-site.example");
headers.set("Cross-Origin-Resource-Policy", "cross-origin");
return new Response(response.body, { status: response.status, headers });
}
};
- Security Impact: Setting the header at the edge guarantees every cache HIT and MISS carries
Access-Control-Allow-Origin, so integrity checks do not intermittently fail depending on cache state. The full delivery patterns are in Cloudflare Security Headers at the Edge. - Verification Command:
curl -sI -H "Origin: https://your-site.example" https://cdn.example.com/lib.min.js | grep -i access-control-allow-origin
Node/Express (Helmet)
// Emit the CSP that MAKES SRI mandatory for scripts and styles
const helmet = require("helmet");
app.use(
helmet.contentSecurityPolicy({
useDefaults: true,
directives: {
"script-src": ["'self'", "https://cdn.example.com"],
"require-sri-for": ["script", "style"], // reject any script/style lacking integrity
},
})
);
- Security Impact:
require-sri-for script styleshifts SRI from opt-in to enforced — a newly added<script src>without anintegrityattribute is refused by the browser, so a forgotten pin fails in testing rather than shipping unprotected. Helmet writes theContent-Security-Policyheader with thealways-equivalent behavior of the Express response pipeline. See Node.js Express Helmet configuration for the full middleware order. - Verification Command:
curl -sI https://your-site.example/ | grep -i "require-sri-for"
Verification & Diagnostic Workflows
Verification has two independent checks: the digest matches the bytes, and the CORS headers permit the integrity read.
Regenerate the digest from the live resource and compare it to what is in your HTML:
# Fetch the asset and compute its SHA-384 SRI digest in one line
curl -s https://cdn.example.com/lib.min.js | openssl dgst -sha384 -binary | openssl base64 -A
# Expected: the exact base64 string that follows "sha384-" in your integrity attribute
Confirm the asset host answers CORS so the browser is allowed to hash the body:
curl -sI -H "Origin: https://your-site.example" https://cdn.example.com/lib.min.js \
| grep -iE 'access-control-allow-origin|content-type'
# Expected:
# access-control-allow-origin: https://your-site.example
# content-type: application/javascript
Wire the digest check into CI so a drifted dependency fails the build before deploy:
#!/usr/bin/env bash
# ci-check-sri.sh — fail if the live digest no longer matches the pinned one
set -euo pipefail
URL="https://cdn.example.com/lib.min.js"
PINNED="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
LIVE="sha384-$(curl -s "$URL" | openssl dgst -sha384 -binary | openssl base64 -A)"
if [[ "$LIVE" != "$PINNED" ]]; then
echo "SRI MISMATCH for $URL"
echo " pinned: $PINNED"
echo " live: $LIVE"
exit 1
fi
echo "SRI OK: $URL"
In the browser, a failed check surfaces in the DevTools Console as a message of the form: Failed to find a valid digest in the 'integrity' attribute for resource '…' with computed SHA-384 integrity '…'. The resource has been blocked. The computed digest in that message is the value the browser actually calculated — paste it into your HTML to correct a legitimately updated resource.
Troubleshooting, Misconfigurations & Safe Rollback
Symptom-to-fix mapping for the failure modes that account for nearly every SRI incident:
- Symptom: script blocked, console says “no valid digest,” but the file looks unchanged. The resource is cross-origin and lacks
crossorigin="anonymous", so the browser fetched it in no-CORS mode and refuses to read the body. Fix: addcrossorigin="anonymous"to the element and returnAccess-Control-Allow-Originfrom the asset host. - Symptom: works locally, fails in production behind a CDN. The CDN serves the asset gzipped or minifies/rewrites it, changing the bytes. Fix: compute the digest from the exact bytes the CDN delivers (
curlthe production URL throughopenssl), not from your source tree. - Symptom: intermittent failures, passing on refresh. The
Access-Control-Allow-Originheader is present on200responses but stripped on304 Not Modifiedcache revalidations. Fix: use thealways/Header always setform so the CORS header is emitted on every status code. - Symptom: the whole page’s scripts break after adding
require-sri-for. The CSP directive now rejects every script lackingintegrity, including your own first-party bundles. Fix: addintegrityto every pinned<script>/<link>before enablingrequire-sri-for, or scope it tostylefirst and addscriptonce all scripts are pinned. - Symptom: a legitimate library update breaks the site. The new bytes hash differently and fail the pin. Fix: regenerate the digest and update the attribute; during a staged rollover, list both the old and new digest (space-separated) so either version validates.
- Symptom: digest never matches despite copying it correctly. The digest was base64-encoded with a URL-safe alphabet or is missing padding. Fix: regenerate with
openssl base64 -A, which produces standard, padded base64.
Safe rollback. SRI has no server-state to unwind — it is fail-closed HTML, so the fastest recovery from a bad pin is to remove the integrity attribute from the affected element, which restores the load immediately while you recompute the correct digest. If you enforced SRI globally with CSP, roll back by dropping require-sri-for from the policy so unpinned resources load again:
# Rollback: remove enforcement, keep per-element integrity where it is already correct
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.example.com"
# (require-sri-for removed — the browser stops rejecting unpinned script/style)
Because removing integrity reopens the tamper window, treat it as a temporary measure: repin the correct digest and restore enforcement in the same deploy cycle.
Frequently Asked Questions
Why does SRI require crossorigin="anonymous" for third-party resources?
The browser will not run the integrity algorithm over an opaque cross-origin response, because exposing those bytes to a check would leak whether the response body matches a guessed value. Requesting the resource in CORS mode with crossorigin="anonymous" — and receiving a matching Access-Control-Allow-Origin header from the asset host — makes the body readable to the integrity check. Without both halves the browser treats even a byte-perfect resource as an integrity failure.
Should I use SHA-256, SHA-384, or SHA-512?
Use sha384. It is the value the SRI specification recommends as the default: it is collision-resistant with a comfortable strength margin and produces a shorter attribute than sha512. sha256 is acceptable for pipeline parity but has the smallest margin; sha512 is worth the extra attribute length only for the highest-value, longest-lived assets.
Can I pin multiple hashes on one element? Yes. List several space-separated hash expressions and the browser accepts the resource if it matches any digest of the strongest algorithm present. This is the mechanism for a safe rollover: include both the outgoing and incoming digest during a deploy so old and new bytes both validate, then drop the stale one once the change has propagated.
How is require-sri-for different from require-trusted-types-for?
They solve different problems. require-sri-for script style is a CSP directive that makes an integrity attribute mandatory on scripts and stylesheets, closing the gap where a developer forgets to pin a dependency. require-trusted-types-for 'script' governs DOM XSS sinks like innerHTML by requiring Trusted Types objects — it has nothing to do with resource hashing. Deploy them together for defense in depth, but do not treat one as a substitute for the other.
Does SRI protect resources loaded dynamically by JavaScript?
Not through the HTML attribute. SRI validates <script> and <link rel=stylesheet> elements the parser sees, so a script injected at runtime via document.createElement("script") is unprotected unless you set its integrity property in code. For dynamically loaded modules, enforce integrity through the import map’s integrity field or keep such loads first-party and version-locked.
Related
-
Generating SRI hashes for CDN scripts — openssl and shasum one-liners.