CDN-Integrität mit sha384sum im Browser prüfen
Die Integrität von Ressourcen, die über ein Content Delivery Network (CDN) ausgeliefert werden, ist
von größter Bedeutung für den Schutz Ihrer Nutzer und Ihrer Reputation. Subresource Integrity (SRI)
ermöglicht es Browsern, zu prüfen, ob eine abgerufene Datei einem erwarteten kryptografischen Hash
entspricht – und blockiert so kompromittierte Assets. In diesem DevTip sehen wir uns an, warum
sha384sum der ideale Kompromiss für SRI-Hashes ist, wie Sie diese Hashes
erzeugen und einbetten und wie Sie den gesamten Arbeitsablauf von der Kommandozeile bis zu Ihrer
CI/CD-Pipeline automatisieren.
Subresource Integrity (SRI) verstehen
Ein Browser mit SRI-Unterstützung durchläuft die folgenden Schritte, wenn er auf das Attribut
integrity trifft:
- Das vom Element referenzierte Script oder Stylesheet herunterladen.
- Dessen Hash unmittelbar berechnen.
- Das Ergebnis mit dem im HTML fest hinterlegten Hash vergleichen.
- Die Ausführung abbrechen, wenn die Werte voneinander abweichen.
Das Ergebnis ist einfach und zugleich wirkungsvoll: Manipuliert jemand ein Script eines Drittanbieters, weigert sich der Browser, es auszuführen.
Warum SHA-384 für SRI wählen?
SRI unterstützt SHA-256, SHA-384 und SHA-512. In diesem Artikel verwenden wir durchgehend SHA-384: Es ist breit unterstützt und erzeugt einen kürzeren Digest als SHA-512. Die Hashing-Geschwindigkeit hängt von der Implementierung und der Hardware ab; wählen Sie einen unterstützten Algorithmus, statt sich auf eine allgemeine Geschwindigkeitsrangfolge zu verlassen.
Einen Hash auf der Kommandozeile erzeugen
sha384sum gibt einen Digest in Hex aus, SRI benötigt jedoch base64.
Verwenden Sie openssl (auf macOS und Linux ohne Weiteres verfügbar), um die
richtige Ausgabe zu erhalten:
cat your-file.js | openssl dgst -sha384 -binary | openssl base64 -A
Um den vollständigen SRI-Wert in einem Durchgang zu erzeugen, stellen Sie
sha384- voran:
echo "sha384-$(cat your-file.js | openssl dgst -sha384 -binary | openssl base64 -A)"
SRI in Ihr HTML oder JSX einbetten
Ersetzen Sie jeden Beispiel-Hash unten durch den Digest der vertrauenswürdigen Release-Bytes genau
dieser Datei. Browser verwenden den stärksten unterstützten Algorithmus, wenn mehrere Algorithmen
angegeben sind; sie weichen nach einer Abweichung nicht auf einen schwächeren Hash aus. Schreiben
Sie das Attribut in JSX als 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"
/>
Das Attribut crossorigin="anonymous" ist erforderlich, wenn die Datei von einem anderen
Ursprung stammt. Zudem muss das CDN den anfragenden Ursprung per CORS zulassen. Ohne diese
Voraussetzungen blockiert der Browser die ursprungsübergreifende Ressource, anstatt die
Integritätsprüfung stillschweigend zu umgehen.
Hashes im Browser mit der Web Crypto API erzeugen
Wenn Sie Hashes zur Laufzeit prüfen oder berechnen müssen (etwa in Browser-Erweiterungen, auf
Sicherheitstestseiten oder in Dashboards), rufen Sie crypto.subtle.digest auf:
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 steht in jedem aktuellen Evergreen-Browser zur Verfügung, jedoch nur
auf sicheren Ursprüngen (https:// oder http://localhost während
der Entwicklung).
Die Laufzeitbeispiele verwenden AbortSignal.timeout(), das in aktuellen
Evergreen-Browsern verfügbar ist, um jede Anfrage auf zehn Sekunden aktive Zeit zu begrenzen. So
blockiert ein hängender Download oder Alert nicht die nachfolgenden Monitoring-Prüfungen; passen
Sie das Timeout an die zu erwartenden Ressourcengrößen an.
Diese Funktion berechnet einen Digest, kein Urteil über die Echtheit. Vergleichen Sie ihn mit einem
vertrauenswürdigen Release-Digest. Einen neuen Erwartungs-Hash aus demselben kompromittierten CDN
zu erzeugen, hieße dem Austausch durch den Angreifer zu vertrauen. Eine URL abzurufen und zu hashen
belegt zudem nicht, welche Bytes eine separate Script-Anfrage zuvor ausgeführt hat; diese Prüfung
erzwingt das Attribut integrity des Elements.
Browser-Unterstützung
Subresource Integrity wird unterstützt in:
- Chrome 45+
- Firefox 43+
- Safari 11.1+
- Edge 17+
Alle genannten Browser bringen auch die Web Crypto API mit. Ältere Browser ignorieren das Attribut
integrity schlicht und laden die Datei wie gewohnt.
Die Web Crypto API, die zur Hash-Erzeugung verwendet wird, erfordert einen sicheren Kontext (HTTPS).
SRI in Ihrer CI/CD-Pipeline automatisieren
Ein Build-Schritt sorgt dafür, dass die Hashes mit Ihren Bundles synchron bleiben. Nachfolgend
sehen Sie einen minimalen GitHub-Actions-Job, der Assets und Hashes aus demselben Checkout erzeugt.
Installieren Sie globby als Entwicklungsabhängigkeit, checken Sie die
Lockfile ein und verwenden Sie Node.js 22 oder neuer. Ihr Build muss
dist/ vor dem Hashing-Schritt erzeugen; rendern Sie die Vorlagen
anschließend mit dem generierten Manifest und deployen Sie beides gemeinsam:
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)
Ihre Build-Vorlage (z. B. Astro, Eleventy, Next.js) kann das JSON anschließend auslesen und den korrekten Hash automatisch einfügen.
Scripts dynamisch laden
Für Code-Splitting oder Feature-Toggles erzeugen Sie Elemente häufig zur Laufzeit. Achten Sie darauf, den Hash ebenfalls programmatisch zu setzen:
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)
})
}
CDN-Assets in der Produktion überwachen
Eine kleine Hilfsklasse kann häufig wechselnde Dateien im Blick behalten und Ihren
Observability-Stack benachrichtigen, wenn abgerufene Inhalte von einem vertrauenswürdigen
Erwartungs-Hash abweichen. Dabei handelt es sich um eine periodische Überwachung, nicht um eine
Garantie in Echtzeit. Definieren Sie einen authentifizierten, ratenbegrenzten Endpunkt
/api/security-alerts, bevor Sie dieses Beispiel verwenden, und nehmen Sie keine
Ressourcen-URLs mit Zugangsdaten in Alerts auf:
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
}
}
Sicherheitsüberlegungen
- Liefern Sie Ressourcen mit SRI stets über HTTPS aus.
- Geben Sie das Attribut
crossorigin="anonymous"an, wenn die Ressource auf einer anderen Domain liegt. - Beziehen Sie erwartete Hashes aus einem vertrauenswürdigen Release oder Ihrem eigenen Build, unabhängig vom CDN.
- Beachten Sie, dass SRI bricht, sobald sich die Ressource ändert: Halten Sie eine Strategie zum Aktualisieren der Hashes bereit, idealerweise automatisiert in Ihrer CI/CD-Pipeline.
- Kombinieren Sie SRI mit einer Content Security Policy (CSP), die nur die erforderlichen
Script-Quellen zulässt und Inline-Scripts mit Nonces oder Hashes autorisiert statt mit
'unsafe-inline'.
Häufige Stolperfallen beheben
| Symptom | Wahrscheinliche Ursache & Behebung |
|---|---|
| Script wird geladen, aber nicht ausgeführt | Hash stimmt nicht überein → neu erzeugen & erneut deployen |
| Funktioniert lokal, schlägt auf PROD fehl | CDN-Minifizierung ändert die decodierten Bytes; gewöhnliche HTTP-Komprimierung ändert den Digest nicht |
Blocked by CORS policy | Fehlendes Attribut crossorigin oder das CDN sendet keine passenden Header (z. B. Access-Control-Allow-Origin) |
| SRI wird vollständig ignoriert | Nicht unterstützter Browser, nicht unterstütztes Element oder fehlende Integritäts-Metadaten |
Fallbacks implementieren
Schlägt die Prüfung fehl, können Sie auf einen byte-identischen vertrauenswürdigen Mirror oder eine selbst gehostete Kopie ausweichen. Lassen Sie die Integritätsprüfung auch für den Fallback aktiviert:
async function loadWithFallback(primary, backup, integrity) {
try {
await loadScript(primary, integrity)
} catch {
console.warn(`Primary failed, switching to ${backup}`)
await loadScript(backup, integrity)
}
}
Wie Transloadit Datei-Hashing einsetzt
Bei Transloadit bieten wir den Robot 🤖 /file/hash an, der mehrere Hash-Algorithmen unterstützt, darunter SHA-384. Dieser Robot lässt sich zwar als Teil Ihrer Pipeline zur Medienverarbeitung einsetzen, für die SRI-Erzeugung sollten Sie jedoch die oben beschriebenen Methoden verwenden. Der Robot berechnet Datei-Digests innerhalb von Assemblies; Ihre Anwendung muss sie mit vertrauenswürdigen Erwartungswerten vergleichen, um die Integrität zu prüfen.
Wenn Sie SRI mit sha384sum einsetzen – und es von der Entwicklung bis zum
Deployment automatisieren –, fügen Sie eine wichtige zusätzliche Verteidigungsebene gegen
Supply-Chain-Angriffe hinzu, ohne Performance einzubüßen.
