Generating SRI Hashes for CDN Scripts
This guide is part of the Subresource Integrity (SRI) reference. Subresource Integrity pins a cryptographic hash to every <script> and <link rel="stylesheet"> you load from a third-party origin. When the browser fetches the resource it recomputes the digest over the exact response bytes and refuses to execute or apply anything whose digest does not match the integrity attribute. A compromised CDN, a poisoned cache, or a tampering proxy therefore cannot silently swap your jquery.min.js for an exfiltration payload — the browser blocks the load instead. This page gives you the exact commands to produce those hashes, the build-tool automation that keeps them current, and the crossorigin and fallback rules that make SRI actually enforce rather than silently fail open.
Configuration Syntax & Exact Values
The integrity attribute is a space-separated list of one or more hash expressions. Each expression is a hash algorithm token, a hyphen, and the base64-encoded raw digest of the resource body. The algorithm token is one of sha256, sha384, or sha512. sha384 is the working default: it is longer than sha256 (blunting theoretical collision shortcuts) without the size overhead of sha512.
<script src="https://cdn.example.com/lib/chart@4.4.1/dist/chart.umd.min.js"
integrity="sha384-Q6E1oHUEHW7NNI4923i0i0eEnFd+3D64ux3xkFTfd0Kq8i0i9J1cQ3n9pXm9m1k"
crossorigin="anonymous"></script>
Three parts of that tag are load-bearing and each fails a different way if wrong:
integritycarries the pinned digest. A single wrong character means the browser computes a mismatch and blocks the resource with a console error. There is no partial acceptance.crossorigin="anonymous"is mandatory for any cross-origin resource. SRI validation requires the response to be readable under CORS; without this attribute the browser fetches the script opaquely, cannot read its bytes to hash them, and refuses the load outright.anonymoussends the request without credentials, which is correct for public CDN assets.- The
src/hreforigin must return anAccess-Control-Allow-Originheader that permits your page. Public CDNs (jsdelivr, cdnjs, unpkg, Google Fonts) already do; a private asset host may not.
You may list several expressions separated by spaces. The browser evaluates them by strongest algorithm present — it picks the highest-strength algorithm it supports, computes that one digest, and requires it to match one of the listed values of that algorithm. Listing multiple algorithms is a migration aid, not a “match any of these files” mechanism.
<link rel="stylesheet" href="https://cdn.example.com/ui/2.9/theme.css"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQ ... sha512-Rk9uP0uY+... "
crossorigin="anonymous">
The single most common mistake is base64-encoding the hex digest instead of the raw binary digest. sha384sum and shasum print hex by default; you must feed the binary form into base64. The commands below do that correctly.
Server-Side Configuration
“Server-side” here means the machine or pipeline where you generate the hash — your workstation or CI runner — plus the tooling that keeps it synchronized. Every command below reads the exact bytes the CDN serves and emits a paste-ready hash expression.
openssl (portable, any Unix or Git Bash)
openssl is present on virtually every Linux and macOS host. The -binary flag is the critical one: it emits the raw digest bytes so the piped openssl base64 encodes the correct value. base64 -A keeps the output on a single line with no wrapping.
# Hash a local file
openssl dgst -sha384 -binary chart.umd.min.js | openssl base64 -A
# Equivalent, streaming the file in
cat chart.umd.min.js | openssl dgst -sha384 -binary | openssl base64 -A
# Prepend the algorithm token to get a paste-ready expression
echo "sha384-$(openssl dgst -sha384 -binary chart.umd.min.js | openssl base64 -A)"
To hash the file the CDN actually serves — the only bytes the browser will ever validate against — pull it over the wire first. Never hash your local node_modules copy and assume the CDN copy is byte-identical; minifier versions and line endings differ.
curl -sSL https://cdn.example.com/lib/chart@4.4.1/dist/chart.umd.min.js \
| openssl dgst -sha384 -binary | openssl base64 -A
shasum / sha384sum (coreutils)
shasum -a 384 and the GNU sha384sum binary print hex, so decode the hex to binary before base64. xxd -r -p performs the hex-to-binary step.
# shasum prints "<hex> <filename>"; take field 1, unhex, base64
shasum -a 384 chart.umd.min.js | cut -d' ' -f1 | xxd -r -p | base64 -w0
# GNU coreutils equivalent
sha384sum chart.umd.min.js | cut -d' ' -f1 | xxd -r -p | base64 -w0
Prefer the openssl form when it is available — it avoids the hex round-trip entirely and is harder to get wrong.
Vite (build-time automation)
For bundled assets that change on every deploy, generate integrity at build time rather than by hand. The vite-plugin-sri family walks the emitted HTML and injects integrity plus crossorigin on every <script> and stylesheet <link>.
// vite.config.js
import { defineConfig } from 'vite';
import sri from 'vite-plugin-sri3';
export default defineConfig({
plugins: [
sri({ algorithm: 'sha384' }),
],
});
webpack (build-time automation)
webpack-subresource-integrity computes hashes over the final emitted chunks. It requires output.crossOriginLoading to be set so the injected <script> tags carry a crossorigin attribute — without it dynamically loaded chunks fail SRI in the same way a hand-written tag would.
// webpack.config.js
const { SubresourceIntegrityPlugin } = require('webpack-subresource-integrity');
module.exports = {
output: {
crossOriginLoading: 'anonymous', // REQUIRED: emits crossorigin on injected tags
},
plugins: [
new SubresourceIntegrityPlugin({
hashFuncNames: ['sha384'],
enabled: process.env.NODE_ENV === 'production',
}),
],
};
Diagnostic & Verification Steps
Verify a hash before you deploy it, then confirm the browser enforces it after.
Recompute and compare against the tag you are about to ship. Identical output means the pinned value matches the live CDN bytes:
$ curl -sSL https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js \
| openssl dgst -sha384 -binary | openssl base64 -A
Q6E1oHUEHW7NNI4923i0i0eEnFd+3D64ux3xkFTfd0Kq8i0i9J1cQ3n9pXm9m1k
Confirm the CDN sends the CORS header SRI needs. access-control-allow-origin: * (or your origin) must be present or crossorigin="anonymous" cannot succeed:
$ curl -sSI https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js \
| grep -i access-control-allow-origin
access-control-allow-origin: *
Confirm the browser enforces the pin. Load the page, then deliberately corrupt one character of the integrity value and reload — DevTools Console prints a precise mismatch error and the resource is blocked:
Failed to find a valid digest in the 'integrity' attribute for resource
'https://cdn.example.com/lib/chart.umd.min.js' with computed SHA-384 integrity
'Q6E1oHUEHW7NNI4923i0i0eEnFd+3D64ux3xkFTfd0Kq8i0i9J1cQ3n9pXm9m1k'.
The resource has been blocked.
That error is the single most useful artifact: the browser prints the digest it computed, so you can copy the value straight from the console into your tag when a CDN version bump changes the bytes.
Edge Cases, Security Implications & Safe Rollback
Three traps account for nearly every broken SRI deployment:
- Version-pinned URLs are mandatory. SRI pins one exact byte sequence. If your
srcpoints at a floating alias like.../jquery/latest/jquery.min.js, the CDN will eventually serve a newer file whose digest no longer matches, and the browser will hard-block your library across every page. Always pin an immutable, version-locked URL (chart.js@4.4.1, notchart.js) so the bytes never move under a fixed hash. - Fallback loaders do not run on an SRI block. The classic pattern
window.jQuery || document.write('<script src="local...">')works when a CDN 404s, but an SRI mismatch is a load failure too — so a genuine tampering event triggers your fallback and can silently pull an unverified local copy. Give the fallback its ownintegrity, and treat SRI enforcement as fail-closed for anything security-sensitive. Do not paper over a mismatch by removing the attribute. - CORS is a prerequisite, not an optional extra. A resource without
crossorigin="anonymous"(or from an origin that omitsAccess-Control-Allow-Origin) is fetched opaquely; the browser cannot read the bytes to hash them and blocks the load. This looks identical to a hash mismatch in the console. Confirm the CORS header with thecurl -Icheck above before blaming the digest.
Rollback is mechanical because SRI lives entirely in the markup. To disable enforcement for one resource in an incident, remove that tag’s integrity attribute (and crossorigin if it is otherwise unused) and redeploy — the browser then loads the resource without validation. Because the pin is per-tag, you roll back exactly one script without touching any other. Keep the previous known-good hash in version control so re-enabling is a one-line revert once the CDN mismatch is understood.
Conclusion
Roll SRI out in the same staged way as any enforcement header. In staging, add integrity to one non-critical CDN asset and confirm it loads, then corrupt the hash to confirm the block fires and the console error appears. Promote to production behind a short cache lifetime so a bad pin can be flushed quickly, watching your error telemetry for integrity mismatches the way you would watch a Content-Security-Policy (CSP) report stream. Once a version-pinned asset validates cleanly, extend the same generated-hash workflow to every third-party script and stylesheet and let your build tool keep the values current.
Frequently Asked Questions
Why is my integrity value rejected even though the file looks correct?
The most common cause is base64-encoding the hex digest instead of the raw binary digest. Tools like sha384sum and shasum -a 384 print hex; you must run it through xxd -r -p before base64, or use openssl dgst -sha384 -binary which emits raw bytes directly. A hex-then-base64 value is the wrong length and always fails.
Do I need crossorigin="anonymous" on same-origin scripts?
No. SRI on a same-origin resource works without crossorigin because the browser can already read the response bytes. The attribute is required only for cross-origin resources such as CDN assets, where SRI validation depends on a CORS-readable response.
Which algorithm should I use — sha256, sha384, or sha512?
Use sha384 as the default. All three are valid SRI algorithms and the browser always validates against the strongest one present. sha384 gives more collision margin than sha256 without the larger attribute size of sha512, and it is the value emitted by most CDN “copy integrity” buttons.
What happens if I list two algorithms in one integrity attribute?
The browser picks the strongest algorithm it supports from the list, computes that single digest, and requires it to match one of the listed values for that algorithm. It is a migration and prioritization mechanism, not a way to whitelist two different files under one tag.
Does SRI protect against a CDN going down or returning a 404?
No. SRI only verifies the integrity of bytes that are successfully fetched; it does nothing for availability. A network failure or 404 is a normal load error. If you need resilience, add a fallback loader that pulls a same-origin copy — and give that fallback its own integrity so it is verified too.