CDN-Integrität mit SHA-384 und SRI prüfen
Um ein CDN-Skript auf vertrauenswürdige Bytes festzulegen, erzeugen Sie einen SHA-384-Hash aus Ihrer
freigegebenen Release-Datei und tragen ihn im Attribut integrity des Skripts ein.
Diese Anleitung liefert einen Hash-Befehl, der bei fehlender Eingabe fehlschlägt, und ein lokales
Browserbeispiel, das belegt, dass geänderte Skripte nicht ausgeführt werden können.
Obwohl sha384sum den richtigen Algorithmus verwendet, benötigt seine hexadezimale
Ausgabe für Subresource Integrity (SRI) eine andere Zeichendarstellung.
Subresource Integrity (SRI) verstehen
Der Browser lädt das Skript herunter, berechnet einen Hash seines Inhalts und vergleicht das Ergebnis vor der Ausführung mit dem erwarteten Wert in Ihrem HTML. Eine Abweichung blockiert die Ausführung. Der Hash muss aus einem vertrauenswürdigen Build oder einem unabhängig geprüften Release stammen: Wenn Sie den Hash einer kompromittierten CDN-Antwort berechnen und diesen Wert akzeptieren, geben Sie den Ersatz frei. Auch Ihr HTML gehört zu dieser Vertrauensgrenze. Wer sowohl das Skript als auch den erwarteten Hash ändern kann, kann die Prüfung umgehen.
SRI unterstützt SHA-256, SHA-384 und SHA-512. SHA-384 ist eine sinnvolle Basis mit einem kürzeren Hash als SHA-512. Wenn Sie mehrere Algorithmen angeben, verwenden Browser den stärksten unterstützten Algorithmus. Bei einer Abweichung greifen sie nicht auf einen schwächeren zurück. Siehe SRI-Spezifikation.
Einen Hash auf der Kommandozeile erzeugen
Verwenden Sie Bash und OpenSSL für den Befehl sowie Node.js für die lokalen Server weiter unten. Dieses Beispiel wurde unter Linux mit Bash 5.3.15, OpenSSL 3.6.4, Node.js 26.8.1 und Chromium 152 getestet. Es erfordert weder eine Paketinstallation noch ein CDN-Konto. Speichern Sie die Beispieldateien in einem neuen, leeren Verzeichnis.
Speichern Sie dies als demo.js. Die Datei steht stellvertretend für die
freigegebene Release-Datei, die Sie normalerweise aus Ihrem Build oder vom Herausgeber beziehen:
document.getElementById('status').textContent = 'Trusted script executed'
Speichern Sie Folgendes als sri.sh. Bei Erfolg gibt es einen SRI-Wert aus,
bei Fehlern schreibt es Diagnosemeldungen auf stderr. Die Eingabedatei wird niemals verändert:
#!/usr/bin/env bash
set -o pipefail
if [[ $# -ne 1 || ! -f "$1" || ! -r "$1" ]]; then
printf 'Usage: bash sri.sh readable-file\n' >&2
exit 1
fi
if digest=$(openssl dgst -sha384 -binary < "$1" | openssl base64 -A); then
printf 'sha384-%s\n' "$digest"
else
printf 'Could not generate SRI for %s\n' "$1" >&2
exit 1
fi
Die OpenSSL-Option -binary erzeugt die
Hash-Bytes; base64 -A stellt sie ohne
Zeilenumbrüche dar. Wenden Sie base64 nicht auf den von sha384sum ausgegebenen
Text an: Dieser Text stellt den Hash hexadezimal dar.
Die Statusprüfung ist genauso wichtig wie die Zeichendarstellung. Eine Pipeline wie
cat missing.js | openssl … kann den Hash eines leeren Streams berechnen, nachdem
cat fehlgeschlagen ist. Hier verhindern die Bash-Option
pipefail und die geprüfte Zuweisung, dass bei fehlgeschlagenen Lesevorgängen
oder OpenSSL-Befehlen ein verwendbarer Wert ausgegeben wird. Die Eingabeumleitung verhindert zudem,
dass Dateinamen, die mit einem Bindestrich beginnen, als OpenSSL-Optionen interpretiert werden.
Eine lesbare leere Datei ist eine gültige Eingabe und hat einen eigenen Hash; eine fehlende Datei
ist ein Fehler.
Führen Sie das Skript direkt mit Bash aus:
bash sri.sh ./demo.js
Die Ausgabe beginnt mit sha384-, gefolgt von 64 base64-Zeichen. Leerraum und
Zeilenenden in demo.js beeinflussen das Ergebnis. Berechnen Sie deshalb den
Hash genau der Bytes, die Sie ausliefern werden.
SRI in Ihr HTML oder JSX einbetten
Setzen Sie für ein klassisches Skript aus einem anderen Ursprung sowohl
integrity als auch crossorigin="anonymous". Der Asset-Server muss
außerdem einen passenden Header Access-Control-Allow-Origin senden. Fehlt eine der beiden
Voraussetzungen, kann das Skript selbst bei übereinstimmenden Bytes blockiert werden.
MDN erläutert die CORS-Anforderung.
In JSX heißt das Attribut crossOrigin.
Speichern Sie dies als server.ts. Es liefert eine Seite und ihr Skript auf zwei
verschiedenen Loopback-Ports aus, sodass der Browser sie als unterschiedliche Ursprünge behandelt.
Das Betriebssystem wählt verfügbare Ports aus. Der Hash stammt aus der Shell, während die geänderte
Antwort bewusst den ursprünglich erwarteten Hash beibehält.
import type { Server } from 'node:http'
import { once } from 'node:events'
import { readFile } from 'node:fs/promises'
import { createServer } from 'node:http'
async function listen(server: Server, port = 0): Promise<string> {
server.listen(port, '127.0.0.1')
await once(server, 'listening')
const address = server.address()
if (!address || typeof address === 'string') throw new Error('Missing listening address')
return `http://127.0.0.1:${address.port}`
}
async function main(): Promise<void> {
const integrity = process.env.SRI
if (!integrity || !/^sha384-[A-Za-z0-9+/]{64}$/.test(integrity)) {
throw new Error('Set SRI to the trusted value from sri.sh')
}
const pagePort = Number(process.env.PAGE_PORT ?? 0)
if (!Number.isInteger(pagePort) || pagePort < 0 || pagePort > 65535) {
throw new Error('PAGE_PORT must be an integer from 0 to 65535')
}
const trusted = await readFile('./demo.js')
const changed = Buffer.concat([trusted, Buffer.from('\n// Changed after release\n')])
const assets = createServer((request, response) => {
response.setHeader('Cache-Control', 'no-store')
if (!['/demo.js', '/changed.js', '/no-cors.js'].includes(request.url ?? '')) {
response.writeHead(404).end()
return
}
if (request.url !== '/no-cors.js') response.setHeader('Access-Control-Allow-Origin', '*')
response.setHeader('Content-Type', 'text/javascript; charset=utf-8')
response.end(request.url === '/changed.js' ? changed : trusted)
})
const assetOrigin = await listen(assets)
const pages = createServer((request, response) => {
response.setHeader('Cache-Control', 'no-store')
if (request.method === 'POST' && request.url === '/api/security-alerts') {
request.resume()
console.log('Local SRI alert received')
response.writeHead(204).end()
return
}
const file = request.url === '/changed' ? 'changed.js' : request.url === '/no-cors' ? 'no-cors.js' : 'demo.js'
const cors = request.url === '/no-attribute' ? '' : 'crossorigin="anonymous"'
response.setHeader('Content-Type', 'text/html; charset=utf-8')
response.end(`<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SRI demo</title>
<h1>SRI demo</h1>
<nav aria-label="Test cases">
<a href="/">Trusted</a> | <a href="/changed">Changed bytes</a> |
<a href="/no-cors">Missing CORS header</a> | <a href="/no-attribute">Missing crossorigin</a>
</nav>
<p id="status" role="status">Waiting for script</p>
<script src="${assetOrigin}/${file}" integrity="${integrity}" ${cors}></script>
</html>`)
})
const pageOrigin = await listen(pages, pagePort)
console.log(`Open ${pageOrigin}`)
}
main().catch((error: unknown) => {
console.error('SRI demo failed:', error instanceof Error ? error.message : 'Unknown error')
process.exit(1)
})
Starten Sie es aus dem Verzeichnis, das alle drei Dateien enthält. Der Operator
&& verhindert den Start, wenn die Hash-Berechnung fehlschlägt. Keiner
dieser Befehle schreibt eine Ausgabedatei oder überschreibt Ihre Beispiele:
expected_sri=$(bash sri.sh ./demo.js) &&
SRI="$expected_sri" node server.ts
Öffnen Sie die ausgegebene URL in Ihrem Browser. Die Seite Trusted zeigt Trusted script executed an. Bei jedem der anderen Links bleibt Waiting for script auf dem Bildschirm stehen:
- Changed bytes liefert einen zusätzlichen Kommentar aus. Selbst diese harmlose Änderung verursacht eine Hash-Abweichung und verhindert die Ausführung des gesamten Skripts.
- Missing CORS header liefert die ursprünglichen Bytes ohne die Erlaubnis des Asset-Servers, sie ursprungsübergreifend zu lesen.
- Missing crossorigin lässt das CORS-Attribut des Elements weg, während der Asset-Server weiterhin seinen Berechtigungsheader sendet.
Öffnen Sie die Browserkonsole, um eine Integritätsabweichung von einem CORS-Fehler zu unterscheiden.
Eine erfolgreiche HTTP-Antwort allein belegt nicht, dass das Skript erfolgreich ausgeführt wurde.
Stoppen Sie beide Server mit Strg+C. Das Beispiel liest demo.js einmal beim
Start und speichert nichts. Sein Warnungsendpunkt ist nur ein lokaler Empfänger für das optionale
Monitoring-Beispiel weiter unten. Verwenden Sie HTTPS für Ihre tatsächliche Seite und Ihr CDN.
SRI schützt kein HTML, das über eine Verbindung ausgeliefert wird, deren Inhalte ein Angreifer
umschreiben kann.
Hashes im Browser mit der Web Crypto API erzeugen
Fügen Sie diese Funktion zur Diagnose in die Entwicklerkonsole der Beispielseite ein. Sie berechnet den Hash der abgerufenen Bytes und weist HTTP-Fehler ab, bevor eine Fehlerseite als Skript gelesen wird:
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)
const hashArray = Array.from(new Uint8Array(hashBuffer))
const binaryString = String.fromCharCode.apply(null, hashArray)
return `sha384-${btoa(binaryString)}`
}
Führen Sie auf der Seite Trusted Folgendes aus:
const resource = document.querySelector('script[integrity]')
console.log(await generateSRIHash(resource.src) === resource.integrity)
Dies gibt true aus. Derselbe Vergleich auf der Seite
Changed bytes gibt false
aus. Behalten Sie den erwarteten Hash aus dem vertrauenswürdigen Release bei. Ersetzen Sie ihn nicht
durch einen beliebigen Wert, den diese Funktion abruft. Dieser separate Abruf kann nicht belegen,
welche Bytes eine frühere Skriptanfrage ausgeführt hat. Das Attribut
integrity des Elements erzwingt diese Prüfung.
crypto.subtle.digest()
benötigt einen sicheren Kontext, etwa HTTPS oder dieses Loopback-Beispiel. Es puffert die gesamte
Ressource im Arbeitsspeicher. Die optionalen JavaScript-Hilfsfunktionen benötigen außerdem
AbortSignal.timeout().
Damit wird jeder Abruf einschließlich des Lesens seines Antwortinhalts auf zehn Sekunden aktiver
Zeit begrenzt. Dieses Zeitlimit kann pausieren, wenn ein Dokument angehalten wird. Prüfen Sie diese
APIs getrennt von der SRI-Unterstützung, wenn Sie ältere Browser unterstützen möchten. Browser ohne
SRI-Unterstützung erzwingen die Prüfung durch das Attribut nicht.
Skripte dynamisch laden
Setzen Sie bei einem Skript, das nach dem Laden der Seite hinzugefügt wird, die Integritäts- und CORS-Eigenschaften vor dem Einfügen. Fügen Sie diese Hilfsfunktion in dieselbe Konsole ein oder nehmen Sie sie in das JavaScript Ihrer Anwendung auf:
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)
})
}
Ausweichlösungen implementieren
Verwenden Sie einen byteidentischen, vertrauenswürdigen Spiegelserver und behalten Sie für die Ausweichlösung denselben erwarteten Hash bei. Dies versucht den Ersatz einmal und gibt dessen Fehler weiter:
async function loadWithFallback(primary, backup, integrity) {
try {
await loadScript(primary, integrity)
} catch {
console.warn(`Primary failed, switching to ${backup}`)
await loadScript(backup, integrity)
}
}
Wenn beide Hilfsfunktionen definiert sind und resource aus dem obigen Vergleich
vorliegt, führt dies zuerst die fehlgeschlagene primäre Anfrage aus. Anschließend wird das
ursprüngliche Asset des Beispiels geladen und geprüft:
await loadWithFallback(
new URL('/changed.js', resource.src).href,
new URL('/demo.js', resource.src).href,
resource.integrity,
)
CDN-Assets im Produktivbetrieb überwachen
Regelmäßige Prüfungen können Änderungen gegenüber einem vertrauenswürdigen Hash melden. Browser-Tabs
können jedoch geschlossen oder angehalten werden. Verwenden Sie für die betriebliche Überwachung
einen unabhängigen, zeitgesteuerten Job. Für die Diagnose im Browser lässt diese Hilfsfunktion jeweils
nur eine Abfrage zu, isoliert Fehler je Ressource, prüft den HTTP-Status von Warnungsmeldungen und
ermöglicht das Stoppen und Neustarten der Abfragen. stop() verhindert
künftige Abfragen; eine aktive Abfrage wird abgeschlossen.
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}`)
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
}
}
Nachdem Sie generateSRIHash, resource und die Klasse definiert
haben, probieren Sie Folgendes in der Konsole des Beispiels aus:
const monitor = new SRIMonitor(1000)
monitor.add(new URL('/changed.js', resource.src).href, resource.integrity)
monitor.start()
// Run monitor.stop() when finished.
Der Browser protokolliert eine Abweichung, und der Server gibt Local SRI alert received aus. Der Loopback-Empfänger bestätigt Warnungen und verwirft sie. Er bietet weder Authentifizierung noch Speicherung oder externe Benachrichtigungen. Stellen Sie für den Produktivbetrieb einen authentifizierten Endpunkt mit Ratenbegrenzung bereit und vermeiden Sie URLs mit Zugangsdaten in Berichten. Ein Netzwerk- oder CORS-Fehler ist eine fehlgeschlagene Prüfung, kein Beleg für geänderte Inhalte.
SRI in Ihrer CI/CD-Pipeline automatisieren
Erzeugen Sie Hashes nach dem letzten Minifizierungsschritt Ihres vertrauenswürdigen Builds und vor
dem Rendern des HTML. Führen Sie sri.sh für jedes vorgesehene Skript oder
Stylesheet aus und lassen Sie den Build bei jedem Status ungleich null fehlschlagen. Lassen Sie ihn
auch fehlschlagen, wenn die erwartete Asset-Liste leer ist. Speichern Sie jeden Dateinamen und
SRI-Wert in Ihrem Build-Manifest. Stellen Sie dann die Assets dieses Manifests und das gerenderte HTML
gemeinsam bereit. Diese Integration hängt von Ihrem Build-System ab. Das lokale Beispiel oben
installiert keinen CI-Workflow.
Bevorzugen Sie versionierte Asset-URLs gegenüber veränderlichen Aliasen wie
latest. Ein beabsichtigtes Update einer Abhängigkeit erfordert die Prüfung
des neuen Releases, gefolgt von einer neuen Asset-URL und ihrem neuen erwarteten Hash. Lassen Sie den
erwarteten Wert nicht automatisch aktualisieren, wenn eine Integritätsprüfung fehlschlägt.
Häufige Fehler beheben
| Symptom | Was Sie prüfen sollten |
|---|---|
| Hash-Befehl schlägt fehl | Prüfen Sie Pfad, Berechtigungen und OpenSSL-Installation. Fehlende Eingaben dürfen nicht zum Hash einer leeren Datei führen. |
| Browser meldet eine Integritätsabweichung | Vergleichen Sie die Antwort mit dem freigegebenen Release oder Build-Artefakt. Untersuchen Sie unerwartete Änderungen. Aktualisieren Sie den Hash erst, nachdem Sie die neuen Bytes freigegeben haben. |
| Lokaler Hash weicht von CDN-Bytes ab | Prüfen Sie Minifizierung, eingefügte Banner und Zeilenenden. Übliche HTTP-Komprimierung wird vor der Integritätsprüfung decodiert. |
| Browser meldet einen CORS-Fehler | Behalten Sie crossorigin="anonymous" bei und konfigurieren Sie Access-Control-Allow-Origin beim CDN für Ihre Seite oder * für öffentliche, anonym zugängliche Assets. |
| Skript läuft ohne Schutz | Prüfen Sie am tatsächlichen Element, ob gültige Integritätsmetadaten vorhanden sind, und bestätigen Sie die Browserunterstützung. Leere oder nicht unterstützte Metadaten ermöglichen die vorgesehene Prüfung nicht. |
