Mapping CDN Origins to SRI Policies
Permalink to "Mapping CDN Origins to SRI Policies"This page is part of CDN Trust Mapping & Routing, which sits within Asset Hashing & Dynamic Script Injection. It covers exactly one workflow: building a deterministic, machine-readable map from CDN origin URLs to SHA-384 integrity hashes, then consuming that map to construct Content-Security-Policy headers that enforce both origin and payload verification simultaneously.
Quick reference
Permalink to "Quick reference"| Attribute / directive | Required value | Notes |
|---|---|---|
integrity on <script> |
sha384-<base64> |
SHA-384 is the industry default; SHA-512 is also accepted |
crossorigin on <script> |
"anonymous" |
Mandatory for cross-origin resources; omitting it makes hash comparison impossible |
script-src in CSP |
https://cdn.example.com 'sha384-<hash>' |
List the origin URL and each hash value |
Access-Control-Allow-Origin on CDN response |
* or your domain |
Required for the browser’s CORS check that gates SRI |
Algorithm identifier in integrity |
sha384- prefix |
Must match the algorithm used during hash generation |
Browser support: all modern browsers (Chrome 45+, Firefox 43+, Safari 11.1+, Edge 17+). IE 11 ignores the integrity attribute silently.
Why origin-to-hash mapping exists
Permalink to "Why origin-to-hash mapping exists"A <script integrity="sha384-..."> attribute alone protects the payload but says nothing about where the file came from. Without a Content-Security-Policy script-src directive that also names the CDN origin, the browser’s fetch is permitted to any origin that returns the right bytes — including an attacker-controlled mirror. Conversely, listing a CDN origin in script-src without integrity hashes allows that CDN to serve any bytes it chooses.
The origin-to-hash map is the connective tissue: it records that a specific CDN base URL is authorized to serve only the assets whose hashes are listed alongside it. At deploy time the map is consumed by the edge layer or CI step that emits the CSP header, ensuring both the origin and the payload constraints are present in the same directive and stay synchronized across deployments.
Canonical implementation: manifest-driven CSP injection
Permalink to "Canonical implementation: manifest-driven CSP injection"The pattern below generates an sri-manifest.json in Webpack, then reads it in a Node.js edge-startup script to build the exact Content-Security-Policy string.
Step 1 — emit the manifest in Webpack
Permalink to "Step 1 — emit the manifest in Webpack"// webpack.config.js
const { SubresourceIntegrityPlugin } = require('webpack-subresource-integrity');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const SriManifestPlugin = require('./scripts/sri-manifest-plugin'); // local plugin below
module.exports = {
output: {
crossOriginLoading: 'anonymous',
filename: '[name].[contenthash].js',
},
plugins: [
new HtmlWebpackPlugin(),
new SubresourceIntegrityPlugin({ hashFuncNames: ['sha384'] }),
new SriManifestPlugin({ cdnOrigin: 'https://cdn.example.com' }),
],
};
SriManifestPlugin is a small custom plugin that iterates compilation.assets after webpack-subresource-integrity has annotated them:
// scripts/sri-manifest-plugin.js
class SriManifestPlugin {
constructor({ cdnOrigin }) {
this.cdnOrigin = cdnOrigin;
}
apply(compiler) {
compiler.hooks.emit.tapAsync('SriManifestPlugin', (compilation, cb) => {
const entries = [];
for (const [filename, asset] of Object.entries(compilation.assets)) {
const integrity = asset.integrity; // injected by webpack-subresource-integrity
if (integrity && /\.(js|css)$/.test(filename)) {
entries.push({
origin: this.cdnOrigin,
path: `/${filename}`,
integrity,
});
}
}
const json = JSON.stringify({ generated: new Date().toISOString(), entries }, null, 2);
compilation.assets['sri-manifest.json'] = {
source: () => json,
size: () => json.length,
};
cb();
});
}
}
module.exports = SriManifestPlugin;
The resulting sri-manifest.json looks like:
{
"generated": "2026-06-23T08:00:00Z",
"entries": [
{
"origin": "https://cdn.example.com",
"path": "/main.a1b2c3d4.js",
"integrity": "sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
},
{
"origin": "https://cdn.example.com",
"path": "/vendor.e5f6a7b8.js",
"integrity": "sha384-H8BRh8j48O9oYatfu5AZzq6A9RINhZO5H16dQZngK7T62em8MUt1FLm52t+eX4v0"
}
]
}
Step 2 — consume the manifest to build the CSP header
Permalink to "Step 2 — consume the manifest to build the CSP header"// scripts/build-csp-header.js (runs in CI or at edge startup)
const manifest = require('../dist/sri-manifest.json');
function buildScriptSrc(manifest) {
const origins = new Set();
const hashes = [];
for (const entry of manifest.entries) {
origins.add(entry.origin);
hashes.push(`'${entry.integrity}'`);
}
const originList = [...origins].join(' ');
const hashList = hashes.join(' ');
return `script-src 'self' ${originList} ${hashList}`;
}
const scriptSrc = buildScriptSrc(manifest);
const cspHeader = `${scriptSrc}; object-src 'none'; base-uri 'self'`;
console.log(cspHeader);
// script-src 'self' https://cdn.example.com 'sha384-oqVu...' 'sha384-H8BR...'; object-src 'none'; base-uri 'self'
Step 3 — inject the header at the edge
Permalink to "Step 3 — inject the header at the edge"For Nginx, write the computed string to a variable file that the configuration includes at runtime:
# /etc/nginx/conf.d/sri.conf — generated by build-csp-header.js in CI
geo $csp_script_src {
default "script-src 'self' https://cdn.example.com 'sha384-oqVu...' 'sha384-H8BR...'";
}
# /etc/nginx/sites-enabled/app.conf
server {
listen 443 ssl http2;
server_name app.example.com;
include conf.d/sri.conf;
location / {
add_header Content-Security-Policy "${csp_script_src}; object-src 'none'; base-uri 'self'" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Cross-Origin-Resource-Policy "cross-origin" always;
try_files $uri $uri/ =404;
}
}
For a Cloudflare Worker, fetch the manifest from R2 or a KV namespace at startup and build the header string dynamically — this is described in CDN Trust Mapping & Routing.
Variant configurations
Permalink to "Variant configurations"Multiple CDN origins with separate trust tiers
Permalink to "Multiple CDN origins with separate trust tiers"When assets come from a primary CDN and a secondary failover CDN, list both origins but assign hashes only once — the hash covers the payload, not the origin:
Content-Security-Policy:
script-src 'self'
https://primary.cdn.example.com
https://fallback.cdn.example.com
'sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC'
'sha384-H8BRh8j48O9oYatfu5AZzq6A9RINhZO5H16dQZngK7T62em8MUt1FLm52t+eX4v0';
style-src 'self'
https://primary.cdn.example.com
'sha384-abc123...';
object-src 'none';
base-uri 'self'
Vite project with vite-plugin-subresource-integrity
Permalink to "Vite project with vite-plugin-subresource-integrity" Vite does not expose a plugin hook as early as Webpack’s emit, so the manifest is built by a post-build script that reads the generated HTML:
// vite.config.js
import { defineConfig } from 'vite';
import sri from 'vite-plugin-subresource-integrity';
export default defineConfig({
build: { rollupOptions: { output: { entryFileNames: '[name].[hash].js' } } },
plugins: [sri({ algorithms: ['sha384'] })],
});
# post-build: extract hashes from dist/index.html into sri-manifest.json
node scripts/extract-sri-from-html.js dist/index.html \
--origin https://cdn.example.com \
--out dist/sri-manifest.json
Third-party script pinning (no build tool)
Permalink to "Third-party script pinning (no build tool)"For a third-party library served from a public CDN, compute the hash manually and hard-code it. Re-verify the hash whenever the vendor ships a new version:
<script
src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"
integrity="sha384-dHdlP36K5RFVVJ3Q8HBqMreYqNR7fVSChzTh0dSNuMEUhYaUCBV2dA2Hm1KhDi/"
crossorigin="anonymous"
defer></script>
Use How to Calculate SHA-256 vs SHA-384 for SRI to generate the hash value from the file locally before embedding it.
Gotchas and edge cases
Permalink to "Gotchas and edge cases"-
Missing
crossorigin="anonymous"breaks hash comparison. When a<script>tag on a cross-origin resource omits this attribute, the browser performs a no-CORS fetch and receives an opaque response. The browser cannot read the response body to compute a hash, so the integrity check fails unconditionally. Every<script>or<link>that references a cross-origin resource and carries anintegrityattribute must also carrycrossorigin="anonymous". -
CDN-side byte-altering transforms invalidate hashes. Standard
Content-Encodingcompression (gzip, Brotli) is safe — the browser computes the hash over the decoded body. But CDN-level minification, whitespace removal, byte injection (ad networks, analytics snippets), or character-encoding normalization alter the decoded bytes and will cause every integrity check to fail. Disable all such transforms for SRI-protected paths. -
CSP must name the origin, not just the hash. Listing only
'sha384-...'inscript-srccauses Chrome to allow the script if the hash matches regardless of origin, but only when there is no explicit origin in the directive. Firefox and Safari behave differently. The safe and spec-compliant approach is always to list both the origin URL and the hash together. -
Hash drift on hotfix deployments. Deploying a patched asset file without updating the
integrityattribute in the HTML and thescript-srcCSP header simultaneously causes an immediate integrity mismatch and a broken site. Use atomic deployments (deploy HTML and assets in a single pipeline step) and content-hashed filenames so the manifest stays in sync with the served files. -
Cache-Controlon HTML entry points must not cache stale hashes. If an HTML page is cached at a CDN edge for hours but assets have been redeployed with new hashes, returning browsers will load the old HTML with staleintegrityvalues that no longer match the new assets. SetCache-Control: no-cacheormax-age=0, must-revalidateon all HTML entry points.
Verification steps
Permalink to "Verification steps"DevTools console — what a correct deployment looks like
Permalink to "DevTools console — what a correct deployment looks like"Open the browser console after a page load. If SRI is working correctly, there are no integrity-related messages. A failure looks like:
Failed to find a valid digest in the 'integrity' attribute for resource
'https://cdn.example.com/main.a1b2c3d4.js' with computed SHA-384 integrity
'sha384-DIFFERENT_HASH_HERE'. The resource has been blocked.
If you see this in production, the asset bytes the CDN served do not match the hash compiled into the HTML. Check for CDN-side transforms, cache staleness, or a deployment ordering issue.
CLI — verify the manifest before deployment
Permalink to "CLI — verify the manifest before deployment"Run this check in CI before the edge-inject step to catch drift early:
#!/usr/bin/env bash
# verify-sri-manifest.sh
MANIFEST="dist/sri-manifest.json"
FAIL=0
while IFS= read -r entry; do
path=$(echo "$entry" | jq -r '.path')
expected=$(echo "$entry" | jq -r '.integrity')
file="dist${path}"
if [ ! -f "$file" ]; then
echo "MISSING: $file"
FAIL=1
continue
fi
actual=$(openssl dgst -sha384 -binary "$file" | openssl base64 -A)
actual_sri="sha384-${actual}"
if [ "$actual_sri" != "$expected" ]; then
echo "MISMATCH: $file"
echo " expected: $expected"
echo " actual: $actual_sri"
FAIL=1
else
echo "OK: $file"
fi
done < <(jq -c '.entries[]' "$MANIFEST")
exit $FAIL
Expected output on a clean build:
OK: dist/main.a1b2c3d4.js
OK: dist/vendor.e5f6a7b8.js
Network tab — confirm CORS headers are present
Permalink to "Network tab — confirm CORS headers are present"In DevTools → Network, click the asset request and check the response headers. The CDN must return:
Access-Control-Allow-Origin: *
If this header is absent, the browser’s CORS check blocks the integrity verification step. For Configuring Content-Security-Policy with SRI, CORS is equally critical — both CSP enforcement and SRI depend on a valid CORS response for cross-origin resources.
Related
Permalink to "Related"- CDN Trust Mapping & Routing — parent page covering multi-CDN trust tiers, routing policies, and fallback strategies
- Automating Hash Generation in Webpack 5 — full Webpack build configuration for deterministic SHA-384 output
- Configuring Content-Security-Policy with SRI — combining CSP directives with SRI hashes for layered enforcement