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.

AlgorithmHash lengthBase64 lengthCollision resistanceRecommendation
sha256256 bits44 chars128-bit security level — meets NIST SP 800-57 current guidance for data integrity through 2030Minimum acceptable. Use only if your build tooling cannot produce sha384.
sha384384 bits64 chars192-bit security level — well above current and near-future attack thresholds; significantly better collision resistance than sha256 at modest hash length costRecommended. Standard choice for SRI in production.
sha512512 bits88 chars256-bit security level — maximum available in SRI; slight performance overhead for very large filesValid. 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:

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.

CRITICAL −22 CDN-hosted third-party script loaded without any integrity attribute — supply chain compromise of the CDN grants arbitrary code execution in the application with no browser-level defense
CRITICAL −20 Cross-origin <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
HIGH −18 Module script with 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
HIGH −16 Service worker registered at a URL served without Cache-Control: no-store — a compromised or stale service worker can intercept and bypass all fetch()-level hash verification implemented in the SW
MEDIUM −12 sha256 used instead of sha384 for CDN resource integrity attributes — below current recommended minimum for collision resistance per NIST SP 800-57 forward guidance
MEDIUM −10 Single algorithm specified in integrity attribute with no fallback hash — no algorithm agility path during hash migration; version bumps require atomic update of URL and hash with no transition period

SRI implementation checklist

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.