Asset Hashing & Dynamic Script Injection

Permalink to "Asset Hashing & Dynamic Script Injection"

Asset hashing and dynamic script injection together form the operational backbone of frontend supply chain security: hashing ensures every delivered byte matches what you built and audited, while controlled injection patterns extend that guarantee to scripts loaded at runtime. This guide covers the full stack — from SHA-384 fingerprinting and build-tool integration to CDN trust policies, browser enforcement mechanics, and compliance controls — for frontend engineers, DevOps teams, and security architects who need deterministic verification at every delivery hop.


SRI asset hashing and dynamic injection flow Data-flow diagram showing how a build artifact moves from the CI/CD build step through hash generation, deployment to a CDN, and finally browser-side verification via the integrity attribute. CI / CD Build + emit artifacts Hash generation SHA-384 digest → manifest CDN edge Immutable cache CORS headers Browser Fetch → recompute hash → compare ✓ execute / ✗ block CSP violation report on mismatch SRI Delivery & Verification Flow Build artifact → SHA-384 manifest → CDN edge → browser integrity check

Threat Model: What Attack Vectors This Defends Against

Permalink to "Threat Model: What Attack Vectors This Defends Against"

Supply chain attacks against frontend assets operate across three distinct surfaces. Understanding each is essential before choosing which controls to apply.

CDN payload substitution. An attacker who gains write access to a CDN bucket or edge configuration can replace a legitimate JavaScript bundle with a weaponized version. Without SRI, every visitor’s browser silently executes the malicious payload. With SRI, the hash mismatch is detected at fetch time and the script is blocked before a single line runs.

Dependency registry poisoning. Package registries can be compromised, typosquatted, or subjected to dependency confusion attacks. A malicious version of a transitive dependency that looks identical to the legitimate one will differ in at least one byte — enough for a hash mismatch to surface. Combining SRI with Automated SBOM Generation gives you a full artifact inventory against which you can verify digests.

Dynamic injection hijacking. Single-page applications frequently load scripts at runtime based on route, feature flags, or user context. If the injection logic constructs src URLs from untrusted input and omits the integrity attribute, an attacker who controls even a fraction of the URL surface can redirect the fetch to an arbitrary payload. Dynamic Script Loading Patterns demonstrates how to enforce integrity on every programmatic element creation.

Man-in-the-middle on open networks. On HTTP connections (or HTTPS with a compromised certificate), in-transit modification is trivial. SRI is transport-independent — the hash is verified regardless of how the bytes arrived — but it is not a substitute for HTTPS; it is a last line of defence when transport security fails.

What SRI does not protect against: attacks that modify the origin server itself before the hash is generated, or that replace both the asset and the integrity attribute in the same compromise. SRI is an integrity check, not an authentication mechanism.


Core Specification: Attributes, Syntax, and Algorithm Choices

Permalink to "Core Specification: Attributes, Syntax, and Algorithm Choices"

The W3C Subresource Integrity specification defines the integrity attribute for <script> and <link rel="stylesheet"> elements. The attribute value is one or more space-separated hash expressions, each in the form <algorithm>-<base64-encoded-digest>.

<!-- Correct: integrity attribute + crossorigin on a cross-origin script -->
<script
  src="https://cdn.example.com/app.c8a2f1e3.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/ux8ByDjKoV3gHRl6jLl1O2q3M4P5Q6"
  crossorigin="anonymous"
></script>

<!-- Correct: SRI on a stylesheet -->
<link
  rel="stylesheet"
  href="https://cdn.example.com/styles.c8a2f1e3.css"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/ux8ByDjKoV3gHRl6jLl1O2q3M4P5Q6"
  crossorigin="anonymous"
/>

<!-- Multiple hash algorithms (browser uses the strongest it supports) -->
<script
  src="https://cdn.example.com/vendor.js"
  integrity="sha384-<hash1> sha512-<hash2>"
  crossorigin="anonymous"
></script>

Algorithm selection rationale:

Algorithm Digest length Browser support Recommended use
sha256 256 bits / 44 chars base64 All SRI-capable browsers Low-risk, size-constrained manifests
sha384 384 bits / 64 chars base64 All SRI-capable browsers Default — best balance of security margin and size
sha512 512 bits / 88 chars base64 All SRI-capable browsers Highest-security environments, large manifests acceptable

