Core SRI Fundamentals & Browser Security Boundaries
Permalink to "Core SRI Fundamentals & Browser Security Boundaries"Subresource Integrity (SRI) is a W3C browser security mechanism that binds a cryptographic digest to every external resource tag, preventing the browser from executing a script or applying a stylesheet unless its bytes match the declared hash exactly. It is the primary defence against compromised CDN delivery and third-party supply chain injection, and it is directly cited in PCI DSS v4.0.1, SOC 2 Type II, and ISO 27001 as a control for frontend asset verification.
The threat model: what SRI defends against
Permalink to "The threat model: what SRI defends against"Modern frontend stacks load dozens of assets from third-party origins — JavaScript frameworks, analytics, fonts, icon sprites. Each of those fetches is a trust boundary: you control neither the CDN edge node nor the origin storage bucket, so a compromise anywhere in that chain can silently replace a legitimate file with a version that exfiltrates credentials or injects a skimmer.
Concrete attack vectors SRI interrupts:
- CDN cache poisoning. An attacker replaces a cached
.min.jswith a patched version. Without SRI the browser executes it; with SRI, the digest mismatch blocks execution before a single instruction runs. - Registry typosquatting pulled at runtime. A compromised npm package published under a lookalike name serves a modified bundle from a CDN URL. SRI pins the exact bytes the developer audited.
- Supply chain injection through a vendor’s own infrastructure. A third-party analytics provider’s S3 bucket is compromised and a skimmer is prepended to their tag. SRI catches the byte change immediately.
- BGP hijack or anycast misdirection. Traffic to a CDN prefix is rerouted to a malicious edge that returns a manipulated file with a valid TLS certificate. SRI operates on response bytes, not on TLS origin, so the digest mismatch still fires.
SRI does not protect against:
- Inline script injection (use a
script-srcCSP nonce or hash for that). - Same-origin asset tampering (same-origin fetches bypass the CORS/opacity constraint entirely).
- Post-execution DOM mutation by code that already passed the integrity check.
- Compromised TLS private keys (the attacker can serve an unmodified file with a valid certificate; SRI only detects byte-level changes).
Understanding this threat boundary is what tells you which control answers which question. SRI answers “are these the exact bytes I audited”. CSP script-src answers “is this origin permitted to contribute executable code at all”. Trusted Types answers “may this string reach a DOM sink”. A stack that deploys only one of the three leaves a gap the other two would have covered: a hash-pinned bundle served from an allowlisted origin can still assign attacker-controlled markup to innerHTML, and a nonce-protected inline script can still be the thing that loads a tampered bundle. Layering CSP Nonces, SRI and Trusted Types sets out the order in which the three controls fire and which failure each one surfaces to your telemetry.
The SRI fetch lifecycle
Permalink to "The SRI fetch lifecycle"The diagram below shows exactly where in the browser’s resource-loading pipeline the integrity check fires and what each failure path produces.
The critical insight the diagram makes concrete: the crossorigin="anonymous" attribute is not optional boilerplate — without it the browser cannot read the response bytes and the integrity check is silently skipped, defeating the entire mechanism.
Core specification: attribute syntax and algorithm choices
Permalink to "Core specification: attribute syntax and algorithm choices"The SRI specification (W3C Recommendation, 2016, maintained by the WebAppSec WG) defines the integrity attribute for <script> and <link rel="stylesheet"> elements. The value is one or more whitespace-separated hash tokens, each in the form <algorithm>-<base64-digest>.
<!-- Canonical SRI syntax — SHA-384 is the recommended default algorithm -->
<script
src="https://cdn.example.com/framework.min.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"
></script>
<!-- Stylesheet variant -->
<link
rel="stylesheet"
href="https://cdn.example.com/styles.min.css"
integrity="sha384-3cskD8shRqChMKCMlMNPezEwv0GFHsT7OxGnbFPeK4mvPkVGKpkB7Vp0jLvIFRE"
crossorigin="anonymous"
/>
Each token decomposes into three fixed parts, and every one of them is a routine source of copy-paste failure. The diagram below names them.
The digest half is standard base64 with padding, not the URL-safe base64url alphabet, so +, / and any trailing = characters are legal inside the attribute and must survive templating untouched. Tooling that re-encodes with a URL-safe alphabet, or an HTML escaper that turns + into a space, emits a token no browser will ever match. Base64 Encoding Rules for SRI Hashes covers the encoding traps in detail, including why the -A flag on openssl base64 is mandatory: without it OpenSSL wraps output at 64 characters and the embedded newline invalidates the token.
Algorithm comparison
Permalink to "Algorithm comparison"The three supported algorithms differ in collision resistance and output size. Understanding Cryptographic Hash Algorithms covers the mathematical tradeoffs in depth; the table below gives the operational view:
| Algorithm | Output bits | Collision resistance | Key standard | Recommended use |
|---|---|---|---|---|
| SHA-256 | 256 | 128-bit | FIPS 180-4 | Acceptable; minimum viable for new work |
| SHA-384 | 384 | 192-bit | FIPS 180-4 | Default for production SRI |
| SHA-512 | 512 | 256-bit | FIPS 180-4 | High-assurance or PCI-scoped pages |
NIST SP 800-131A Rev 2 deprecated SHA-1 for digital signatures and hash-based verification in 2019. MD5 has been broken for collision attacks since 2004. Neither must appear in SRI integrity values. Modern browsers reject both.
Multiple-hash syntax for algorithm migration
Permalink to "Multiple-hash syntax for algorithm migration"You can list two tokens on a single integrity attribute. The browser selects the strongest algorithm it supports:
<script
src="https://cdn.example.com/lib.min.js"
integrity="sha256-abc123... sha384-xyz456..."
crossorigin="anonymous"
></script>
Use this during a transition from SHA-256 to SHA-384 so older browsers (where SHA-384 support was added in 2015 and is now universal) continue to verify while you confirm all edge caches carry the new hash.
Browser enforcement mechanics
Permalink to "Browser enforcement mechanics"The browser’s integrity check is a fetch-time gate, not a runtime sandbox. Understanding its exact execution point and failure modes is essential for designing reliable fallback strategies.
Enforcement lifecycle (detailed)
Permalink to "Enforcement lifecycle (detailed)"- The HTML parser encounters an element with an
integrityattribute. - The browser issues a CORS-eligible request. The
crossorigin="anonymous"attribute is mandatory; its absence causes the browser to issue a no-CORS opaque fetch whose bytes cannot be read, silently bypassing the check. - The CDN must respond with
Access-Control-Allow-Origin: *(or the origin). Without this, the CORS preflight or response read fails before hashing can begin. - The browser reads the full response body into memory and computes the digest using the specified algorithm.
- The computed base64 digest is compared character-for-character against the
integritytoken(s). Comparison is case-sensitive. - Match: execution or stylesheet application proceeds normally.
- Mismatch: the resource is blocked. A
SecurityErroris thrown internally, anet::ERR_SRI_FAILEDmessage appears in the DevTools console, and if anonerrorevent handler is attached to the element it fires. Crucially, noloadevent fires.
For a full breakdown of enforcement configuration — including how CSP’s require-sri-for directive extends this gate to every resource in a given class — see Browser Enforcement & Security Boundaries.
Failure modes that are not SRI mismatches
Permalink to "Failure modes that are not SRI mismatches"| Failure | Symptom | Not an SRI issue |
|---|---|---|
| CORS preflight rejected | net::ERR_FAILED with CORS error |
CDN missing Access-Control-Allow-Origin |
| TLS certificate error | net::ERR_CERT_* |
TLS layer, not integrity layer |
| 404 / CDN purge | net::ERR_ABORTED |
Resource missing entirely |
| Non-deterministic build | Hash mismatch every deploy | Build tooling, not browser bug |
Production implementation patterns
Permalink to "Production implementation patterns"Webpack
Permalink to "Webpack"The webpack-subresource-integrity plugin computes digests after minification and injects them into the emitted HTML automatically:
// webpack.config.js
const { SubresourceIntegrityPlugin } = require('webpack-subresource-integrity');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
output: {
crossOriginLoading: 'anonymous',
},
plugins: [
new HtmlWebpackPlugin({ template: './src/index.html' }),
new SubresourceIntegrityPlugin({ hashFuncNames: ['sha384'] }),
],
};
The crossOriginLoading: 'anonymous' output option is mandatory — without it, dynamically injected chunks from code-splitting will not carry the crossorigin attribute. For a full Webpack walkthrough with chunk hashing and source-map considerations, see Automating Hash Generation in Webpack 5.
Vite
Permalink to "Vite"// vite.config.ts
import { defineConfig } from 'vite';
import sri from 'vite-plugin-subresource-integrity';
export default defineConfig({
plugins: [sri({ algorithms: ['sha384'] })],
});
Vite’s content-hashed filenames (app.Bx7kqL9a.js) already guarantee immutability; the SRI plugin adds the digest-to-HTML injection step.
Nginx server-side injection for third-party scripts
Permalink to "Nginx server-side injection for third-party scripts"When you do not own the build pipeline for a third-party snippet, generate the hash at deploy time and inject it via a server-side include:
# nginx.conf — add_header for the SRI-verified asset via SSI
ssi on;
# In your HTML template:
# <!--# include file="/sri/framework-hash.txt" -->
# Then set Content-Security-Policy accordingly.
A more composable pattern uses Cloudflare Workers to rewrite <script> tags in flight, appending the current hash from a KV-stored manifest before the browser receives the response.
CI/CD hash gating
Permalink to "CI/CD hash gating"#!/usr/bin/env bash
# post-build-sri-check.sh — run after npm run build
set -euo pipefail
DIST_DIR="dist"
HTML_FILE="${DIST_DIR}/index.html"
# Extract every integrity value from the built HTML and re-compute each
grep -oP 'integrity="sha384-\K[^"]+' "$HTML_FILE" | while read -r declared; do
# find the corresponding src
src=$(grep -oP "src=\"[^\"]+(?= integrity)" "$HTML_FILE" | head -1 | cut -d'"' -f2)
computed=$(openssl dgst -sha384 -binary "${DIST_DIR}${src}" | openssl base64 -A)
if [ "$declared" != "$computed" ]; then
echo "FAIL: hash mismatch for ${src}" >&2
exit 1
fi
done
echo "All SRI hashes verified."
Cross-cutting architecture considerations
Permalink to "Cross-cutting architecture considerations"CORS requirements
Permalink to "CORS requirements"SRI is only effective when combined with CORS. Every CDN serving SRI-tagged assets must include Access-Control-Allow-Origin in its response. The two most common misconfigurations:
- Missing CORS header on the CDN. The browser either cannot read the response (CORS error) or issues an opaque no-CORS fetch that bypasses the check. Either way, SRI provides no protection.
- Caching a CORS response without
Vary: Origin. A CDN that caches a CORS response and later serves it to a non-CORS request (or vice versa) can produce stale responses that fail the check for some users. - Redirects that drop the header on the second hop. A CDN that answers with a 302 to a signed storage URL must emit
Access-Control-Allow-Originon both the redirect response and the final response. A header present only on the first hop yields a CORS failure whose console text mentions the redirect, not the integrity attribute, which sends most people hunting for a hash bug that does not exist.
The distinction that matters operationally: a CORS failure means the browser never got readable bytes, so no digest was ever computed and nothing was verified; a hash mismatch means bytes arrived and were rejected. Only the second is evidence of tampering. How CORS and crossorigin Affect SRI sets out the fetch mode and response-type combinations behind each outcome.
CSP interaction
Permalink to "CSP interaction"Content Security Policy and SRI are complementary, not overlapping:
| Mechanism | Controls | Cannot control |
|---|---|---|
| SRI | Byte-level integrity of a specific fetched resource | What scripts are allowed to run (allowlisting) |
CSP script-src |
Which origins and inline scripts are allowed | Whether a permitted script was tampered with |
CSP require-sri-for |
Enforces that every script/style in the page carries an integrity attribute | Correctness of the hash value itself |
A hardened configuration uses all three together. Configuring Content-Security-Policy with SRI details the header syntax and priority rules.
COOP / COEP and isolation headers
Permalink to "COOP / COEP and isolation headers"Cross-Origin Opener Policy (COOP) and Cross-Origin Embedder Policy (COEP) affect the browser’s process isolation model. Resources loaded in a COEP-enabled context must themselves be CORS-eligible (they need crossorigin and a permissive CORS response) — exactly the same prerequisite that SRI demands. Enabling both together is therefore architecturally consistent and does not require separate CDN configuration.
Dynamic script injection
Permalink to "Dynamic script injection"Scripts injected via document.createElement('script') support SRI, but integrity and crossorigin must be set before the element is appended to the DOM:
// Correct: attributes set before DOM insertion
const s = document.createElement('script');
s.src = 'https://cdn.example.com/widget.min.js';
s.integrity = 'sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC';
s.crossOrigin = 'anonymous';
document.head.appendChild(s);
Setting integrity after appendChild has no effect; the browser has already initiated the fetch. For patterns around loader functions and runtime hash resolution, see Implementing Dynamic Script Loaders with Integrity.
Compliance mapping
Permalink to "Compliance mapping"PCI DSS v4.0.1 — Requirement 6.4.3
Permalink to "PCI DSS v4.0.1 — Requirement 6.4.3"Requirement 6.4.3 became mandatory on 31 March 2025. It applies to all scripts loaded by and executed in the consumer’s browser on payment pages:
"All payment page scripts that are loaded and executed in the consumer’s browser are managed as follows:
- A method is implemented to confirm that each script is authorized.
- A method is implemented to assure the integrity of each script.
- An inventory of all scripts is maintained with written justification as to why each is necessary."
SRI satisfies the integrity assurance element. The “authorized” element requires a documented allowlist (typically driven by CSP script-src combined with the hash inventory from your CI pipeline). The inventory element is satisfied by the build-time SRI manifest your tooling generates.
SOC 2 Type II — Change Management
Permalink to "SOC 2 Type II — Change Management"SOC 2 CC8.1 requires that changes to system components (including third-party scripts) are authorized, tested, and documented. An automated SRI pipeline that regenerates hashes on every build and gates deployment on hash validity directly satisfies the “tested and documented” sub-criteria.
ISO 27001:2022 — A.8.9 Configuration Management and A.8.20 Network Security
Permalink to "ISO 27001:2022 — A.8.9 Configuration Management and A.8.20 Network Security"A.8.9 requires that configurations of assets (including software components) be established, documented, and managed throughout their lifecycle. A.8.20 requires that networks be managed and controlled to protect information in systems and applications. SRI-enforced asset pinning maps to both controls.
Operational runbook
Permalink to "Operational runbook"Hash rotation SLA
Permalink to "Hash rotation SLA"Define rotation triggers in your security runbook:
| Trigger | SLA | Action |
|---|---|---|
| Vendor releases updated bundle | 24 h | Regenerate hash; deploy updated HTML |
| CDN vendor security advisory | 4 h | Pin to a known-good bundle; notify team |
| CI hash mismatch in staging | Block deploy | Investigate before promoting to production |
| Scheduled dependency audit | Weekly | Re-run hash generation; compare to committed manifest |
The clock starts when the vendor publishes, not when someone notices. Everything between those two moments is time the page is either serving a stale-but-safe pinned bundle or, if the vendor invalidated the old artifact, serving nothing at all — SRI fails closed, so a rotation you have not shipped presents to users as a missing script.
Mismatch telemetry
Permalink to "Mismatch telemetry"Browser-native SecurityPolicyViolationEvent fires when a CSP require-sri-for directive is active, but SRI mismatches without CSP do not emit a structured event — they only produce a console error. Capture them by attaching an onerror listener:
// Centralized SRI mismatch reporter — attach before any SRI-tagged script loads
document.addEventListener('error', (e) => {
const target = e.target;
if (target instanceof HTMLScriptElement && target.integrity) {
fetch('/api/sri-violation', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
src: target.src,
integrity: target.integrity,
timestamp: Date.now(),
userAgent: navigator.userAgent,
}),
keepalive: true,
});
}
}, true /* capture phase */);
Two operational details make or break this listener. It must be registered in an inline bootstrap covered by a CSP nonce and placed before the first SRI-tagged tag in the document — a reporter loaded from an external bundle can be blocked by exactly the failure it exists to report. And it must use the capture phase, because error events from resource elements do not bubble; a listener attached with the default options never sees them. Because the console message carries no structured payload, the capture-phase listener and CSP violation reports raised by require-sri-for are the only two reliable collection channels; Alerting on SRI Failures from CSP Reports covers the report endpoint shape and which thresholds are worth paging on rather than filing.
Rollback procedure
Permalink to "Rollback procedure"- Identify the last known-good asset version from your CDN or build artifact store.
- Re-compute its SHA-384 digest:
openssl dgst -sha384 -binary asset.js | openssl base64 -A. - Update the
integrityattribute in your HTML or HTML template. - Deploy the updated HTML. The rollback is complete in one deploy cycle.
- Log the incident in your change management system with the affected asset URL, hash before and after, and time-to-detect.
Common pitfalls and mitigations
Permalink to "Common pitfalls and mitigations"| Issue | Root cause | Fix |
|---|---|---|
crossorigin attribute omitted |
Treated as optional; CDN request is no-CORS | Always include crossorigin="anonymous" on every SRI-tagged element |
| Hash computed from development source | Minification and transpilation change bytes | Always hash the final production artifact, not the source file |
| Non-deterministic build output | Timestamps, content-hashes, or plugin ordering differs between runs | Pin all build tooling versions; use deterministic build flags (e.g. Webpack output.hashSalt) |
| SHA-1 or MD5 used | Legacy tooling default | Browsers reject both; enforce SHA-384 in your pipeline config |
| Static hash hardcoded without CI regeneration | Manual one-time setup | Automate hash injection as part of every build step |
| CORS header missing on CDN | CDN origin rule not configured | Add Access-Control-Allow-Origin: * to CDN response rules; include Vary: Origin |
require-sri-for CSP directive missing |
SRI is advisory, not enforced at policy level | Add Content-Security-Policy: require-sri-for script style in staging first, then production |
| Ignoring onerror events | No monitoring for mismatch failures | Implement the capture-phase error listener and forward to your observability pipeline |
FAQ
Permalink to "FAQ"Does SRI protect against XSS attacks?
No. SRI verifies the integrity of externally fetched resources at fetch time. It does not prevent inline script injection, DOM clobbering, or same-origin attacks. Use a strict Content-Security-Policy with a nonce or hash-based script-src to address inline injection, and Trusted Types to restrict DOM sinks.
Why is crossorigin="anonymous" required for SRI to work?
Without crossorigin="anonymous", the browser issues a no-CORS fetch that produces an opaque response. The browser cannot read the bytes of an opaque response, so hash computation is impossible and the integrity check is silently skipped — the resource loads as if no integrity attribute was present. This is the single most common real-world SRI bug.
Can I use SHA-256 instead of SHA-384?
SHA-256 provides 128-bit collision resistance and is supported by all major browsers. It is acceptable for general web assets. SHA-384 is the W3C recommendation for SRI and provides 192-bit resistance; it is the safer default for production systems and the required minimum for high-risk pages such as payment flows. The SHA-256 vs SHA-384 vs SHA-512 for SRI page gives the full comparison, including the negligible client-side hashing cost difference on bundles under a few megabytes.
What happens when an SRI check fails in production?
The browser blocks script execution or stylesheet application and logs net::ERR_SRI_FAILED in the DevTools console. Any onerror handler attached to the element fires; the load event does not. Without a fallback, the blocked resource is silently unavailable and any code that depends on it will throw runtime errors. For patterns that recover gracefully, see Graceful Fallback Strategies.
Does SRI work with dynamically injected scripts?
Yes, but integrity and crossorigin must be set on the HTMLScriptElement before it is appended to the DOM. Setting them after appendChild has no effect because the browser initiates the fetch at insertion time. The same rule applies to fetch(), which accepts an integrity option in its init object and rejects the promise with a TypeError on mismatch rather than firing an error event. The Adding Integrity to Runtime-Injected Scripts page shows production-safe loader patterns.
Is SRI required for PCI DSS compliance?
PCI DSS v4.0.1 Requirement 6.4.3 (mandatory since 31 March 2025) requires that all scripts loaded on payment pages are inventoried, authorized, and integrity-verified. SRI is the primary browser-native mechanism satisfying the integrity-verification component of that requirement. Combine it with a CSP script-src allowlist and a build-time hash inventory for full compliance coverage.
Can SRI be used with module scripts and import maps?
Yes. The integrity attribute applies to <script type="module"> the same way it does to classic scripts. Import maps (<script type="importmap">) carry an integrity section that maps module URLs to hash tokens, which is the only way to cover statically and dynamically imported modules — those fetches have no element to hang an attribute on. Support is in Chromium 104+, Safari 17.2+ and Firefox 115+. See Using the Import Map integrity Key for the map syntax and its fallback behaviour on older engines.
Related
Permalink to "Related"- Understanding Cryptographic Hash Algorithms — SHA-256 vs SHA-384 vs SHA-512 tradeoffs, digest generation commands, and algorithm migration guidance.
- Browser Enforcement & Security Boundaries — Detailed enforcement lifecycle, CSP
require-sri-for, and COOP/COEP interaction. - Graceful Fallback Strategies —
onerrorhandler patterns, secondary origin routing, and CSP report-only monitoring. - Integrity for Workers & WebAssembly — Where the
integrityattribute stops applying, and how to verify worker scripts, WebAssembly modules and service worker caches instead. - Supply Chain Auditing & Dependency Verification — Lockfile integrity, SBOM generation, and provenance verification as the broader context for SRI in a defence-in-depth strategy.