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:

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.

SRI fetch, hash, and compare pipeline A sequence showing the browser fetching a cross-origin script in CORS mode, hashing the body, comparing against the pinned integrity digest, and either executing or failing closed. Browser (your page) Third-party CDN parse element integrity + crossorigin GET (CORS mode, Origin sent) 200 + Access-Control-Allow-Origin hash response body sha384(bytes) digest == pinned? match: execute mismatch: block
The browser fetches in CORS mode, hashes the returned bytes, and fails closed on any mismatch — the script never runs.

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:

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.”

CSP versus SRI: complementary layers of resource control A two-lane comparison showing CSP controlling which origins may load and SRI controlling whether the delivered bytes are authentic, meeting at the executed-script gate. Content-Security-Policy controls WHICH origins may be loaded at all script-src https://cdn.example.com Subresource Integrity controls WHETHER the bytes are the authentic ones integrity="sha384-…" Script executes only if BOTH gates pass require-sri-for makes the right lane mandatory
CSP authorizes the source; SRI authenticates the bytes. A resource runs only when both layers agree.

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;
}

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>

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 });
  }
};

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
    },
  })
);

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.

Diagnostic decision tree for a blocked SRI resource A decision tree mapping the two failure causes — missing CORS headers versus a digest mismatch — to their specific fixes. resource blocked console integrity error CORS header present? check ACAO on the asset digest matches bytes? re-run openssl dgst add crossorigin + ACAO both halves required repin to new digest or add rollover hash
Two root causes, two fixes: a missing CORS header needs `crossorigin` plus `Access-Control-Allow-Origin`; a real byte change needs a repinned digest.

Troubleshooting, Misconfigurations & Safe Rollback

Symptom-to-fix mapping for the failure modes that account for nearly every SRI incident:

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.