SHA-384 is the industry norm. It is the algorithm used in every official CDN provider’s integrity snippet (jsDelivr, cdnjs, unpkg) and is required by How to Calculate SHA-256 vs SHA-384 for SRI.

The base64 string is computed over the raw bytes of the file, not any decoded or normalized form. Compression, minification, and transpilation all change bytes — always hash the final artifact, not the source.


Browser Enforcement Mechanics: Fetch Lifecycle and Failure Modes

Permalink to "Browser Enforcement Mechanics: Fetch Lifecycle and Failure Modes"

When a browser encounters a <script> or <link> element with an integrity attribute, it executes a fetch-and-verify flow that Browser Enforcement & Security Boundaries documents in detail. The high-level steps:

  1. The browser initiates a fetch for the resource URL.
  2. If the URL is cross-origin and crossorigin="anonymous" is set, the fetch is a CORS request. Without this attribute, the browser receives an opaque response and cannot read the body for hashing — resulting in a block (some browsers skip verification silently, others block outright; neither is safe).
  3. Upon receiving the response body, the browser computes the digest using the algorithm(s) named in integrity.
  4. If the computed digest matches any of the listed hash expressions, execution or rendering proceeds.
  5. On mismatch: the resource is blocked, an error event fires on the element, and a CSP violation report is sent if report-uri or report-to is configured.

Opaque response edge case: Same-origin resources with an integrity attribute do not require crossorigin because the browser has full access to the response body via the same-origin policy. The requirement applies exclusively to cross-origin URLs.

Multiple hash algorithms: When multiple hash expressions are listed in a single integrity attribute, the browser selects the strongest algorithm it supports and ignores the others. This means you can add a sha512 hash alongside sha384 without breaking older browsers that support only the latter.

Service worker interception: A service worker that intercepts a request and serves a cached response must ensure the cached bytes still match the integrity hash in the HTML. If the service worker applies any transformation — compression, injection of analytics, content rewriting — the hash will mismatch and the resource will be blocked. Design service worker cache strategies to store and serve assets unmodified.


Production Implementation Patterns

Permalink to "Production Implementation Patterns"

Webpack 5 with webpack-subresource-integrity

Permalink to "Webpack 5 with webpack-subresource-integrity"

The webpack-subresource-integrity plugin computes SHA-384 hashes for every emitted chunk and writes them into the HTML output via html-webpack-plugin. This is the zero-overhead path for Automating Hash Generation in Webpack 5.

// webpack.config.js
const { SubresourceIntegrityPlugin } = require('webpack-subresource-integrity');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  output: {
    filename: '[name].[contenthash:8].js',
    chunkFilename: '[name].[contenthash:8].chunk.js',
    crossOriginLoading: 'anonymous',
  },
  plugins: [
    new SubresourceIntegrityPlugin({
      hashFuncNames: ['sha384'],
    }),
    new HtmlWebpackPlugin({ template: './src/index.html' }),
  ],
};

contenthash in the filename ensures cache-busting and makes the filename itself a weak fingerprint. crossOriginLoading: 'anonymous' injects crossorigin="anonymous" on every dynamically loaded chunk — without it, code-split chunks loaded at runtime omit the attribute and will fail the browser’s integrity check.

Vite

Permalink to "Vite"

Vite 4+ computes SRI hashes natively when build.rollupOptions.plugins includes vite-plugin-sri or equivalent, or via the @vitejs/plugin-legacy integration. For manual generation:

// vite.config.js
import { defineConfig } from 'vite';
import { sri } from 'vite-plugin-sri3';

export default defineConfig({
  plugins: [sri()],
  build: {
    rollupOptions: {
      output: {
        entryFileNames: '[name].[hash].js',
        chunkFileNames: '[name].[hash].js',
      },
    },
  },
});

Nginx: injecting integrity headers at the edge

Permalink to "Nginx: injecting integrity headers at the edge"

For server-side rendered HTML not managed by a bundler, Nginx can inject the Content-Security-Policy header that constraints script execution, but the integrity attribute itself must appear in the HTML — it cannot be injected via a response header. The recommended pattern is a build-time manifest lookup:

