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.

Webpack 5 SRI automation pipeline Source files enter the Webpack compilation pipeline, pass through minification and chunk splitting, then SubresourceIntegrityPlugin computes SHA-384 digests at the processAssets stage, and finally HtmlWebpackPlugin emits HTML with integrity and crossorigin attributes injected on each script and link tag. Source files JS · CSS · assets Compilation Terser · tree-shake CSS extract · split SRI Plugin processAssets hook SHA-384 per chunk HtmlWebpackPlugin integrity="sha384-…" crossorigin="anonymous" entry + chunks final byte buffers only emitted index.html

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>

The value the plugin writes is not opaque — it decomposes into a fixed grammar that the browser re-parses on every load. The token before the hyphen names the digest function, and everything after it is the standard base64 encoding of the raw digest bytes, padding included. If either half is malformed the whole attribute is discarded as unparseable and the resource loads unverified rather than failing closed, which is why a typo in a hand-written hash is far more dangerous than a wrong one.

Anatomy of an emitted integrity attribute The attribute string is split into five segments: the attribute name with its opening quote, the algorithm label sha384, the hyphen separator, the base64 digest of the final bytes, and the closing quote. Callouts label each segment, and a note below states that the same tag must also carry crossorigin equals anonymous. What SubresourceIntegrityPlugin writes onto every tag integrity=" sha384 - oqVuAfXRKap7fdgcCY5uykM6 " attribute name on the tag algorithm label sha384 preferred base64 of the final bytes recomputed by the browser The same tag must carry crossorigin="anonymous" or the no-CORS fetch is never hash-verified at all

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; for the two remaining mainstream bundlers, Adding SRI to Rollup and esbuild Builds covers the post-build hashing step each of them needs. 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"
  • crossOriginLoading omission is the most common failure. Without crossOriginLoading: 'anonymous', dynamically loaded chunks do not carry crossorigin="anonymous", so the browser issues a no-CORS fetch and then refuses to hash-verify the result. The console shows ERR_FAILED on the chunk, not an SRI error, which makes this hard to diagnose; SRI for Lazy-Loaded Chunks covers the runtime half of the same failure.
  • Plugin ordering matters for HTML injection. HtmlWebpackPlugin must appear before SubresourceIntegrityPlugin in 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-integrity runs — 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-level Content-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 integrity attributes and their content does not affect production hashes.
  • contenthash vs chunkhash vs hash. 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 with Cache-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.

The filename placeholder choice deserves a second look, because it decides how often your integrity values churn. [chunkhash] is derived from the whole chunk graph, so a change to any module that lands in the chunk renames every file in it — and because module identifiers can differ between a local build and a CI build with a different working directory or a different Node version, the same source tree can produce two different [chunkhash] values. That produces integrity attributes that look drifted when nothing was tampered with, and it is the single most common cause of a false positive in a hash-comparison gate.

Filename placeholder comparison A three-column matrix comparing the contenthash, chunkhash and hash filename placeholders across three rows: when the value changes, the effect on browser caching, and the effect on SRI integrity values. Placeholder [contenthash] [chunkhash] [hash] Changes when the file's own bytes change any module in that chunk changes every build, even with no edits Cache effect immutable one-year cache is safe over-invalidates sibling bundles defeats long-lived browser caching SRI impact name and digest rotate together same bytes, new name across envs manifest churn on every deploy

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)

That check only asserts that some integrity attribute exists. A stricter gate records the emitted digests as a build artifact and diffs them against the previous release, which is the workflow Generating an SRI Manifest in GitHub Actions builds out step by step.

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.

Permalink to "Related"

Related Articles

Generating SRI Hashes in Vite
Generating SRI Hashes with OpenSSL and shasum
Adding SRI to Rollup and esbuild Builds
Static Asset Hash Generation Asset Hashing & Dynamic Script…