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 script or stylesheet referenced by the element.
- 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?
SRI supports SHA-256, SHA-384, and SHA-512. We use SHA-384 throughout this article: it is widely supported and produces a shorter digest than SHA-512. Hashing speed depends on the implementation and hardware; choose a supported algorithm rather than relying on a general speed ranking.
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)"
Embed SRI in your HTML or JSX
Replace every sample hash below with the digest of that exact file's trusted release bytes.
Browsers use the strongest supported algorithm when multiple algorithms are provided; they do not
fall back to a weaker hash after a mismatch. In JSX, spell the attribute crossOrigin.
<!-- Single hash example -->
<script
src="https://example.com/js/library.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"
></script>
<!-- Multiple algorithms; replace these example digests for this stylesheet -->
<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.
The CDN must also allow the requesting origin through CORS. Without those conditions the browser
blocks the cross-origin resource instead of silently bypassing integrity 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, {
cache: 'no-store',
signal: AbortSignal.timeout(10_000),
})
if (!response.ok) throw new Error(`Unable to fetch resource: HTTP ${response.status}`)
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).catch(console.error)
crypto.subtle is available in every current evergreen browser, but only on secure origins
(https:// or http://localhost during development).
The runtime examples use AbortSignal.timeout(), available in current evergreen browsers, to
bound each request to ten seconds of active time. This prevents a stalled download or alert from
blocking subsequent monitoring checks; adjust the timeout for your expected resource sizes.
This function calculates a digest, not a verdict about authenticity. Compare it with a trusted
release digest. Generating a new expected hash from the same compromised CDN would trust the
attacker's replacement. Fetching and hashing a URL also does not prove which bytes a separate
script request previously executed; the element's integrity attribute enforces that check.
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.
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 builds assets and hashes from the same checkout. Install globby as a development
dependency, commit the lockfile, and use Node.js 22 or later. Your build must emit dist/ before
the hashing step; render templates with the generated manifest afterward and deploy both together:
name: build
on: [push]
permissions:
contents: read
jobs:
sri:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Install dependencies
run: npm ci
- name: Build assets
run: npm run build
- name: Generate SRI hashes
run: |
node scripts/generate-sri.mjs
- name: Save assets and hash manifest
uses: actions/upload-artifact@v4
with:
name: assets-with-integrity
path: |
dist/
sri-hashes.json
scripts/generate-sri.mjs:
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'])
if (files.length === 0) throw new Error('No built assets found in dist/')
const map = Object.fromEntries(files.map((f) => [f, sri(f)]))
writeFileSync('sri-hashes.json', `${JSON.stringify(map, null, 2)}\n`)
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 fetched content differs from a trusted expected hash. This is periodic monitoring, not a
real-time guarantee. Define an authenticated, rate-limited /api/security-alerts endpoint before
using this example, and do not include credential-bearing resource URLs in alerts:
class SRIMonitor {
#entries = new Map()
#intervalId
#checking = false
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 …
const response = await fetch('/api/security-alerts', {
method: 'POST',
signal: AbortSignal.timeout(10_000),
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url, expected, actual }),
})
if (!response.ok) throw new Error(`Alert delivery failed: HTTP ${response.status}`)
}
}
async #poll() {
if (this.#checking) return
this.#checking = true
try {
for (const [url, hash] of this.#entries) {
try {
await this.#check(url, hash)
} catch (error) {
console.error('SRI monitor check failed:', error)
}
}
} finally {
this.#checking = false
}
}
start() {
if (this.#intervalId !== undefined) return
this.#intervalId = setInterval(() => {
void this.#poll()
}, this.interval)
}
stop() {
clearInterval(this.#intervalId)
this.#intervalId = undefined
}
}
Security considerations
- Always serve SRI-enabled resources over HTTPS.
- Include the
crossorigin="anonymous"attribute when the resource is on a different domain. - Obtain expected hashes from a trusted release or your own build, independently of the CDN.
- 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 allows only required script sources and
authorizes inline scripts with nonces or hashes instead of
'unsafe-inline'.
Troubleshoot common pitfalls
| Symptom | Likely cause & fix |
|---|---|
| Script loads but doesn’t execute | Hash mismatch → regenerate & redeploy |
| Works locally, fails on PROD | CDN minification changes decoded bytes; ordinary HTTP compression does not change the digest |
Blocked by CORS policy | Missing crossorigin attribute or CDN lacks proper headers (e.g. Access-Control-Allow-Origin) |
| SRI ignored entirely | Unsupported browser, unsupported element, or missing integrity metadata |
Implement fallbacks
If verification fails you can fall back to a byte-identical trusted mirror or self-hosted copy. Keep integrity verification enabled on the fallback:
async function loadWithFallback(primary, backup, integrity) {
try {
await loadScript(primary, integrity)
} catch {
console.warn(`Primary failed, switching to ${backup}`)
await loadScript(backup, integrity)
}
}
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. The Robot computes file digests within Assemblies; your application must compare them with trusted expected values to verify integrity.
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.