# nginx.conf — security headers for SRI-protected assets
server {
  add_header Content-Security-Policy
    "script-src 'self' https://cdn.example.com; style-src 'self' https://cdn.example.com"
    always;
  add_header Cache-Control "public, max-age=31536000, immutable" always;
  add_header X-Content-Type-Options "nosniff" always;
}

The immutable directive on Cache-Control is critical: it tells browsers and intermediary caches never to revalidate this URL while the max-age is valid. Combined with content-addressed filenames (app.c8a2f1e3.js), this prevents stale-content scenarios where an outdated CDN copy mismatches a freshly deployed hash.

CI/CD gating: reject mismatched hashes before deploy

Permalink to "CI/CD gating: reject mismatched hashes before deploy"

Hash verification should fail the build, not just log a warning. The following shell fragment, run after the build step and before deployment, recomputes hashes from disk and diffs against the manifest:

#!/usr/bin/env bash
# ci-sri-verify.sh — abort if any chunk hash diverges from the asset manifest
set -euo pipefail

MANIFEST="dist/asset-manifest.json"
FAILED=0

while IFS= read -r file; do
  expected=$(jq -r --arg f "$file" '.[$f].integrity' "$MANIFEST")
  if [[ "$expected" == "null" ]]; then continue; fi

  algorithm="${expected%%-*}"     # e.g. "sha384"
  actual=$(openssl dgst -"${algorithm/sha/sha-}" -binary "$file" | \
           openssl base64 -A)
  actual_sri="${algorithm}-${actual}"

  if [[ "$actual_sri" != "$expected" ]]; then
    echo "FAIL: $file  expected=$expected  got=$actual_sri"
    FAILED=1
  fi
done < <(jq -r 'keys[]' "$MANIFEST")

[[ $FAILED -eq 0 ]] || { echo "SRI integrity gate failed — aborting deploy"; exit 1; }
echo "SRI integrity gate passed"

Cross-Cutting Architecture Considerations

Permalink to "Cross-Cutting Architecture Considerations"

CORS requirements

Permalink to "CORS requirements"

SRI on a cross-origin resource is only viable if the server delivering the asset sends a CORS Access-Control-Allow-Origin response header. Without it, the browser cannot read the response body to compute the hash. When evaluating CDN providers for CDN Trust Mapping & Routing, confirm that CORS headers are enabled for every origin you intend to verify.

CSP interaction

Permalink to "CSP interaction"

Content-Security-Policy and SRI operate at different layers. CSP script-src controls which origins are allowed to serve scripts; SRI verifies the payload from those origins. For the strongest posture, apply both:

  • Restrict script-src to the exact CDN hostnames that serve your hashed assets.
  • Add require-sri-for script style if your CSP supports it (Chrome/Edge), which makes SRI mandatory for all covered resource types.
  • Wire report-uri or report-to so every mismatch produces a machine-parseable violation report.

For a complete reference on combining these directives, see Configuring Content-Security-Policy with SRI.

COOP/COEP isolation

Permalink to "COOP/COEP isolation"

Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp (the prerequisites for enabling SharedArrayBuffer) require every cross-origin resource to opt in via Cross-Origin-Resource-Policy. SRI does not satisfy this requirement — the two mechanisms are complementary, not substitutes.

Dynamic vs static assets

Permalink to "Dynamic vs static assets"

Static assets — bundles emitted by your build tool with a deterministic hash in the filename — are the simplest case: the integrity hash is computed once and baked into the HTML template. Dynamic assets (A/B test variants, feature-flagged modules, analytics scripts loaded by a tag manager) require the hash to be pre-computed and stored in a lookup table that the injection logic queries at runtime. Never skip the integrity attribute for dynamic assets because the filename is computed at runtime; that is precisely when the risk is highest.


Dynamic Script Injection and Runtime Security

Permalink to "Dynamic Script Injection and Runtime Security"

Programmatic DOM insertion with document.createElement('script') is the primary vector for bypassing static SRI checks. Secure injection requires passing a pre-computed integrity value for every URL the loader might request:

