Wenn du eine Datei von einem externen Server lädst, vertraust du darauf, dass der Inhalt, den du anforderst, auch tatsächlich der ist, den du erwartest. Da du den Server nicht selbst verwaltest, bist du auf die Sicherheit einer weiteren Drittpartei angewiesen und erhöhst die Angriffsfläche. Dem Vertrauen in eine Drittpartei ist per se nichts Falsches, aber es sollte sicherlich im Kontext der Sicherheit deiner Website berücksichtigt werden.
Ein reales Beispiel
Dies ist keine rein theoretische Gefahr. Das Ignorieren potenzieller Sicherheitsprobleme kann bereits zu schwerwiegenden Folgen geführt haben und hat dies auch getan. Am 4. Juni 2019 gab Malwarebytes die Entdeckung eines bösartigen Skimmers auf der Website NBA.com bekannt. Aufgrund eines kompromittierten Amazon S3 Buckets konnten Angreifer eine JavaScript-Bibliothek manipulieren, um Kreditkarteninformationen von Kunden zu stehlen.
Nicht nur JavaScript ist Anlass zur Sorge. CSS ist ebenfalls eine Ressource, die gefährliche Aktionen wie den Diebstahl von Passwörtern ausführen kann, und es braucht nur einen einzigen kompromittierten Drittanbieterserver, um eine Katastrophe auszulösen. Aber sie können unschätzbare Dienste leisten, auf die wir nicht einfach verzichten können, wie z. B. CDNs, die die Bandbreitennutzung einer Website reduzieren und Dateien aufgrund von standortbasiertem Caching viel schneller für den Endbenutzer bereitstellen. Es steht also fest, dass wir uns manchmal auf einen Host verlassen müssen, über den wir keine Kontrolle haben, aber wir müssen auch sicherstellen, dass der Inhalt, den wir von ihm erhalten, sicher ist. Was können wir tun?
Lösung: Subresource Integrity (SRI)
SRI ist eine Sicherheitsrichtlinie, die das Laden von Ressourcen verhindert, die nicht mit einem erwarteten Hash übereinstimmen. Dadurch wird, wenn ein Angreifer Zugriff auf eine Datei erlangt und deren Inhalt mit bösartigem Code verändert, der Hash nicht mit dem erwarteten übereinstimmen und der Code gar nicht erst ausgeführt.
Schützt HTTPS nicht schon davor?
HTTPS ist großartig für die Sicherheit und ein Muss für jede Website. Obwohl es ähnliche Probleme (und viel mehr) verhindert, schützt es nur vor Manipulationen während der Übertragung. Wenn eine Datei auf dem Host selbst manipuliert würde, würde die bösartige Datei trotzdem über HTTPS gesendet werden, was den Angriff nicht verhindert.
Wie funktioniert Hashing?
Eine Hashing-Funktion nimmt Daten beliebiger Größe als Eingabe und gibt Daten fester Größe als Ausgabe zurück. Hashing-Funktionen hätten idealerweise eine gleichmäßige Verteilung. Das bedeutet, dass für jede Eingabe, x, die Wahrscheinlichkeit, dass die Ausgabe, y, irgendein spezifischer möglicher Wert ist, ähnlich der Wahrscheinlichkeit ist, dass sie irgendein anderer Wert im Bereich der Ausgaben ist.
Hier ist eine Metapher
Stell dir vor, du hast einen 6-seitigen Würfel und eine Liste von Namen. Die Namen wären in diesem Fall die „Eingabe“ der Hash-Funktion und die gewürfelte Zahl die „Ausgabe“ der Funktion. Für jeden Namen in der Liste würfelst du und notierst dir, welchem Namen jede Zahl entspricht, indem du die Zahl neben den Namen schreibst. Wenn ein Name mehr als einmal als Eingabe verwendet wird, ist die entsprechende Ausgabe immer die gleiche wie beim ersten Mal. Für den ersten Namen, Alice, würfelst du 4. Für den nächsten, John, würfelst du 6. Dann erhältst du für Bob, Mary, William, Susan und Joseph 2, 2, 5, 1 und 1. Wenn du „John“ erneut als Eingabe verwendest, ist die Ausgabe wieder 6. Diese Metapher beschreibt im Wesentlichen, wie Hash-Funktionen funktionieren.
| Name (Eingabe) | Gewürfelte Zahl (Ausgabe) |
|---|---|
| Alice | 4 |
| John | 6 |
| Bob | 2 |
| Mary | 2 |
| William | 5 |
| Susan | 1 |
| Joseph | 1 |
Du hast vielleicht bemerkt, dass z. B. Bob und Mary die gleiche Ausgabe haben. Bei Hash-Funktionen nennt man das eine „Kollision“. Für unser Beispiel kommt es unweigerlich vor. Da wir sieben Namen als Eingaben und nur sechs mögliche Ausgaben haben, ist mindestens eine Kollision garantiert.
Ein bemerkenswerter Unterschied zwischen diesem Beispiel und einer Hash-Funktion in der Praxis ist, dass praktische Hash-Funktionen typischerweise deterministisch sind, d. h. sie verwenden keine Zufälligkeit wie unser Beispiel. Vielmehr ordnet sie Eingaben vorhersagbar Ausgaben zu, sodass jede Eingabe gleich wahrscheinlich jeder bestimmten Ausgabe zugeordnet wird.
SRI verwendet eine Familie von Hash-Funktionen namens Secure Hash Algorithm (SHA). Dies ist eine Familie kryptografischer Hash-Funktionen, die Varianten mit 128, 256, 384 und 512 Bit umfasst. Eine kryptografische Hash-Funktion ist eine spezifischere Art von Hash-Funktion mit den Eigenschaften, dass sie praktisch unumkehrbar ist, um die ursprüngliche Eingabe zu finden (ohne bereits die entsprechende Eingabe zu haben oder Brute-Force anzuwenden), kollisionsresistent ist und so konzipiert ist, dass eine kleine Änderung der Eingabe die gesamte Ausgabe verändert. SRI unterstützt die 256-, 384- und 512-Bit-Varianten der SHA-Familie.
Hier ist ein Beispiel mit SHA-256
Zum Beispiel ist die Ausgabe für hello
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
Und die Ausgabe für hell0 (mit einer Null anstelle eines O) ist
bdeddd433637173928fe7202b663157c9e1881c3e4da1d45e8fff8fb944a4868
Du wirst bemerken, dass die kleinste Änderung an der Eingabe eine Ausgabe erzeugt, die völlig anders ist. Das ist eine der Eigenschaften kryptografischer Hashes, die zuvor aufgeführt wurden.
Das Format, das du am häufigsten für Hashes sehen wirst, ist Hexadezimal, das alle Dezimalziffern (0-9) und die Buchstaben A bis F umfasst. Einer der Vorteile dieses Formats ist, dass jedes Zeichenpaar ein Byte darstellt und die Gleichmäßigkeit für Zwecke wie die Farbfomattierung nützlich sein kann, bei der ein Byte jede Farbe darstellt. Das bedeutet, dass eine Farbe ohne Alpha-Kanal mit nur sechs Zeichen dargestellt werden kann (z. B. Rot = ff0000)
Diese Platzersparnis ist auch der Grund, warum wir Hashing anstelle des direkten Vergleichs der gesamten Datei mit den erwarteten Daten jedes Mal verwenden. Obwohl 256 Bit nicht alle Daten einer Datei, die größer als 256 Bit ist, ohne Kompression darstellen können, sorgt die Kollisionsresistenz von SHA-256 (und 384, 512) dafür, dass es praktisch unmöglich ist, zwei Hashes für unterschiedliche Eingaben zu finden, die übereinstimmen. Und was SHA-1 betrifft, so ist es nicht mehr sicher, da eine Kollision gefunden wurde.
Interessanterweise ist die Attraktivität der Kompaktheit wahrscheinlich einer der Gründe, warum SRI-Hashes *kein* hexadezimales Format verwenden, sondern stattdessen Base64. Das mag auf den ersten Blick eine seltsame Entscheidung sein, aber wenn wir bedenken, dass diese Hashes im Code enthalten sein werden und Base64 die gleiche Datenmenge wie Hexadezimal vermitteln kann, während es 33 % kürzer ist, ergibt das Sinn. Ein einzelnes Zeichen von Base64 kann 64 verschiedene Zustände annehmen, was 6 Bit Daten entspricht, während Hexadezimal nur 16 Zustände oder 4 Bit Daten darstellen kann. Wenn wir also beispielsweise 32 Byte Daten (256 Bit) darstellen wollen, bräuchten wir 64 Zeichen in Hexadezimal, aber nur 44 Zeichen in Base64. Wenn wir längere Hashes wie sha384/512 verwenden, spart Base64 viel Platz.
Warum funktioniert Hashing für SRI?
Stellen wir uns vor, es gäbe eine JavaScript-Datei, die auf einem Drittanbieterserver gehostet wird, den wir in unsere Webseite eingebunden haben und für die wir Subresource Integrity aktiviert haben. Wenn nun ein Angreifer die Daten der Datei mit bösartigem Code modifiziert, würde der Hash nicht mehr mit dem erwarteten Hash übereinstimmen und die Datei würde nicht ausgeführt werden. Erinnern wir uns daran, dass jede kleine Änderung an einer Datei ihren entsprechenden SHA-Hash vollständig verändert und dass Hash-Kollisionen mit SHA-256 und höher zum Zeitpunkt der Erstellung dieses Textes praktisch unmöglich sind.
Unser erster SRI-Hash
Es gibt also ein paar Methoden, mit denen du den SRI-Hash einer Datei berechnen kannst. Eine Methode (und vielleicht die einfachste) ist die Verwendung von srihash.org, aber wenn du eine programmatischere Methode bevorzugst, kannst du Folgendes verwenden:
sha384sum [filename here] | head -c 96 | xxd -r -p | base64
sha384sumBerechnet den SHA-384-Hash einer Dateihead -c 96Schneidet alle bis auf die ersten 96 Zeichen des Strings ab, der an es übergeben wird-c 96Gibt an, alle bis auf die ersten 96 Zeichen abzuschneiden. Wir verwenden96, da dies die Zeichenlänge eines SHA-384-Hashs im hexadezimalen Format ist
xxd -r -pNimmt Hex-Eingaben, die an es übergeben werden, und konvertiert sie in Binärform-rWeistxxdan, Hex-Eingaben zu empfangen und in Binärform zu konvertieren-pEntfernt die zusätzliche Ausgabeformatierung
base64Konvertiert einfach die Binärausgabe vonxxdin Base64
Wenn du dich entscheidest, diese Methode zu verwenden, sieh dir die folgende Tabelle an, um die Längen jedes SHA-Hashs zu sehen.
| Hash-Algorithmus | Bits | Bytes | Hex-Zeichen |
|---|---|---|---|
| SHA-256 | 256 | 32 | 64 |
| SHA-384 | 384 | 48 | 96 |
| SHA-512 | 512 | 64 | 128 |
Für den Befehl head -c [x] ist x die Anzahl der Hex-Zeichen für den entsprechenden Algorithmus.
MDN erwähnt auch einen Befehl zur Berechnung des SRI-Hashs
shasum -b -a 384 FILENAME.js | awk '{ print $1 }' | xxd -r -p | base64
awk '{print $1}' Findet den ersten Teil eines Strings (getrennt durch Tab oder Leerzeichen) und übergibt ihn an xxd. $1 repräsentiert das erste Segment des übergebenen Strings.
Und wenn du Windows verwendest
@echo off
set bits=384
openssl dgst -sha%bits% -binary %1% | openssl base64 -A > tmp
set /p a= < tmp
del tmp
echo sha%bits%-%a%
pause
@echo offverhindert, dass die ausgeführten Befehle angezeigt werden. Dies ist besonders hilfreich, um sicherzustellen, dass das Terminal nicht überladen wird.set bits=384setzt eine Variable namensbitsauf 384. Diese wird später im Skript verwendet.openssl dgst -sha%bits% -binary %1% | openssl base64 -A > tmpist komplexer, also zerlegen wir sie in Teile.openssl dgstberechnet eine Digest einer Eingabedatei.-sha%bits%verwendet die Variablebitsund kombiniert sie mit dem Rest des Strings zu einem der möglichen Flag-Werte:sha256,sha384odersha512.-binarygibt den Hash als Binärdaten aus, anstatt als String-Format, wie z. B. Hexadezimal.%1%ist das erste Argument, das dem Skript beim Ausführen übergeben wird.- Der erste Teil des Befehls hasht die Datei, die dem Skript als Argument übergeben wurde.
| openssl base64 -A > tmpkonvertiert die Binärausgabe, die durch sie geleitet wird, in Base64 und schreibt sie in eine Datei namenstmp.-Agibt die Base64-Ausgabe in einer einzigen Zeile aus.set /p a= <tmpspeichert den Inhalt der Dateitmpin einer Variablea.del tmplöscht die Dateitmp.echo sha%bits%-%a%gibt den Typ des SHA-Hashs zusammen mit der Base64-Darstellung der Eingabedatei aus.pauseVerhindert, dass das Terminal geschlossen wird.
SRI in Aktion
Nachdem wir nun verstehen, wie Hashing und SRI-Hashes funktionieren, versuchen wir es mit einem konkreten Beispiel. Wir erstellen zwei Dateien
// file1.js
alert('Hello, world!');
und
// file2.js
alert('Hi, world!');
Dann berechnen wir die SHA-384 SRI-Hashes für beide
| Dateiname | SHA-384 Hash (Base64) |
|---|---|
file1.js | 3frxDlOvLa6GGEUwMh9AowcepHRx/rwFT9VW9yL1wv/OcerR39FEfAUHZRrqaOy2 |
file2.js | htr1LmWx3PQJIPw5bM9kZKq/FL0jMBuJDxhwdsMHULKybAG5dGURvJIXR9bh5xJ9 |
Dann erstellen wir eine Datei namens index.html
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="./file1.js" crossorigin="anonymous"></script>
<script type="text/javascript" src="./file2.js" crossorigin="anonymous"></script>
</head>
</html>
Lege alle diese Dateien im selben Ordner ab und starte einen Server innerhalb dieses Ordners (führe z. B. npx http-server im Ordner mit den Dateien aus und öffne dann eine der Adressen, die von http-server oder einem Server deiner Wahl bereitgestellt werden, z. B. 127.0.0.1:8080). Du solltest zwei Alert-Dialogfelder sehen. Das erste sollte "Hello, world!" und das zweite "Hi, world!" anzeigen.
Wenn du den Inhalt der Skripte änderst, wirst du feststellen, dass sie nicht mehr ausgeführt werden. Das ist Subresource Integrity in Aktion. Der Browser erkennt, dass der Hash der angeforderten Datei nicht mit dem erwarteten Hash übereinstimmt und weigert sich, ihn auszuführen.
Wir können auch mehrere Hashes für eine Ressource einfügen, und der stärkste Hash wird gewählt, so
<!DOCTYPE html>
<html>
<head>
<script
type="text/javascript"
src="./file1.js"
crossorigin="anonymous"></script>
<script
type="text/javascript"
src="./file2.js"
crossorigin="anonymous"></script>
</head>
</html>
Der Browser wählt den Hash, der als der stärkste gilt, und vergleicht den Hash der Datei damit.
Warum gibt es das Attribut „crossorigin“?
Das Attribut crossorigin teilt dem Browser mit, wann die Benutzeranmeldedaten mit der Anfrage für die Ressource gesendet werden sollen. Es gibt zwei Optionen zur Auswahl:
Wert (crossorigin=) | Beschreibung |
|---|---|
anonym | Die Anfrage hat den Anmeldemodus auf same-origin und den Modus auf cors eingestellt. |
use-credentials | Die Anfrage hat den Anmeldemodus auf include und den Modus auf cors eingestellt. |
Erwähnte Anmeldedatenmodi der Anfrage
| Anmeldemodus | Beschreibung |
|---|---|
same-origin | Anmeldedaten werden bei Anfragen an Same-Origin-Domains gesendet und Anmeldedaten, die von Same-Origin-Domains gesendet werden, werden verwendet. |
include | Anmeldedaten werden auch an Cross-Origin-Domains gesendet und von Cross-Origin-Domains gesendete Anmeldedaten werden verwendet. |
Erwähnte Anfrage-Modi
| Anfrage-Modus | Beschreibung |
|---|---|
cors | Die Anfrage wird eine CORS-Anfrage sein, die erfordert, dass der Server eine definierte CORS-Richtlinie hat. Wenn nicht, wird die Anfrage einen Fehler auslösen. |
Warum ist das Attribut „crossorigin“ für Subresource Integrity erforderlich?
Standardmäßig können Skripte und Stylesheets Cross-Origin geladen werden, und da Subresource Integrity das Laden einer Datei verhindert, wenn der Hash der geladenen Ressource nicht mit dem erwarteten Hash übereinstimmt, könnte ein Angreifer Cross-Origin-Ressourcen massenhaft laden und testen, ob das Laden mit bestimmten Hashes fehlschlägt, und dadurch Informationen über einen Benutzer ableiten, die er sonst nicht erhalten könnte.
Wenn du das Attribut crossorigin einfügst, muss die Cross-Origin-Domain entscheiden, ob sie Anfragen von dem Ursprung erlaubt, von dem die Anfrage gesendet wird, damit die Anfrage erfolgreich ist. Dies verhindert Cross-Origin-Angriffe mit Subresource Integrity.
Subresource Integrity mit Webpack verwenden
Es klingt wahrscheinlich nach viel Arbeit, die SRI-Hashes jeder Datei jedes Mal neu zu berechnen, wenn sie aktualisiert werden, aber glücklicherweise gibt es eine Möglichkeit, dies zu automatisieren. Lass uns ein Beispiel gemeinsam durchgehen. Du benötigst ein paar Dinge, bevor du anfängst.
Node.js und npm
Node.js ist eine JavaScript-Laufzeitumgebung, die uns zusammen mit npm (seinem Paketmanager) die Verwendung von webpack ermöglicht. Um es zu installieren, besuche die Node.js-Website und wähle den Download, der deinem Betriebssystem entspricht.
Projekt einrichten
Erstelle einen Ordner und gib ihm einen beliebigen Namen mit mkdir [Name des Ordners]. Gib dann cd [Name des Ordners] ein, um dorthin zu wechseln. Jetzt müssen wir das Verzeichnis als Node-Projekt einrichten. Gib also npm init ein. Es werden dir ein paar Fragen gestellt, aber du kannst sie mit Enter überspringen, da sie für unser Beispiel nicht relevant sind.
webpack
webpack ist eine Bibliothek, die es dir ermöglicht, deine Dateien automatisch zu einer oder mehreren Bündeln zu kombinieren. Mit webpack müssen wir die Hashes nicht mehr manuell aktualisieren. Stattdessen fügt webpack die Ressourcen mit den Attributen integrity und crossorigin in das HTML ein.
webpack installieren
Du musst webpack und webpack-cli installieren
npm i --save-dev webpack webpack-cli
Der Unterschied zwischen den beiden ist, dass webpack die Kernfunktionalitäten enthält, während webpack-cli für die Kommandozeilenschnittstelle gedacht ist.
Wir werden unsere package.json bearbeiten, um einen scripts-Abschnitt hinzuzufügen, wie folgt:
{
//... rest of package.json ...,
"scripts": {
"dev": "webpack --mode=development"
}
//... rest of package.json ...,
}
Dies ermöglicht uns, npm run dev auszuführen und unser Bundle zu erstellen.
Webpack-Konfiguration einrichten
Als Nächstes richten wir die webpack-Konfiguration ein. Dies ist notwendig, um webpack mitzuteilen, mit welchen Dateien es arbeiten soll und wie.
Zuerst müssen wir zwei Pakete installieren: html-webpack-plugin und webpack-subresource-integrity
npm i --save-dev html-webpack-plugin webpack-subresource-integrity style-loader css-loader
| Paketname | Beschreibung |
|---|---|
| html-webpack-plugin | Erstellt eine HTML-Datei, in die Ressourcen eingefügt werden können |
| webpack-subresource-integrity | Berechnet und fügt Subresource-Integrity-Informationen in Ressourcen wie <script> und <link rel=…> ein |
| style-loader | Wendet die CSS-Styles an, die wir importieren |
| css-loader | Ermöglicht es uns, CSS-Dateien in unseren JavaScript-Code zu importieren |
Konfiguration einrichten
const path = require('path'),
HTMLWebpackPlugin = require('html-webpack-plugin'),
SriPlugin = require('webpack-subresource-integrity');
module.exports = {
output: {
// The output file's name
filename: 'bundle.js',
// Where the output file will be placed. Resolves to
// the "dist" folder in the directory of the project
path: path.resolve(__dirname, 'dist'),
// Configures the "crossorigin" attribute for resources
// with subresource integrity injected
crossOriginLoading: 'anonymous'
},
// Used for configuring how various modules (files that
// are imported) will be treated
modules: {
// Configures how specific module types are handled
rules: [
{
// Regular expression to test for the file extension.
// These loaders will only be activated if they match
// this expression.
test: /\.css$/,
// An array of loaders that will be applied to the file
use: ['style-loader', 'css-loader'],
// Prevents the accidental loading of files within the
// "node_modules" folder
exclude: /node_modules/
}
]
},
// webpack plugins alter the function of webpack itself
plugins: [
// Plugin that will inject integrity hashes into index.html
new SriPlugin({
// The hash functions used (e.g.
// <script ...
hashFuncNames: ['sha384']
}),
// Creates an HTML file along with the bundle. We will
// inject the subresource integrity information into
// the resources using webpack-subresource-integrity
new HTMLWebpackPlugin({
// The file that will be injected into. We can use
// EJS templating within this file, too
template: path.resolve(__dirname, 'src', 'index.ejs'),
// Whether or not to insert scripts and other resources
// into the file dynamically. For our example, we will
// enable this.
inject: true
})
]
};
Vorlage erstellen
Wir müssen eine Vorlage erstellen, um webpack mitzuteilen, in welche Datei das Bundle und die Subresource-Integrity-Informationen eingefügt werden sollen. Erstelle eine Datei namens index.ejs
<!DOCTYPE html>
<html>
<body></body>
</html>
Erstelle nun eine index.js im Ordner mit dem folgenden Skript
// Imports the CSS stylesheet
import './styles.css'
alert('Hello, world!');
Bundle erstellen
Gib npm run build im Terminal ein. Du wirst feststellen, dass ein Ordner namens dist erstellt wird und darin eine Datei namens index.html, die ungefähr so aussieht:
<!DOCTYPE HTML>
<html><head><script defer src="bundle.js" crossorigin="anonymous">
</script></head>
<body>
</body>
</html>
Die CSS wird als Teil der Datei bundle.js enthalten sein.
Dies funktioniert nicht für Dateien, die von externen Servern geladen werden, und sollte es auch nicht, da Cross-Origin-Dateien, die ständig aktualisiert werden müssen, mit aktivierter Subresource Integrity nicht funktionieren würden.
Danke fürs Lesen!
Das war's für dieses Mal. Subresource Integrity ist eine einfache und effektive Ergänzung, um sicherzustellen, dass du nur das lädst, was du erwartest, und deine Benutzer schützt. Und denk daran: Sicherheit ist mehr als nur eine Lösung. Sei also immer auf der Suche nach weiteren Möglichkeiten, deine Website sicher zu halten.
Wenn du Node verwendest, brauchst du nur das eingebaute Modul "crypto" zum Generieren der Hashes, z. B.: https://gist.github.com/cecilemuller/8477130a4fbb427fb341f67af128ab5a
Das Generieren eines SRI-Hashs ist mit einem Tool wie https://www.srihash.org/ ganz einfach.
Danke für den Artikel!
Eine Frage jedoch
Aus einer JAMStack-Perspektive: Wenn der Server kompromittiert wird, so dass der Angreifer eine JavaScript-/CSS-Datei ändern kann. Sie werden auch die HTML-Datei ändern und den Hash aktualisieren können, richtig?
Sieht großartig aus! Aber leider viel zu wenig Dokumentation, um es mit Webpack einrichten zu können. Hatte nur falsche Digests für mich generiert.