Automatic SRI hashes with webpack and Vite
CentralCSP Team ·
Last update:
A bundler emits dozens of hashed chunk files, and their names and contents change on every build. Computing a Subresource Integrity (SRI) hash for each one by hand is not realistic. The fix is to let the build do it: both webpack and Vite can write the integrity attribute onto the <script> and <link> tags they inject, and keep each hash in sync with the chunk it covers.
The short version: add the webpack-subresource-integrity plugin for webpack, or a community plugin such as @small-tech/vite-plugin-sri for Vite, and the integrity attributes appear automatically. One catch matters: the bundler only hashes the chunks it emits. The third-party scripts you load from a CDN are not in the build, so they get no integrity, and you still have to watch them yourself.
Why automate it
SRI is tied to the exact bytes of a file. Change the file, regenerate the hash, or the browser blocks the load. For your own bundled output, the bytes change on every meaningful build, and the file names are content-hashed too, so a hand-maintained list of hashes would be wrong within one commit.
Wiring SRI into the build removes that maintenance entirely for your own assets. The plugin hashes each chunk after it is emitted and writes the matching integrity attribute into the HTML or asset manifest, so the hash and the file always agree. How to generate a Subresource Integrity (SRI) hash covers the manual route for one-off files; this post is the build-time route for many.
Webpack
Install the plugin and add it to your plugins list. It reads which hash functions you want and writes the integrity attribute onto the tags injected by html-webpack-plugin.
const { SubresourceIntegrityPlugin } = require("webpack-subresource-integrity");
module.exports = {
output: {
crossOriginLoading: "anonymous", // required for SRI to be checked
},
plugins: [
new SubresourceIntegrityPlugin({
hashFuncNames: ["sha384"],
}),
],
};Two pieces are easy to miss. You must set output.crossOriginLoading to "anonymous", because the browser cannot verify an integrity hash on a cross-origin chunk fetched without CORS, and a bundle served from a CDN is cross-origin. The plugin emits a warning if you forget. Second, the plugin integrates with html-webpack-plugin to stamp the attributes onto the generated tags; if you inject scripts another way, you read the hashes from the asset manifest instead.
The code-splitting gotcha
This is the reason a generic "hash the files" script is not enough. Webpack code-splitting produces dynamically imported chunks that the runtime loads on demand, not tags in your HTML. Those chunks reference each other, so a child chunk's hash is part of its parent's content, and computing the hashes in the wrong order produces values that never match.
webpack-subresource-integrity handles this. It hooks into the webpack runtime so dynamically loaded chunks carry and verify integrity too, and it orders the hashing so cross-chunk references resolve correctly. A naive post-build hashing pass over the output folder cannot do that, which is why the plugin exists rather than a shell one-liner.
Vite
Vite ships no first-party SRI, so use a community plugin. @small-tech/vite-plugin-sri computes the hashes at build time and injects the integrity attributes into the emitted index.html.
import { defineConfig } from "vite";
import sri from "@small-tech/vite-plugin-sri";
export default defineConfig({
plugins: [sri()],
});The plugin runs during the build and rewrites the script and stylesheet tags with their integrity values. Check the plugin's own readme for how it handles your asset layout and whether it covers dynamically imported chunks, since the Vite ecosystem has several SRI plugins with different scopes. As with webpack, assets served cross-origin still need the browser to fetch them with CORS for the check to run.
The gap, third-party CDN scripts
Here is the part teams miss. The bundler hashes your chunks. It does not hash the scripts you pull straight from a third-party CDN, a tag manager, an analytics snippet, a payment widget, a chat embed, because those never pass through the build.
Those third-party scripts are exactly the ones worth pinning, since you do not control the server that serves them. The bundler cannot help here, so you handle them separately:
- Add
integrityandcrossoriginto each third-party tag by hand, generating the value with the SRI generator, and pin a versioned URL so the bytes do not move under you. - Or require integrity across the whole document with the
Integrity-Policyheader, which blocks in-scope scripts that load without integrity metadata (recently available across current Chrome, Firefox, and Safari).
Either way, you first have to know which third-party scripts you load, and that list changes as marketing and product add tags over time. CentralCSP builds a script inventory of every script running on your pages from CSP hash reports, so a new or changed third-party file shows up as a finding instead of slipping in unnoticed. That is the visibility the bundler cannot give you, because the bundler only sees what it built.
Putting it together
- Wire SRI into the build for your own assets:
webpack-subresource-integrityfor webpack, a Vite SRI plugin for Vite. Set cross-origin loading toanonymous. - Let the plugin handle code-split dynamic imports; do not roll your own post-build hashing pass.
- Hash third-party CDN scripts separately, and track which ones you load so the list does not drift.
To see what third-party code is actually running on your pages before you start pinning it, start free and let CentralCSP inventory your client-side scripts.
Sources
- webpack-subresource-integrity, GitHub
- webpack-subresource-integrity, npm
- @small-tech/vite-plugin-sri, GitHub
- MDN, Subresource Integrity