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.


CDN origin to SRI policy mapping flow Diagram showing four stages: CI build produces hashes; a manifest maps origin URLs to hashes; edge injects CSP headers; browser validates integrity attribute against downloaded bytes. CI Build sha384 per asset Origin→Hash manifest.json Edge / Nginx injects CSP header Browser SRI check webpack / Vite keyed by origin URL script-src + sha384- pass / block

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 an integrity attribute must also carry crossorigin="anonymous".

  • CDN-side byte-altering transforms invalidate hashes. Standard Content-Encoding compression (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-...' in script-src causes 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 integrity attribute in the HTML and the script-src CSP 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-Control on 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 stale integrity values that no longer match the new assets. Set Cache-Control: no-cache or max-age=0, must-revalidate on 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.


Permalink to "Related"

Related Articles

Configuring SRI for jsDelivr and unpkg
CDN Trust Mapping & Routing Asset Hashing & Dynamic Script…