Verify CDN integrity with sha384sum in browsers

Ensuring the integrity of resources served from a Content Delivery Network (CDN) is paramount for
protecting your users and your reputation. Subresource Integrity (SRI) lets browsers verify that a
fetched file matches an expected cryptographic hash—effectively blocking compromised assets. In this
DevTip, we will look at why sha384sum
is the sweet spot for SRI hashes, how to generate and embed
those hashes, and how to automate the whole workflow from the command line to your CI/CD pipeline.
Understand subresource integrity (sri)
An SRI‐enabled browser walks through the following steps when it encounters the integrity
attribute:
- Download the resource (JavaScript, CSS, font, …).
- Compute its hash on the fly.
- Compare the result with the hash hard-coded in the HTML.
- Abort execution if the values differ.
The result is simple yet powerful—if someone tampers with a third-party script, the browser refuses to run it.
Why choose sha-384 for sri?
SHA-256 is the minimum algorithm the spec allows, and SHA-512 offers the strongest theoretical
security. SHA-384 hits the sweet spot: stronger than SHA-256 and slightly faster to compute than
SHA-512 on most CPUs. Every mainstream browser supports it, so we will use sha384-…
throughout
this article.
Generate a hash on the command line
sha384sum
prints a hex digest, but SRI needs base64. Use openssl
(readily available on
macOS and Linux) to get the right output:
cat your-file.js | openssl dgst -sha384 -binary | openssl base64 -A
To create the full SRI value in one go, prefix it with sha384-
:
echo "sha384-$(cat your-file.js | openssl dgst -sha384 -binary | openssl base64 -A)"
Bonus tip: the cross-platform srihash-cli
npm
package achieves the same without shell gymnastics.
Embed sri in your HTML or jsx
<!-- Single hash example -->
<script
src="https://example.com/js/library.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"
></script>
<!-- Multiple algorithms for legacy browsers -->
<link
rel="stylesheet"
href="https://example.com/css/styles.css"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC sha512-/OeNzF1PFA/xbzX92DyMRPz3opJ4nVDJ+EYhXpqwOFpq1DMLulFbN5OPUXmHkXmNovSH7BwpUQX/xFG0QZgTcA=="
crossorigin="anonymous"
/>
The crossorigin="anonymous"
attribute is required when the file comes from a different origin—
otherwise the browser skips verification.
Generate hashes in the browser with the Web Crypto API
When you need to validate or calculate hashes at runtime (think browser extensions, security test
pages, or dashboards), call crypto.subtle.digest
:
async function generateSRIHash(url) {
const response = await fetch(url)
const buffer = await response.arrayBuffer()
const hashBuffer = await crypto.subtle.digest('SHA-384', buffer)
// Convert the ArrayBuffer to a base64 string
const hashArray = Array.from(new Uint8Array(hashBuffer))
const binaryString = String.fromCharCode.apply(null, hashArray)
const base64Hash = btoa(binaryString)
return `sha384-${base64Hash}`
}
// Demo usage
generateSRIHash('https://cdn.example.com/library.min.js').then(console.log)
crypto.subtle
is available in every current evergreen browser, but only on secure origins
(https://
or http://localhost
during development).
Browser support
Subresource Integrity is supported in:
- Chrome 45+
- Firefox 43+
- Safari 11.1+
- Edge 17+
All of the above also ship the Web Crypto API. Older browsers will simply ignore the integrity
attribute and load the file as usual. Note: The Web Crypto API used for hash generation requires a
secure context (HTTPS).
Automate sri inside your ci/cd pipeline
A build step ensures that hashes stay in sync with your bundles. Below is a minimal GitHub Actions
job that updates an sri-hashes.json
file used by your templates:
name: build
on: [push]
jobs:
sri:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: npm ci
- name: Build assets
run: npm run build
- name: Generate SRI hashes
run: |
node scripts/generate-sri.js
- name: Commit updated hashes
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: 'chore(ci): update SRI hashes'
scripts/generate-sri.js
(kept below 20 lines) might look like this:
import { createHash } from 'node:crypto'
import { readFileSync, writeFileSync } from 'node:fs'
import { globby } from 'globby'
function sri(file) {
const data = readFileSync(file)
const hash = createHash('sha384').update(data).digest('base64')
return `sha384-${hash}`
}
const files = await globby(['dist/**/*.js', 'dist/**/*.css'])
const map = Object.fromEntries(files.map((f) => [f, sri(f)]))
writeFileSync('sri-hashes.json', JSON.stringify(map, null, 2))
console.table(map)
Your build template (e.g., Astro, Eleventy, Next.js) can then read the JSON and inject the correct hash automatically.
Load scripts dynamically
For code-splitting or feature toggles you often create elements on the fly. Make sure to apply the hash programmatically as well:
export async function loadScript(src, integrity) {
return new Promise((resolve, reject) => {
const script = document.createElement('script')
Object.assign(script, {
src,
integrity,
crossOrigin: 'anonymous',
})
script.addEventListener('load', () => resolve())
script.addEventListener('error', () => reject(new Error(`Failed to load or verify ${src}`)))
document.head.append(script)
})
}
Monitor CDN assets in production
A small helper class can keep an eye on frequently changing files and ping your observability stack when something looks fishy:
class SRIMonitor {
#entries = new Map()
#intervalId
constructor(interval = 5 * 60_000) {
this.interval = interval
}
add(url, expectedHash) {
this.#entries.set(url, expectedHash)
}
async #check(url, expected) {
const actual = await generateSRIHash(url)
if (actual !== expected) {
console.warn(`[SRI] Mismatch for ${url}`)
// Push to your SecOps webhook / Slack / PagerDuty …
await fetch('/api/security-alerts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url, expected, actual }),
})
}
}
start() {
if (this.#intervalId) return
this.#intervalId = setInterval(() => {
this.#entries.forEach((hash, url) => this.#check(url, hash))
}, this.interval)
}
stop() {
clearInterval(this.#intervalId)
}
}
Security considerations
- Always serve SRI-enabled resources over HTTPS.
- Include the
crossorigin="anonymous"
attribute when the resource is on a different domain. - Consider using multiple hash algorithms for better browser compatibility (e.g.,
sha384
andsha512
). - Be aware that SRI breaks if the resource changes—have a strategy for updating hashes, ideally automated in your CI/CD pipeline.
- Combine SRI with a Content Security Policy (CSP) that sets a denylist for inline scripts to further enhance security.
Troubleshoot common pitfalls
Symptom | Likely cause & fix |
---|---|
Script loads but doesn’t execute | Hash mismatch → regenerate & redeploy |
Works locally, fails on PROD | Production CDN rewrites files (minification, compression) |
Blocked by CORS policy |
Missing crossorigin attribute or CDN lacks proper headers (e.g. Access-Control-Allow-Origin ) |
SRI ignored entirely | Page or resource served over HTTP instead of HTTPS |
Implement fallbacks
If verification fails you can gracefully fall back to a mirror or a self-hosted copy:
async function loadWithFallback(primary, backup, integrity) {
try {
await loadScript(primary, integrity)
} catch {
console.warn(`Primary failed, switching to ${backup}`)
await loadScript(backup, integrity) // Consider if the same integrity hash applies or if backup has its own
}
}
How Transloadit uses file hashing
At Transloadit, we provide the 🤖 /file/hash Robot that supports multiple hash algorithms including SHA-384. While this Robot can be used as part of your media processing pipeline, for SRI generation specifically, you should use the methods described above since the Robot's primary purpose is for file integrity verification within Assemblies.
By adopting SRI with sha384sum
—and automating it from development through deployment—you add an
important extra layer of defense against supply-chain attacks without sacrificing performance.