// secure-loader.js — dynamic script injection with SRI
const INTEGRITY_MAP = {
  'https://cdn.example.com/analytics.v2.js':
    'sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/ux8ByDjKoV3gHRl6jLl1O2q3M4P5Q6',
  'https://cdn.example.com/chat-widget.v1.js':
    'sha384-pqWuBfXRKap8gdgcDY6vylN7+R0GqP9K/uz9CzEkLV4iIsl2P3r4N5O6Q7R8S9T0',
};

function loadScript(url) {
  const hash = INTEGRITY_MAP[url];
  if (!hash) {
    throw new Error(`No integrity hash registered for ${url}. Refusing to load.`);
  }
  return new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = url;
    script.integrity = hash;
    script.crossOrigin = 'anonymous';
    script.onload = resolve;
    script.onerror = () =>
      reject(new Error(`SRI check failed or network error loading ${url}`));
    document.head.appendChild(script);
  });
}

The integrity map is the allowlist. Any URL not present in it is rejected before the request is made. Coupling this with a strict CSP script-src directive that limits origins to cdn.example.com creates defense-in-depth: CSP blocks unknown origins, SRI blocks known origins serving unexpected content.

For handling failures gracefully, Handling SRI Failures with onerror Handlers covers circuit-breaker patterns and local fallback bundles.


Compliance Mapping

Permalink to "Compliance Mapping"
Control Requirement How SRI satisfies it
PCI DSS v4.0.1 — Req 6.4.3 All payment page scripts must be inventoried, authorized, and their integrity validated SRI hashes in HTML constitute per-script authorization; the asset manifest is the inventory
PCI DSS v4.0.1 — Req 11.6.1 Detect unauthorized modification of HTTP headers and payment page contents CSP violation reports triggered on SRI mismatch provide the detection signal
SOC 2 Type II — CC6.1 Logical access controls restrict software changes CI/CD hash gate prevents deployment of artifacts whose hash diverges from the reviewed build
SOC 2 Type II — CC7.2 Monitor for security events and anomalies SRI violation reports in a SIEM constitute the anomaly-detection evidence
ISO 27001 — A.12.5.1 Restrict installation of software on operational systems Hash pinning in the asset manifest ensures only reviewed versions of dependencies can execute
ISO 27001 — A.14.2.5 Establish secure system engineering principles SRI is a structural integrity control at the delivery layer, satisfying the “verify before execute” principle
NIST SSDF — PW.4.1 Review and test code to identify vulnerabilities SRI hash gates in CI are a mandatory verification step in the SSDF build-and-release practice

Evidence collection for auditors: export the asset manifest (filename → hash), the CI pipeline logs showing hash gate execution, and a 90-day sample of CSP violation reports. These three artefacts together satisfy most PCI DSS 6.4.3 assessor requests without manual attestation.


Operational Runbook

Permalink to "Operational Runbook"

Hash rotation SLA

Permalink to "Hash rotation SLA"

Rotate integrity hashes whenever any of the following occur:

  • A dependency receives a version bump (patch, minor, or major).
  • A build-tool configuration change alters transpilation, minification, or bundling output.
  • A security advisory is published for any included package.
  • The CDN provider announces a key compromise or a routing policy change.

The rotation SLA for security-advisory-triggered updates should be ≤4 hours from advisory publication to deployed hash. For routine dependency updates, align with your existing deployment cadence.

Mismatch telemetry

Permalink to "Mismatch telemetry"

Wire every CSP report-to endpoint to a structured log pipeline. Minimum fields per violation report: document-uri, blocked-uri, effective-directive, status-code, timestamp. Aggregate on blocked-uri — a spike in mismatch reports against a single asset URL is the primary indicator of an in-progress supply chain attack or a botched CDN cache invalidation.

Rollback procedures

Permalink to "Rollback procedures"
  1. Immediately pin the script-src CSP directive to block the compromised CDN origin.
  2. Deploy the last known-good artifact from your build cache, with its original hash, to a controlled origin.
  3. Update the HTML template or manifest to point to the controlled origin.
  4. Verify the integrity hash in the new HTML matches the known-good artifact before the deployment goes live.
  5. Perform a Provenance Verification Workflows check on the restored artifact to confirm the Sigstore or internal provenance attestation is intact.

Never re-enable a CDN origin after a suspected compromise without a full content audit — even a single stale object with a modified payload can re-introduce the attack.


