Blog · 2026-06-21 · Subresource Integrity · Supply Chain Security · CSP · MCP Servers
MCP Server Subresource Integrity (SRI) Security: integrity attribute verification, hash algorithm selection, CORS requirements, dynamic import() limitations, and CSP require-sri-for
Subresource Integrity (SRI) is the browser's built-in mechanism for verifying that a fetched script or stylesheet has not been tampered with in transit or at the CDN. A single integrity="sha384-..." attribute on a <script> or <link> tag causes the browser to compute a cryptographic hash of the fetched bytes and compare it to the attribute value before executing or applying the resource — any mismatch blocks execution and logs a console error. For MCP server dashboard UIs and client-side tool runners that load third-party scripts from CDNs, SRI is the primary defense against CDN-level supply chain compromise. But SRI has a critical failure mode involving CORS headers that silently disables the check with no error — leaving you exactly as unprotected as if the attribute were absent, while creating the false confidence that it was present.
What SRI is and how the integrity attribute works
Subresource Integrity is defined in the W3C SRI specification and has been supported in all major browsers since 2016. The mechanism is straightforward: when a browser fetches a resource referenced by a <script src> or <link rel="stylesheet" href> element that carries an integrity attribute, the browser computes the cryptographic hash of the response body and compares it to the base64-encoded hash embedded in the attribute. If the hashes match, the resource is executed or applied. If they do not match, the browser refuses to execute the resource and fires an error event on the element — the same error event that fires for a network failure, but with a distinct console message.
The attribute format is strictly defined: <algorithm>-<base64-encoded-hash>. The algorithm must be one of sha256, sha384, or sha512. The base64 encoding must be standard base64 (not URL-safe base64). The hash is computed over the raw response body bytes — the bytes as returned by the server, before any browser processing, decompression, or encoding normalization. A gzipped CDN response is verified against the hash of the decompressed content (the browser decompresses first, then hashes).
<!-- Minimal SRI example: sha384 hash on a CDN-hosted React build -->
<script
src="https://cdn.example.com/react/18.3.1/react.production.min.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8w"
crossorigin="anonymous">
</script>
<!-- SRI on a stylesheet -->
<link
rel="stylesheet"
href="https://cdn.example.com/tailwind/3.4.0/tailwind.min.css"
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
crossorigin="anonymous">
<!-- Multiple hash values — browser verifies any one matches (algorithm agility) -->
<script
src="https://cdn.example.com/lodash/4.17.21/lodash.min.js"
integrity="sha256-geUvdk7DecfAs/k+1/+vulanAU7E/kpFozA4LAh0iNE=
sha384-wg5Y/JwtQy3HzF8jt2Q2Y0B9H5M9JtQ9H5M9Jt2Q2Y0B9H5M9Jt2Q2Y0B9H5M9"
crossorigin="anonymous">
</script>
SRI applies only to <script> and <link rel="stylesheet"> elements. It does not apply to <img>, <video>, <audio>, <iframe>, CSS @import, or any resource loaded via fetch() or XMLHttpRequest — those APIs load the resource regardless of any hash you might attempt to specify, and the browser has no enforcement mechanism for them. SRI is a declarative HTML attribute mechanism, not a fetch-level API.
When an SRI check fails, the browser produces a console error message of the form: "Failed to find a valid digest in the 'integrity' attribute for resource 'https://cdn.example.com/...' with computed SHA-384 integrity '...'. The resource has been blocked." The element's error event fires, and any onload callback is not called. From a JavaScript perspective, the behavior is identical to a network failure — the script is not available, and any code that depends on it will encounter a ReferenceError or undefined global.
Generating SRI hashes: The canonical tool for generating SRI hashes is openssl dgst -sha384 -binary < file.js | openssl base64 -A combined with the algorithm prefix, or the shasum -a 384 command with base64 piping. The srihash.com tool and the integrity field in webpack and Vite build outputs automate this. Never generate a hash from a CDN URL at build time without also pinning the CDN version — the same URL may serve different content at different times if you use a floating version like @latest.
Why sha384 is the recommended algorithm
The SRI specification permits sha256, sha384, and sha512. The choice of algorithm has meaningful security and performance implications. The specification defines a priority ordering when multiple algorithms appear in the attribute: sha512 > sha384 > sha256. When multiple hashes with different algorithms are present, the browser uses only the strongest algorithm's hash for verification — the weaker hashes are ignored for that verification pass.
| Algorithm | Hash length | Base64 length | Collision resistance | Recommendation |
|---|---|---|---|---|
sha256 | 256 bits | 44 chars | 128-bit security level — meets NIST SP 800-57 current guidance for data integrity through 2030 | Minimum acceptable. Use only if your build tooling cannot produce sha384. |
sha384 | 384 bits | 64 chars | 192-bit security level — well above current and near-future attack thresholds; significantly better collision resistance than sha256 at modest hash length cost | Recommended. Standard choice for SRI in production. |
sha512 | 512 bits | 88 chars | 256-bit security level — maximum available in SRI; slight performance overhead for very large files | Valid. Use for highest-assurance environments or when you already generate sha512 for other purposes. |
The performance difference between sha256, sha384, and sha512 is negligible for browser-side hash verification of typical JavaScript and CSS files — the hashing takes single-digit milliseconds even on mobile devices. The choice is essentially a security posture decision. NIST's guidance upgrades the recommended minimum to sha384 for data integrity beyond 2030 due to the growing viability of birthday attacks against sha256 in certain collision scenarios.
Multiple hash values in a single integrity attribute — separated by spaces — serve a specific purpose: algorithm agility during a migration. If you are transitioning from sha256 to sha384 across a large CDN asset fleet, you can specify both hashes during the transition period, and browsers that cached the old sha256 hash (in extension-based SRI policies or pinning tools) will still verify successfully, while the primary verification uses sha384:
<!-- Algorithm agility: specify both sha256 and sha384 during migration --> <!-- Browser uses the strongest algorithm present (sha384 in this case) --> <!-- Legacy pinning tools checking sha256 still get a valid match --> <script src="https://cdn.example.com/toolkit/2.1.0/toolkit.min.js" integrity="sha256-abc123...= sha384-xyz789...=" crossorigin="anonymous"> </script> <!-- After migration is complete, remove sha256 from all elements --> <script src="https://cdn.example.com/toolkit/2.1.0/toolkit.min.js" integrity="sha384-xyz789...=" crossorigin="anonymous"> </script>
Multiple hashes are OR, not AND: The browser verifies that at least one of the provided hashes matches — not that all of them match. If you specify sha256-AAAA sha384-BBBB and the browser uses sha384, it computes the sha384 hash and checks only against BBBB. It does not verify the sha256 AAAA value. This is intentional for algorithm agility but means you cannot use multiple hashes as a double-check — they are alternatives, not redundant verifications.
The CORS requirement — the most misunderstood SRI failure mode
This is the section that most SRI tutorials skip, and it is the source of the most dangerous SRI misconfigurations in production. The rule is: SRI verification for cross-origin resources requires both a valid CORS response from the server AND a crossorigin attribute on the HTML element. If either condition is absent, the behavior depends on which one is missing — and one of the two cases is a silent, undetectable failure.
The underlying reason is that SRI verification requires the browser to be able to read the response body to hash it. The browser's same-origin policy normally blocks JavaScript from reading cross-origin response bodies. SRI needs to read those bytes to compute the hash. The mechanism for allowing a cross-origin response to be read is CORS — specifically, a CORS response with appropriate Access-Control-Allow-Origin headers. The crossorigin attribute on the element signals to the browser that it should make a CORS-mode request for this resource, which enables the CORS protocol and, in turn, enables SRI to read and verify the response bytes.
Case 1: integrity + crossorigin="anonymous" + CDN sends CORS headers
This is the correct, protected configuration. The browser makes a CORS-mode request, the CDN responds with Access-Control-Allow-Origin: *, the browser reads the response body, computes the sha384 hash, compares to the integrity attribute, and either executes the script or blocks it. Full SRI protection.
Case 2: integrity present, NO crossorigin attribute, CDN sends CORS headers
Silent SRI bypass — the worst case. The browser makes a no-cors-mode request (normal script loading). The CDN sends CORS headers, but the browser ignores them because the request was not CORS-mode. The browser loads and executes the script normally. The integrity attribute is present but the SRI check is silently skipped. No error, no console warning, no block. The script runs unverified.
Case 3: crossorigin="anonymous" + integrity, CDN does NOT send CORS headers
CORS failure (not SRI failure). The browser makes a CORS-mode request. The CDN responds without Access-Control-Allow-Origin. The browser blocks the response — CORS error, not SRI error. The script does not load at all. Console shows CORS error. This is a load failure, not an SRI bypass — but it also means your page is broken.
Case 4: crossorigin="use-credentials" + integrity + CDN sends specific CORS headers
Correct for authenticated CDN resources. The browser makes a credentialed CORS request (sends cookies and auth headers). The CDN must respond with Access-Control-Allow-Origin: https://your-specific-origin.com (wildcard not allowed for credentialed requests) and Access-Control-Allow-Credentials: true. SRI verification proceeds normally after CORS succeeds.
<!-- CASE 1: Correct — full SRI protection --> <script src="https://cdn.example.com/lib.js" integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8w" crossorigin="anonymous"> </script> <!-- CDN response headers must include: Access-Control-Allow-Origin: * --> <!-- CASE 2: SILENT BYPASS — integrity present but crossorigin missing --> <!-- Script loads and executes unverified with NO error or warning --> <script src="https://cdn.example.com/lib.js" integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8w"> <!-- Missing: crossorigin="anonymous" --> </script> <!-- CASE 3: CORS failure — script does not load, console shows CORS error --> <script src="https://cdn.example.com/lib.js" integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8w" crossorigin="anonymous"> </script> <!-- CDN does NOT send Access-Control-Allow-Origin — browser blocks (CORS error) --> <!-- CASE 4: Credentialed CORS — for authenticated CDN resources --> <script src="https://private-cdn.example.com/internal/lib.js" integrity="sha384-..." crossorigin="use-credentials"> </script> <!-- CDN must send: Access-Control-Allow-Origin: https://app.example.com --> <!-- CDN must send: Access-Control-Allow-Credentials: true --> <!-- Wildcard ACAO header does NOT work with use-credentials -->
Case 2 is the critical risk: The silent SRI bypass (integrity attribute present, no crossorigin attribute, CDN serves the resource normally) is exactly what a CDN supply chain compromise exploits. An attacker who compromises the CDN and modifies lib.js does not need to defeat the cryptographic hash — they just need the page to load without crossorigin="anonymous", and the browser helpfully executes the modified script while displaying no error. Automated audits that check for the presence of the integrity attribute but not for the paired crossorigin attribute give a false sense of security.
Verifying your CDN's CORS headers is straightforward with curl. Both conditions — CDN CORS headers and page element crossorigin attribute — must be confirmed:
# Step 1: Verify the CDN sends CORS headers curl -I \ -H "Origin: https://your-app.example.com" \ "https://cdn.example.com/lib.js" # Look for: Access-Control-Allow-Origin: * or Access-Control-Allow-Origin: https://your-app.example.com # Step 2: Confirm the CORS request mode works curl -v \ -H "Origin: https://your-app.example.com" \ -H "Access-Control-Request-Method: GET" \ "https://cdn.example.com/lib.js" 2>&1 | grep -i "access-control" # Step 3: Compute the expected sha384 hash for comparison curl -s "https://cdn.example.com/lib.js" | openssl dgst -sha384 -binary | openssl base64 -A # Output should match the integrity attribute value exactly # Step 4: Audit HTML for the paired crossorigin attribute # grep or audit tooling — check every script/link with integrity for crossorigin presence grep -n 'integrity=' index.html | grep -v 'crossorigin=' # Any output here is a misconfigured SRI element
dynamic import() and SRI — the gap
The import() function — dynamic import — is the standard JavaScript mechanism for lazy-loading ES modules at runtime. It is widely used in MCP server dashboard UIs and client-side tool runners to split code into on-demand chunks, load locale files, and conditionally load heavy libraries. But as of 2026, dynamic import has no integrity checking support. There is no import('./module.js', { integrity: 'sha384-...' }) syntax. The TC39 proposal for module assertions and the follow-on import attributes proposal define an with clause for type annotations, but the integrity option for hash verification has not been standardized or implemented in any browser.
<!-- Static module script: integrity attribute is supported and enforced -->
<script
type="module"
src="/dist/main.js"
integrity="sha384-abc123..."
crossorigin="anonymous">
</script>
<!-- Dynamic import inside main.js: NO integrity checking -->
<!-- The sub-module loads and executes without any hash verification -->
// main.js
async function loadAnalytics() {
// No way to specify an integrity hash for dynamically imported modules
// This import is NOT protected by SRI regardless of the parent module's integrity
const { Analytics } = await import('https://cdn.example.com/analytics/v2.js');
return new Analytics();
}
// The TC39 import attributes proposal syntax (not yet for integrity):
// const mod = await import('./mod.js', { with: { type: 'json' } }); // type annotation only
// There is no: await import('./mod.js', { with: { integrity: 'sha384-...' } });
// This does not exist and is not implemented in any browser as of 2026
This gap is significant because a module script verified by SRI at load time may dynamically import unverified sub-modules at runtime. An attacker who compromises the CDN hosting those dynamically imported modules gains code execution in your page even though the entry-point module was SRI-verified. The sub-module chain is outside the SRI trust boundary.
The current approaches to mitigating dynamic import SRI gaps are:
1. Bundle all module code into a single SRI-protected file. If your build toolchain (Vite, webpack, Rollup, esbuild) produces a single bundled output file, dynamic imports are resolved at build time and included in the bundle. The single output file is covered by one integrity hash on the <script type="module"> element. No runtime dynamic imports, no SRI gap. This is the most complete mitigation but requires disabling code splitting, which may have performance implications for large applications.
2. Use an importmap with integrity fields. The HTML import map specification includes an integrity field (introduced in Chrome 117+, Firefox Nightly as of 2025). When specified, the browser enforces SRI on any module loaded through the mapped specifier — including dynamically imported modules that use the specifier:
<!-- Import map with integrity: browser verifies SRI on dynamic imports via specifier -->
<script type="importmap">
{
"imports": {
"react": "https://cdn.example.com/react/18.3.1/react.production.min.js",
"react-dom": "https://cdn.example.com/react-dom/18.3.1/react-dom.production.min.js"
},
"integrity": {
"https://cdn.example.com/react/18.3.1/react.production.min.js":
"sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8w",
"https://cdn.example.com/react-dom/18.3.1/react-dom.production.min.js":
"sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
}
}
</script>
<!-- Dynamic import using the specifier from the importmap -->
<script type="module">
// This dynamic import IS protected by SRI via the importmap integrity field
const { useState } = await import('react');
// Browser checks hash against the integrity field in the importmap
</script>
3. Service worker as a verification proxy. A service worker can intercept fetch() calls for module scripts and verify their hash using the SubtleCrypto API before returning the response to the browser. This approach is discussed in the next section, along with its own security limitations.
Service worker SRI limitations
Service workers can intercept all fetch() requests within their scope, including module fetches. A service worker that implements hash verification provides SRI-like protection for dynamically imported modules and fetch()-loaded resources that the browser's native SRI mechanism cannot cover. The SubtleCrypto API (crypto.subtle.digest) provides the same SHA algorithms used in SRI:
// service-worker.js — implementing SRI-like verification for dynamic imports
// WARNING: See limitations discussed below before deploying this
const RESOURCE_HASHES = {
'https://cdn.example.com/analytics/v2.js':
'sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8w',
'https://cdn.example.com/charting/lib.js':
'sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN',
};
self.addEventListener('fetch', event => {
const url = event.request.url;
const expectedHash = RESOURCE_HASHES[url];
if (expectedHash) {
event.respondWith(verifyAndRespond(event.request, expectedHash));
}
// Resources not in RESOURCE_HASHES pass through unverified
});
async function verifyAndRespond(request, expectedIntegrity) {
const response = await fetch(request);
const buffer = await response.clone().arrayBuffer();
// Parse the integrity string: "sha384-<base64>"
const [algorithm, expectedBase64] = expectedIntegrity.split('-');
const algoMap = { sha256: 'SHA-256', sha384: 'SHA-384', sha512: 'SHA-512' };
const hashBuffer = await crypto.subtle.digest(algoMap[algorithm], buffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const actualBase64 = btoa(String.fromCharCode(...hashArray));
if (actualBase64 !== expectedBase64) {
console.error(`SRI verification failed for ${request.url}`);
console.error(`Expected: ${expectedBase64}`);
console.error(`Got: ${actualBase64}`);
// Return a synthetic error response to block the script
return new Response('SRI verification failed', { status: 403 });
}
return response;
}
However, service workers have a critical SRI limitation of their own: the navigator.serviceWorker.register() call does not accept an integrity attribute. The browser verifies service worker script updates using HTTP cache headers — specifically, the browser re-fetches the service worker script URL on each page load and checks if the response differs from the cached version using ETag or Last-Modified. There is no cryptographic hash verification of the service worker script itself.
// Service worker registration — no integrity attribute supported
navigator.serviceWorker.register('/sw.js')
// There is no: navigator.serviceWorker.register('/sw.js', { integrity: 'sha384-...' })
// The second argument accepts { scope, type, updateViaCache } — no integrity option
.then(registration => {
console.log('SW registered:', registration.scope);
});
// The browser uses HTTP cache headers to detect SW updates, not SRI:
// - ETag: "abc123" / If-None-Match: "abc123"
// - Last-Modified: Tue, 21 Jun 2026 10:00:00 GMT
// If an attacker compromises the server hosting /sw.js:
// - They can serve a modified sw.js that bypasses all hash verification
// - The browser has no SRI mechanism to detect the modification
// - The compromised SW then intercepts all fetch() calls and strips your verification logic
This creates a high-severity attack pattern: a compromised service worker registration URL is an SRI bypass for all subsequent fetch() calls the service worker intercepts. An attacker who can serve a modified sw.js from your origin can replace your verification logic with a pass-through, defeating the entire service-worker-as-SRI-proxy approach. The mitigation is to serve sw.js with Cache-Control: no-store (forcing the browser to re-fetch on every page load, reducing the window for a stale compromised SW) and to implement an internal hash check that the SW verifies against itself on activation:
// sw.js — self-verification on activation (defense-in-depth, not cryptographically robust)
// This is not a replacement for preventing the compromise of sw.js itself
const SELF_EXPECTED_HASH = 'sha384-...'; // Hash of sw.js computed at build time
self.addEventListener('activate', event => {
event.waitUntil(verifySelf());
});
async function verifySelf() {
// Fetch the current sw.js to check its hash
const response = await fetch('/sw.js', { cache: 'no-store' });
const buffer = await response.arrayBuffer();
const hashBuffer = await crypto.subtle.digest('SHA-384', buffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const actualBase64 = btoa(String.fromCharCode(...hashArray));
const actual = `sha384-${actualBase64}`;
if (actual !== SELF_EXPECTED_HASH) {
// SW script has changed — log and refuse to control pages
console.error('Service worker self-verification failed — refusing to activate');
// Note: this only works if the SW was already installed before the compromise
// A freshly installed compromised SW can skip or bypass this check
self.registration.unregister();
}
}
// Serve sw.js with this header to minimize stale-SW attack window:
// Cache-Control: no-store, no-cache
Service worker self-verification is not bulletproof: A freshly served compromised sw.js can simply omit or rewrite the verifySelf() function. The self-verification pattern is useful for detecting accidental content drift but does not protect against a determined attacker who can serve arbitrary content at the /sw.js URL. The real mitigation is ensuring the server hosting sw.js itself is not compromised — SRI cannot protect you from a compromise of same-origin resources.
CSP require-sri-for directive
The require-sri-for Content Security Policy directive requires that all <script> and <link rel="stylesheet"> elements have a valid integrity attribute. Without such an attribute, the browser blocks the resource load — even for same-origin resources. The directive was intended to provide a policy-level enforcement mechanism to complement SRI's per-element declarations.
# CSP header with require-sri-for Content-Security-Policy: require-sri-for script style # This means: # - Every <script src> must have an integrity attribute — or it is blocked # - Every <link rel="stylesheet" href> must have an integrity attribute — or it is blocked # - Inline <script> and <style> elements are NOT affected (they have no src to hash) # - fetch(), XMLHttpRequest, and dynamic import() are NOT affected (API resources, not HTML elements)
However, require-sri-for has significant real-world limitations that make it unreliable as a primary SRI enforcement mechanism in 2026:
Browser support is Chrome-only. Firefox and Safari do not implement require-sri-for. Users on those browsers receive no enforcement. For a directive intended to enforce supply chain security, single-browser enforcement is a significant gap — a CDN compromise affects all users, not just Chrome users.
The directive was deprecated in the CSP specification. The CSP Working Group deprecated require-sri-for in favor of more granular controls via Trusted Types and future CSP Level 3 mechanisms. The directive is not included in CSP Level 3 and browser vendors have discussed removing it. Building a security architecture on a deprecated directive is inadvisable.
It enforces attribute presence, not correctness. If the integrity attribute is present but contains an incorrect or outdated hash, require-sri-for is satisfied — the attribute is present. The SRI mechanism itself will then block the resource (hash mismatch), but require-sri-for does not add any additional verification. Conversely, if the attribute is present but the crossorigin attribute is missing (the silent bypass case described above), require-sri-for is satisfied and no error is produced, but SRI is silently skipped.
<!-- require-sri-for is satisfied by presence of integrity attribute --> <!-- but CANNOT detect the missing crossorigin=anonymous silent bypass case --> Content-Security-Policy: require-sri-for script <!-- CSP is satisfied (integrity present) but SRI is silently bypassed (crossorigin missing) --> <script src="https://cdn.example.com/compromised.js" integrity="sha384-abc123..."> <!-- Missing crossorigin="anonymous" — SRI check silently skipped, script executes unverified --> </script>
The current recommendation from the SkillAudit methodology is: do not rely on require-sri-for as a primary SRI enforcement mechanism. Instead, enforce SRI through:
- Build tooling that automatically generates and injects integrity attributes for all external script and stylesheet references (Vite's
build.cssCodeSplitwith integrity, webpack'ssubresource-integrityplugin, the@sri-hash/rollup-plugin) - CI pipeline checks that scan the built HTML output for
<script src>and<link href>elements without integrity attributes — failing the build if any are found - CI pipeline checks that verify every
integrityattribute is paired with acrossoriginattribute - Automated SRI hash freshness verification that re-computes hashes against live CDN URLs and alerts on drift (CDN content changed without hash update)
SRI in the MCP server context: what to protect and what SRI cannot cover
For MCP server deployments, SRI is relevant in two contexts: the MCP server dashboard or management UI (a web application that may load third-party scripts from CDNs) and any client-side tool runner that loads MCP skill scripts dynamically. SRI is not relevant for the server-side MCP protocol implementation itself — Node.js, Python, or Go servers loading npm/pip/Go modules at install time are covered by lockfile integrity mechanisms (npm's package-lock.json SHA-512 hashes, pip's requirements.txt hash pinning, Go's go.sum file), not by browser SRI.
The related area of MCP server supply chain security covers these server-side dependency integrity mechanisms in detail. SRI specifically addresses the browser-side layer: the HTML page that loads JavaScript and CSS from external CDNs. For completeness, the Content Security Policy deep dive covers how script-src and style-src directives complement SRI by restricting which origins can serve executable content, and the Trusted Types API post covers protecting against DOM-based injection of unverified script elements at runtime — a bypass path for SRI if an attacker can inject a <script> tag without an integrity attribute via a DOM XSS vector.
Generating and maintaining SRI hashes in production
The operational challenge of SRI is hash maintenance: every time a CDN-hosted resource changes (new library version, security patch, CDN migration), the integrity hash must be updated in every HTML file that references it. A stale hash causes the resource to be blocked, breaking the page. An un-updated hash after a CDN compromise provides no protection. The solution is automating hash generation and verification as part of the build and deployment pipeline:
# CI pipeline SRI verification script (bash)
# Run after build to verify all external resources have correct, current SRI hashes
#!/bin/bash
set -euo pipefail
HTML_FILES=$(find ./dist -name "*.html")
FAILED=0
for html_file in $HTML_FILES; do
# Extract all script src + integrity pairs
while IFS= read -r line; do
# Parse src and integrity from the line
src=$(echo "$line" | grep -oP 'src="[^"]*"' | head -1 | cut -d'"' -f2)
integrity=$(echo "$line" | grep -oP 'integrity="[^"]*"' | head -1 | cut -d'"' -f2)
crossorigin=$(echo "$line" | grep -oP 'crossorigin="[^"]*"' | head -1 | cut -d'"' -f2)
# Skip same-origin and data: URLs
if [[ "$src" != https://* ]]; then continue; fi
# Check crossorigin attribute is present
if [[ -z "$crossorigin" ]]; then
echo "ERROR: Missing crossorigin on $src in $html_file"
FAILED=1
continue
fi
# Fetch the resource and compute its hash
algorithm=$(echo "$integrity" | cut -d'-' -f1)
expected_hash=$(echo "$integrity" | cut -d'-' -f2)
actual_hash=$(curl -sL "$src" | openssl dgst -${algorithm} -binary | openssl base64 -A)
if [[ "$actual_hash" != "$expected_hash" ]]; then
echo "ERROR: SRI hash mismatch for $src"
echo " Expected: $expected_hash"
echo " Actual: $actual_hash"
FAILED=1
else
echo "OK: $src"
fi
done < <(grep -oP '<script[^>]*integrity="[^"]*"[^>]*>' "$html_file")
done
if [[ $FAILED -ne 0 ]]; then
echo "SRI verification failed — blocking deployment"
exit 1
fi
echo "All SRI hashes verified successfully"
For Vite-based MCP dashboard UIs, the @vite-plugin-sri plugin or Vite's built-in build.rollupOptions with the subresource-integrity plugin automates hash injection at build time. For webpack, the webpack-subresource-integrity plugin performs the same function. The key configuration requirement is that these plugins must also inject the crossorigin="anonymous" attribute — plugins that inject only the integrity attribute without crossorigin are partially helpful but create the silent bypass case described above.
// vite.config.ts — SRI with crossorigin attribute injection
import { defineConfig } from 'vite';
import { createHtmlPlugin } from 'vite-plugin-html';
export default defineConfig({
build: {
// Rollup options for SRI hash generation
rollupOptions: {
output: {
// These are injected into generated <script> tags automatically
// with the subresource-integrity plugin
}
}
},
plugins: [
// Note: verify your chosen SRI plugin injects BOTH integrity AND crossorigin
// A plugin that injects integrity without crossorigin is a silent bypass risk
],
// Alternative: use a post-build script to inject both attributes
// and verify against live CDN content before deployment
});
Lock CDN versions absolutely: SRI hashes are content-addressed — the hash corresponds to the exact bytes of a specific version. Using floating CDN version URLs like cdn.example.com/react@latest/react.min.js with a pinned hash will cause an SRI block the next time the CDN updates the content at that URL. Always use absolute version pinning in CDN URLs (cdn.example.com/react/18.3.1/react.min.js) alongside SRI hashes. Treat a version bump as a two-step operation: update the CDN URL version, fetch the new resource, compute the new hash, update the integrity attribute, commit all three changes together.
SkillAudit findings for Subresource Integrity
In SkillAudit's review of MCP server dashboard UIs and client-side tool runners, SRI misconfigurations appear in the majority of codebases that load any third-party resources from CDNs. The silent CORS bypass — integrity attribute present, crossorigin attribute missing — is the most common critical finding, as it creates the appearance of supply chain protection while providing none.
integrity attribute — supply chain compromise of the CDN grants arbitrary code execution in the application with no browser-level defense
<script> or <link> has integrity attribute but missing crossorigin="anonymous" — SRI check is silently skipped, resource executes unverified, no console error or warning is produced
dynamic import() importing third-party modules from CDN URLs — runtime sub-modules unprotected by SRI; no importmap integrity fields or bundling in place to close the gap
Cache-Control: no-store — a compromised or stale service worker can intercept and bypass all fetch()-level hash verification implemented in the SW
sha256 used instead of sha384 for CDN resource integrity attributes — below current recommended minimum for collision resistance per NIST SP 800-57 forward guidance
SRI implementation checklist
- Add
integrity="sha384-..."attribute to every<script src>referencing a cross-origin CDN resource - Add
integrity="sha384-..."attribute to every<link rel="stylesheet" href>referencing a cross-origin CDN resource - Pair every
integrityattribute withcrossorigin="anonymous"— never omit the crossorigin attribute on an SRI-protected element - Verify the CDN sends
Access-Control-Allow-Origin: *(or your specific origin) withcurl -H "Origin: ..." - Use
sha384as the hash algorithm — upgrade any existingsha256hashes - Use absolute version pinning in CDN URLs (no
@latest, no floating major versions) - Configure build tooling (Vite/webpack SRI plugin) to automatically inject both integrity and crossorigin attributes
- Add CI pipeline step that verifies all external script and stylesheet elements have integrity + crossorigin attributes
- Add CI pipeline step that re-fetches CDN resources and verifies current content matches pinned hash
- For ES module apps: add importmap
integrityfields for dynamically imported CDN modules (Chrome 117+) - Or: bundle all module code into a single output file to eliminate dynamic import CDN loading
- Serve
sw.jswithCache-Control: no-storeif using service workers for fetch verification - Do not rely on
CSP: require-sri-foras a primary enforcement mechanism — enforce via build tooling and CI instead
Audit your MCP server's supply chain posture: SkillAudit scans MCP server dashboard UIs and client-side tool runners for SRI misconfigurations — missing integrity attributes, the silent CORS bypass pattern (integrity without crossorigin), sha256 downgrades, CDN resources loaded via dynamic import without importmap integrity fields, and service workers served without Cache-Control: no-store. Paste a GitHub URL to get a graded SRI and supply chain security report in 60 seconds. For related browser security controls, see the Content Security Policy deep dive, the Trusted Types API security post, and the supply chain security overview.