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.
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:
- The browser initiates a fetch for the resource URL.
- 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). - Upon receiving the response body, the browser computes the digest using the algorithm(s) named in
integrity. - If the computed digest matches any of the listed hash expressions, execution or rendering proceeds.
- On mismatch: the resource is blocked, an
errorevent fires on the element, and a CSP violation report is sent ifreport-uriorreport-tois 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-srcto the exact CDN hostnames that serve your hashed assets. - Add
require-sri-for script styleif your CSP supports it (Chrome/Edge), which makes SRI mandatory for all covered resource types. - Wire
report-uriorreport-toso 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"- Immediately pin the
script-srcCSP directive to block the compromised CDN origin. - Deploy the last known-good artifact from your build cache, with its original hash, to a controlled origin.
- Update the HTML template or manifest to point to the controlled origin.
- Verify the integrity hash in the new HTML matches the known-good artifact before the deployment goes live.
- 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.
Related
Permalink to "Related"- Static Asset Hash Generation — build-tool integration for Webpack 5, Vite, and Rollup; deterministic hash pipelines
- Dynamic Script Loading Patterns — secure async loaders, ES module preload strategies, and runtime integrity enforcement
- CDN Trust Mapping & Routing — mapping CDN origins to SRI policies; immutable delivery path configuration
- Browser Enforcement & Security Boundaries — the full fetch lifecycle, CORS mechanics, and CSP interaction at the browser layer
- Supply Chain Auditing & Dependency Verification — SBOM generation, lockfile analysis, and provenance verification for the full dependency graph