Automating Hash Generation in Webpack 5
Permalink to "Automating Hash Generation in Webpack 5"This page is part of Static Asset Hash Generation, which sits inside the broader Asset Hashing & Dynamic Script Injection topic area. It covers exactly one workflow: configuring Webpack 5 to compute and inject SHA-384 integrity hashes automatically during the build, so every emitted script and stylesheet carries a verifiable fingerprint before it reaches a CDN or browser.
Quick reference
Permalink to "Quick reference"| Item | Value |
|---|---|
| Plugin | webpack-subresource-integrity v5+ |
| Webpack version | 5.x (uses processAssets hook) |
| Default algorithm | sha384 |
| Required output option | crossOriginLoading: 'anonymous' |
| HTML integration | html-webpack-plugin v5+ |
| Browser support | All evergreen browsers (Chrome 45+, Firefox 43+, Safari 11.1+, Edge 17+) |
Why automate this in the bundler?
Permalink to "Why automate this in the bundler?"SRI hashes must be computed against the final, transformed bytes of each asset — after minification, tree-shaking, and CSS extraction have all run. Computing digests earlier in the pipeline means the hash covers pre-transformation bytes, and the browser’s digest of the delivered file will never match. Doing it manually after the build is error-prone and breaks every time a dependency version changes.
webpack-subresource-integrity hooks into Webpack’s processAssets stage at the PROCESS_ASSETS_STAGE_OPTIMIZE_HASH point, meaning it always sees the finalized byte buffers. It then patches the chunk loading runtime so that dynamically imported modules are also integrity-checked at load time, and it passes hash metadata to HtmlWebpackPlugin so the emitted HTML carries correct integrity and crossorigin attributes without any manual templating.
Canonical implementation
Permalink to "Canonical implementation"Install the two required packages, then use the configuration below as a complete starting point. The comments explain why each option is present.
npm install --save-dev webpack-subresource-integrity html-webpack-plugin
// webpack.config.js
const { SubresourceIntegrityPlugin } = require('webpack-subresource-integrity');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].chunk.js',
// Required: emits crossorigin="anonymous" on runtime chunk loaders.
// Without this, browsers cannot perform integrity checks on lazy chunks.
crossOriginLoading: 'anonymous',
clean: true,
},
plugins: [
// HtmlWebpackPlugin must be listed BEFORE SubresourceIntegrityPlugin
// so the plugin can intercept and annotate the tag objects it produces.
new HtmlWebpackPlugin({
template: './src/index.html',
}),
new SubresourceIntegrityPlugin({
hashFuncNames: ['sha384'],
}),
],
};
After npm run build, inspect dist/index.html. Every <script> and <link rel="stylesheet"> tag will carry both attributes:
<script
src="/dist/main.a1b2c3d4e5f6.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"
></script>
Variant examples
Permalink to "Variant examples"Dual-algorithm integrity attribute
Permalink to "Dual-algorithm integrity attribute"Pass two algorithm names to produce a space-separated dual-hash attribute. Browsers pick the strongest algorithm they recognise, so this pattern retains forward compatibility while ensuring older clients fall back to SHA-256.
new SubresourceIntegrityPlugin({
hashFuncNames: ['sha384', 'sha256'],
})
Emitted output:
<script
src="/dist/vendor.f9e2c1.js"
integrity="sha384-<hash> sha256-<hash>"
crossorigin="anonymous"
></script>
Vite alternative (for comparison)
Permalink to "Vite alternative (for comparison)"If your project uses Vite instead of Webpack, the equivalent is the vite-plugin-subresource-integrity community plugin. The configuration shape is similar:
// vite.config.js
import { VitePluginSubresourceIntegrity } from 'vite-plugin-subresource-integrity';
export default {
plugins: [
VitePluginSubresourceIntegrity({
algorithm: 'sha384',
}),
],
};
Manifest-driven injection for server-side rendering
Permalink to "Manifest-driven injection for server-side rendering"When you render HTML server-side (Express, Next.js custom server, etc.) rather than through HtmlWebpackPlugin, you need the hash manifest directly. webpack-subresource-integrity writes the hash metadata into Webpack’s asset manifest when used alongside webpack-manifest-plugin:
const { WebpackManifestPlugin } = require('webpack-manifest-plugin');
// In plugins array:
new WebpackManifestPlugin({ fileName: 'sri-manifest.json' }),
new SubresourceIntegrityPlugin({ hashFuncNames: ['sha384'] }),
Your server reads sri-manifest.json at startup and injects the correct integrity value alongside each asset reference. This is the approach Implementing Dynamic Script Loaders with Integrity uses when hashes must be embedded at request time.
Gotchas and edge cases
Permalink to "Gotchas and edge cases"crossOriginLoadingomission is the most common failure. WithoutcrossOriginLoading: 'anonymous', dynamically loaded chunks do not carrycrossorigin="anonymous", so the browser issues a no-CORS fetch and then refuses to hash-verify the result. The console showsERR_FAILEDon the chunk, not an SRI error, which makes this hard to diagnose.- Plugin ordering matters for HTML injection.
HtmlWebpackPluginmust appear beforeSubresourceIntegrityPluginin the plugins array. The SRI plugin intercepts tag objects produced by HtmlWebpackPlugin; if it runs first there are no tag objects to annotate. - Post-build asset transformation invalidates hashes. Any byte-level change after
webpack-subresource-integrityruns — including CDN-side script injection, re-minification, or even some HTML prettifiers touching the whitespace inside the script body — will cause a hash mismatch. CDN-levelContent-Encoding(gzip, Brotli) is safe because browsers hash the decoded body. - Source maps are not hashed. Source map files are fetched by DevTools, not by the browser’s secure loader, so they do not receive
integrityattributes and their content does not affect production hashes. contenthashvschunkhashvshash. Use[contenthash]— it changes only when the file’s content changes.[hash]changes on every build (too aggressive for caching);[chunkhash]includes related modules and can produce different values for the same file across environments.- Immutable CDN cache headers must match filename rotation. Because
[contenthash]filenames change when content changes, serve assets withCache-Control: public, max-age=31536000, immutable. The old filename (and old hash) disappear from the CDN naturally; no explicit cache purge is needed for versioned assets.
Verification steps
Permalink to "Verification steps"1. Inspect the built HTML
Permalink to "1. Inspect the built HTML"grep -E 'integrity|crossorigin' dist/index.html
Expected output — both attributes present on every script and stylesheet line:
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+..." crossorigin="anonymous"
2. Verify a hash manually
Permalink to "2. Verify a hash manually"Use OpenSSL to recompute a digest against the built file and compare it to what the HTML declares:
# Compute the sha384 digest of the built bundle
openssl dgst -sha384 -binary dist/main.a1b2c3d4.js \
| openssl base64 -A
# Output should match the base64 portion after "sha384-" in the integrity attribute
3. Browser DevTools check
Permalink to "3. Browser DevTools check"Open the Network panel, reload the page, and filter by JS. Select any script request. In the Response Headers pane there is no integrity header — the attribute lives in the HTML. To confirm the browser enforced SRI, look in the Console: if any integrity check failed you will see:
- Chrome/Edge:
Failed to find a valid digest in the 'integrity' attribute for resource '…' - Firefox:
None of the "sha384" hashes in the integrity attribute match the content of the subresource
If the Console is silent and all script requests show HTTP 200, SRI enforcement is working.
4. CI gate
Permalink to "4. CI gate"Add a post-build verification step to your pipeline that fails the job if any emitted HTML file is missing integrity attributes:
# .github/workflows/build.yml
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build
- name: Assert SRI attributes present
run: |
count=$(grep -c 'integrity="sha384-' dist/index.html)
echo "SRI-annotated tags: $count"
[ "$count" -ge 1 ] || (echo "No integrity attributes found — failing build" && exit 1)
For compliance pipelines that require a machine-readable record, combine this check with Automated SBOM Generation to capture the hash manifest alongside the bill of materials artifact.
Align the hash rotation cadence and mismatch alerting with the Configuring Content-Security-Policy with SRI policy you deploy: a CSP require-sri-for directive enforces at the browser level that every subresource must carry a verified hash, providing a second enforcement layer independent of the build tooling.
Frequently asked questions
Permalink to "Frequently asked questions"Why do I need crossOriginLoading: 'anonymous' in Webpack 5?
Browsers refuse to verify SRI on cross-origin resources that arrive without CORS headers. Setting crossOriginLoading: 'anonymous' makes Webpack emit crossorigin="anonymous" on every dynamically loaded chunk, so the server’s Access-Control-Allow-Origin header is included in the response and the browser can perform the integrity check.
Does SubresourceIntegrityPlugin work with code splitting?
Yes. The plugin hooks into Webpack’s processAssets phase after all chunks are finalised, so split bundles and import() async chunks each receive their own integrity hash. The runtime chunk loader is also patched to verify hashes on lazy-loaded modules.
Which hash algorithm should I use — sha256 or sha384?
SHA-384 is the recommended default: it is stronger than SHA-256 and produces a shorter digest than SHA-512. All modern browsers support it. Use SHA-256 only if you need to match an existing legacy policy; use SHA-512 only for the most sensitive payloads.
Can I use multiple hash algorithms in a single integrity attribute?
Yes. Pass hashFuncNames: ['sha384', 'sha256'] to SubresourceIntegrityPlugin and it emits both digests space-separated in the integrity attribute. Browsers pick the strongest algorithm they support.
Does CDN compression (gzip/Brotli) invalidate SRI hashes?
No. Browsers hash the decoded response body, so transparent Content-Encoding compression is safe. What does break hashes is any CDN-level byte transformation of the payload itself — re-minification, script injection, or URL rewriting.
Related
Permalink to "Related"- Static Asset Hash Generation — the parent guide covering the full hash generation workflow, including CLI tools and manifest patterns
- Dynamic Script Loading Patterns — how to carry SRI hashes through to runtime
import()and script injection scenarios - Configuring Content-Security-Policy with SRI — enforcing hash requirements at the CSP layer to complement build-time automation