Debugging SRI Validation Failures
The browser console message “Failed to find a valid digest in the integrity attribute” — or its network-panel equivalent, a blocked resource with a net::ERR_SRI_MISMATCH error — means the bytes received from the network do not hash to the value recorded in the integrity attribute. Pinpointing which side is wrong requires isolating whether the problem is in the served bytes, the attribute value, or the fetch configuration.
Symptom and Decision Framing
SRI failures surface in two distinct places:
- Console error:
Failed to find a valid digest in the 'integrity' attribute for resource 'https://cdn.example.com/assets/main.a1b2c3d4.js' with computed SHA-384 integrity 'sha384-XXXX...'. The resource has been blocked. - Network panel: The request shows status
(blocked:sri)in Chrome DevTools or(SRI)in Firefox.
A blocked resource means one of four root causes: the bytes were altered after the hash was computed, the hash was computed with the wrong algorithm or wrong input, the crossorigin attribute is absent on a cross-origin load, or the integrity attribute value is stale from a previous build. Each cause has a different fix. Work through the diagnosis table before editing any configuration.
Diagnosis Decision Table
| Symptom | Likely cause | First check |
|---|---|---|
| Failure only on CDN-served assets, not on local dev | CDN content transform (minification, Brotli re-encoding) | Compare curl --compressed bytes locally vs from CDN |
| Failure immediately after a rebuild with no CDN change | Stale integrity attribute in HTML — old hash, new file | Re-run build and SRI generation together; check HTML was updated |
| Failure on cross-origin assets only | Missing crossorigin attribute |
Inspect the blocked <script> or <link> tag in DevTools Elements panel |
| Failure on all assets including same-origin | Wrong algorithm in integrity prefix |
Check sha256- vs sha384- vs sha512- prefix in the attribute |
| Failure only on dynamically imported chunks | Build plugin did not patch runtime | Verify crossOriginLoading: 'anonymous' in Webpack output config |
| Failure after CDN migration (e.g., moved from Nginx to Cloudflare) | New CDN strips/adds headers, alters body | Re-hash the deployed asset with curl and compare |
| Failure on CSS but not JS | CSS processing pipeline produces non-deterministic output | Check PostCSS autoprefixer, CSS Modules scoping, or source-map injection |
Cause 1: CDN Content Transforms
This is the most common production SRI failure and the hardest to catch in development.
Cloudflare’s “Auto Minify” feature, Amazon CloudFront’s “Compress Objects Automatically” setting, and Nginx gzip_vary with certain proxy configurations can all alter the decompressed response body. The browser’s SRI check operates on the decompressed bytes — transport-layer compression (gzip/Brotli via Content-Encoding) is transparent. But if the CDN strips comments, rewrites whitespace, or changes the character encoding of a CSS file, the decompressed payload differs from the file you hashed at build time.
Verify with curl:
# Download the CDN-served asset (decompressed by --compressed)
curl -s --compressed "https://cdn.example.com/assets/main.a1b2c3d4.js" \
-o /tmp/cdn-main.js
# Hash what you built locally
LOCAL_HASH=$(openssl dgst -sha384 -binary dist/assets/main.a1b2c3d4.js | openssl base64 -A)
# Hash what the CDN served
CDN_HASH=$(openssl dgst -sha384 -binary /tmp/cdn-main.js | openssl base64 -A)
echo "Local: sha384-$LOCAL_HASH"
echo "CDN: sha384-$CDN_HASH"
[ "$LOCAL_HASH" = "$CDN_HASH" ] && echo "MATCH — CDN is not transforming" \
|| echo "MISMATCH — CDN is modifying bytes"
If they differ, find the transform:
# Diff to see what changed
diff <(xxd dist/assets/main.a1b2c3d4.js) <(xxd /tmp/cdn-main.js) | head -40
Resolution by CDN:
# Cloudflare: disable Auto Minify via API for a specific zone
curl -X PATCH "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/settings/minify" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"value": {"css": "off", "html": "off", "js": "off"}}'
For CloudFront, set the cache behavior’s “Compress Objects Automatically” to No for the /assets/* path pattern in your distribution configuration.
Cause 2: Missing crossorigin Attribute
Without crossorigin, the browser sends an opaque request for a cross-origin resource. An opaque response has no readable body — the browser cannot pass the bytes through the hash function. The integrity check therefore fails even when the bytes are identical to what was hashed.
Check the HTML tag in DevTools:
- Open DevTools → Elements panel
- Find the
<script>or<link>tag that is blocked - Confirm whether
crossoriginis present
Correct form for a CDN-hosted asset:
<!-- WRONG: missing crossorigin on cross-origin load -->
<script src="https://cdn.example.com/assets/main.a1b2c3d4.js"
integrity="sha384-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU96xtjz3FAPZ3KZHA12iK">
</script>
<!-- CORRECT -->
<script src="https://cdn.example.com/assets/main.a1b2c3d4.js"
integrity="sha384-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU96xtjz3FAPZ3KZHA12iK"
crossorigin="anonymous">
</script>
For same-origin assets (document and asset share the same scheme+host+port), crossorigin is not required for the SRI check to work. But adding crossorigin="anonymous" to same-origin assets is harmless and avoids confusion if the asset is later moved to a CDN.
Also verify the CDN responds with Access-Control-Allow-Origin:
curl -I -H "Origin: https://www.example.com" \
"https://cdn.example.com/assets/main.a1b2c3d4.js" \
| grep -i "access-control"
# Expected: Access-Control-Allow-Origin: * (or https://www.example.com)
If the CDN does not return that header for this origin, the CORS preflight fails and SRI cannot check the response.
Cause 3: Stale Integrity Value After Rebuild
If your build pipeline computes the SRI hash in one step and updates the HTML template in a separate step — and those steps can be run independently — you can end up with a new asset file but an old integrity value in the HTML.
Check by comparing the HTML’s integrity attribute against the current file:
# Extract the integrity value from the deployed HTML
DEPLOYED_INTEGRITY=$(curl -s "https://www.example.com/" \
| grep -o 'integrity="[^"]*"' | head -1 | sed 's/integrity="//;s/"//')
# Compute the expected integrity from the local build
EXPECTED_INTEGRITY="sha384-$(openssl dgst -sha384 -binary dist/assets/main.a1b2c3d4.js \
| openssl base64 -A)"
echo "Deployed: $DEPLOYED_INTEGRITY"
echo "Expected: $EXPECTED_INTEGRITY"
[ "$DEPLOYED_INTEGRITY" = "$EXPECTED_INTEGRITY" ] \
&& echo "MATCH — HTML and asset are in sync" \
|| echo "MISMATCH — HTML has stale integrity value"
The fix is to couple SRI generation and HTML update into a single atomic step. In your package.json:
{
"scripts": {
"build": "vite build && node scripts/sri-manifest.mjs && node scripts/inject-sri.mjs"
}
}
Never deploy HTML and assets as separate steps if the HTML contains integrity attributes — both must update together. See CI/CD asset pipeline integration for atomic deploy patterns.
Cause 4: Wrong Hash Algorithm
The integrity attribute prefix must exactly match the algorithm used to compute the hash. If your build pipeline computed sha256 but the attribute prefix says sha384, the browser computes SHA-384 over the bytes, finds no matching value in the attribute, and blocks the resource.
Verify the algorithm:
# What algorithm is in the HTML?
curl -s "https://www.example.com/" | grep -o 'integrity="[^"]*"'
# Example output: integrity="sha256-abc123..."
# Compute with the same algorithm and compare
openssl dgst -sha256 -binary dist/assets/main.a1b2c3d4.js | openssl base64 -A
For the MD5 vs SHA-256 for assets context: MD5 is not a valid SRI algorithm. Browsers will reject integrity="md5-..." as an unrecognized algorithm and block the resource. Only sha256, sha384, and sha512 are accepted.
crossorigin presence, then algorithm prefix, then whether the integrity value is stale from a prior build.DevTools Verification Workflow
Chrome DevTools provides the most diagnostic detail for SRI failures:
- Network panel → find the blocked request. Status shows
(blocked:sri). Click the request row. - Headers tab → Response Headers. Confirm
Access-Control-Allow-Originis present. If it is missing,crossoriginis the problem regardless of whether the hash is correct. - Preview tab. If the response body is visible here, the fetch succeeded but the integrity check failed — meaning the bytes differ from the hash. If the preview shows an error, the CORS check failed before SRI could run.
- Console. The full message includes the computed hash:
...with computed SHA-384 integrity 'sha384-XXXXXX'. Copy this value and compare it against theintegrityattribute in the Elements panel.
In Firefox, the Network panel marks blocked requests with [SRI] in the Transferred column. The Console error includes the same computed-hash detail as Chrome.
# Replicate the browser's exact hash computation (cross-platform)
# Downloads with Accept-Encoding to trigger CDN compression response
# then decompresses (--compressed) before hashing — same path as the browser
curl -s \
-H "Accept-Encoding: gzip, deflate, br" \
-H "Origin: https://www.example.com" \
--compressed \
"https://cdn.example.com/assets/main.a1b2c3d4.js" \
| openssl dgst -sha384 -binary | openssl base64 -A
Compare the output against the integrity attribute in the HTML. If they match, the problem is crossorigin or CORS headers. If they differ, the CDN is modifying bytes.
Cause 5: Dynamic Import Chunks Without Runtime Patching
Applications using import() for code splitting generate chunks at build time, but those chunks are loaded at runtime by bundler-generated boilerplate. Without explicit runtime patching, the dynamically constructed <script> tags that load these chunks do not carry integrity attributes.
For Webpack, the fix is crossOriginLoading: 'anonymous' in output config combined with the webpack-subresource-integrity plugin, which patches __webpack_require__.l. Without both, the plugin can set integrity on entry chunks in HTML but cannot reach the runtime loader.
Verify which chunks are covered:
# Check the built HTML for integrity attributes
grep -c 'integrity=' dist/index.html
# Should be >= the number of entry scripts + stylesheets
# Check if the Webpack runtime includes integrity data
grep -c '__webpack_require__.*integrity' dist/assets/runtime.*.js
# Should be > 0 if webpack-subresource-integrity patched the runtime
When to Reconsider
Stop using SRI in these situations:
- CDN edge rendering or ESI — if the CDN assembles the HTML from fragments, the
integrityattribute in the final HTML may have been written by a different process than the one that computed the hash. - A/B test asset variants — if your feature flag system serves different asset bytes to different users from the same URL, a single integrity value cannot be correct for all variants. Use different URLs per variant instead.
- Assets served over HTTP (non-TLS) — browsers still check SRI over HTTP, but the channel is not secure so SRI provides limited protection. TLS is a prerequisite for SRI to be meaningful from a security standpoint.
- Frequently updated third-party assets — if you are embedding a third-party script whose hash changes on every vendor release, you will need to update your
integrityattribute on every vendor update. Evaluate whether the maintenance cost is worth the security benefit. For cache key architecture reasons, content-addressed first-party assets do not have this problem.
Related
- Subresource Integrity validation — the full SRI specification, browser verification flow, and attribute reference
- Generating SRI hashes in your build pipeline — automate hash generation with Webpack, Vite, and Node scripts
- MD5 vs SHA-256 for assets — algorithm selection and why MD5 is rejected by SRI
- Cache key architecture — how fingerprinted URLs and SRI work together for immutable caching