Common Pitfalls and Mitigations

Permalink to "Common Pitfalls and Mitigations"
Issue Root cause Fix
SRI check always fails for dynamically loaded chunks crossOriginLoading: 'anonymous' not set in webpack output config Add crossOriginLoading: 'anonymous' to module.exports.output in webpack.config.js
Hash mismatch after CDN cache invalidation CDN edge still serving stale bytes for the old filename Use content-addressed filenames ([contenthash]) so each new build gets a unique URL; invalidation is then irrelevant
crossorigin attribute missing on same-domain CDN Developer assumes same-domain means same-origin; CDN is on a subdomain Subdomains are cross-origins; crossorigin="anonymous" is required whenever the URL differs in host, port, or scheme
Service worker transforming cached responses SW middleware injects analytics or rewrites asset URLs Store and serve assets byte-for-byte unmodified in the service worker cache; apply any metadata via separate fetch paths
Build-time hash differs from deploy-time hash Non-deterministic build (timestamps, random seeds in minifier) Enable deterministic builds: set NODE_ENV=production, fix minifier seeds, verify reproducibility with two consecutive builds
integrity attribute omitted on third-party analytics Analytics tag added via tag manager outside the build pipeline Audit all tag-manager payloads; require SRI or CSP hash allowlisting for every injected script
SRI silently skipped on opaque responses Missing CORS header on CDN origin Confirm Access-Control-Allow-Origin: * or the specific requesting origin is present on every hashed asset response

FAQ

Permalink to "FAQ"
Why does SRI require crossorigin="anonymous" on cross-origin resources?

Without crossorigin="anonymous", the browser issues a no-CORS request and receives an opaque response whose body is hidden from the page’s JavaScript context. Because the browser cannot read the response bytes, it cannot compute the digest and silently skips or blocks the integrity check. The attribute forces a CORS request, which exposes the full response body for hash comparison — at the cost of the server needing to send a valid Access-Control-Allow-Origin header.

Can SRI protect dynamically injected scripts created with document.createElement?

Yes. Setting the integrity property on a dynamically created script element before appending it to the DOM triggers the same browser fetch-and-verify flow as a static HTML <script> tag. The hash must be computed at build time and passed to the runtime loader — it cannot be computed on-the-fly from an already-loaded script.

What is the difference between SHA-256, SHA-384, and SHA-512 for SRI?

All three algorithms are collision-resistant and browser-supported. SHA-384 is the industry default because it offers a larger security margin than SHA-256 with a shorter hash string than SHA-512. SHA-512 provides marginal extra security at the cost of a ~33% longer base64 string in HTML. SHA-256 is acceptable for low-risk assets. Never use MD5 or SHA-1 — browsers reject them outright for SRI.

Does SRI protect against a compromised CDN origin?

SRI protects against payload substitution: if a CDN serves a modified file, the hash mismatch blocks execution. It does not protect against the CDN serving the correct file initially and then replacing it between your hash generation and the user’s fetch. Combining SRI with immutable Cache-Control headers and pinned CDN origin policies narrows this window to near zero.

How should I handle SRI hash rotation when a dependency updates?

Regenerate hashes from the new artifact immediately after the dependency update passes your test suite. Update the integrity attribute in HTML or the asset manifest, and invalidate CDN caches for affected URLs. Never reuse old hashes for new content — even a single changed byte will mismatch. Automate this in CI so the rotation SLA matches your deployment cycle, not a manual process.

Can CSP nonces and SRI hashes be combined?

Yes, and they complement each other. SRI hashes verify that a fetched resource matches an expected digest. CSP nonces authorize inline scripts or scripts whose source was unknown at build time. For dynamic injection where you control both the source URL and the content, use SRI hashes. For server-rendered inline scripts, use nonces. For maximum coverage, apply both. See Configuring Content-Security-Policy with SRI for directive syntax.

What happens at runtime when an SRI check fails?

The browser blocks the resource, fires an error event on the script or link element, and — if report-uri or report-to is configured in the CSP — sends a structured violation report. The script is never executed. The page must handle the failure through an onerror handler, typically degrading to a local fallback bundle. See Handling SRI Failures with onerror Handlers for a production-ready fallback pattern.


Permalink to "Related"

In This Section