Abhängigkeiten: fremder Code, den dein Code ausführt
Kurz gesagt: Abhängigkeiten sind fremder Code, den dein Programm ausführt; die Lock-Datei und der Hash des Pakets schützen vor Manipulation, und ein Scanner findet bekannte Schwachstellen.
Problem
Das Paket, das niemand gewählt hat
Die package.json des Shops listet 14 Pakete. node_modules hat 900. Jedes der 14 bringt eigene Abhängigkeiten mit, und die bringen ihre eigenen mit.
In einer Nacht kommt jemand in das Konto des Autors eines kleinen Pakets zum Formatieren von Datumsangaben. Das Paket liegt tief im Baum: Der Shop benutzt es über die Bibliothek für PDF-Rechnungen, und niemand im Team weiß, dass es da ist. Der Angreifer lädt eine neue Version mit einer zusätzlichen Zeile hoch: einer Zeile, die die Umgebung des Prozesses liest und PAYMENT_API_KEY nach draußen schickt. Am Morgen holt der Build des Shops die neue Version, weil der Bereich ^2.1.3 sie erlaubt.
Der Schlüssel, den du in der vorigen Lektion aus dem Code in die Umgebung verlegt hast, leakt von dort. Der Shop hat in seinem eigenen Code nichts falsch gemacht.
Das ist nicht ausgedacht. Es ist mit event-stream (2018) und mit ua-parser-js (2021) passiert, und 2024 wurde der Austausch in xz utils kurz entdeckt, bevor er Server auf der ganzen Welt erreichte.
Die Idee
Dein Prozess, fremder Code
Jedes Paket, das das Programm lädt, läuft in deinem Prozess und mit deinen Rechten. Es liest dieselbe Umgebung und dieselben Dateien und spricht mit demselben Netz. Manche Pakete führen schon bei der Installation Code aus (die postinstall-Skripte von npm, setup.py in alten Python-Paketen).
Die Pakete, die du angefordert hast, sind direkte Abhängigkeiten. Die, die sie mitbringen, sind transitiv. Die transitiven sind meist die Mehrheit, und niemand hat sie gewählt.
Das ist die Lieferkette (Supply Chain): vom Autor des Pakets über die Registry (npm, Maven Central, NuGet, PyPI) bis zu deinem Build. Ein Angreifer kann an jedem Glied einsteigen:
- ein gekapertes Konto eines Autors: eine neue Version mit schädlichem Code;
- Typosquatting: ein Paket mit dem Namen eines bekannten, aber einen Buchstaben daneben (
reqeusts,lodahs), das auf einen Tippfehler wartet; - ein Austausch unterwegs: ein Mirror, Cache oder Proxy, der unter demselben Namen und derselben Version andere Bytes liefert;
- ein verlassenes Paket, das jemand übernimmt und Monate später „aktualisiert“.
Gegen all das gibt es zwei einfache Regeln. Wisse genau, welche Versionen hereinkommen. Und prüfe, dass die Bytes dieselben sind, die du geprüft hast.
Sieh es dir an
Der Baum und das ausgetauschte Paket
static boolean verify(Tarball pkg, Map<String, String> lockFile) throws Exception { String expected = lockFile.get(pkg.name()); String actual = integrity(pkg.bytes()); if (!actual.equals(expected)) { System.out.println(pkg.name() + ": MANIPULIERT"); System.out.println(" lock: " + brief(expected)); System.out.println(" geladen: " + brief(actual)); return false; } System.out.println(pkg.name() + ": OK"); return true;}Schritt für Schritt, in Worten
- Der Shop fordert zwei Pakete an. Sie bringen drei weitere eigene mit. Alle fünf laufen im Shop.
- Jemand tauscht date-fmt unterwegs aus. Ohne Prüfung läuft sein Code im Shop, mit dessen Umgebung und Dateien.
- Die Lock-Datei enthält die genaue Version und den Hash jedes Pakets, auch der tiefen.
- Der Hash des Heruntergeladenen: SHA-512, in Base64 geschrieben.
- Derselbe Hash wie in der Lock-Datei: Das sind die Bytes, die geprüft wurden. Installiert.
- date-fmt hat eine Zeile mehr, und der ganze Hash ist anders. Es wird nicht installiert.
- Installiert: 4 von 5. Der Build hält an, und niemand führt den ausgetauschten Code aus.
Der Hash hier ist ein Verwandter des hashCode aus der Lektion über Hashcodes, aber er ist kryptografisch. SHA-512 ergibt 64 Bytes, und niemand kann ein anderes Paket mit demselben Fingerabdruck machen. Der Hashcode in den Collections hat 4 Bytes und wiederholt sich leicht: Er ist gut für Buckets in einer Tabelle, aber nicht, um fremden Code zu prüfen.
Die Idee
Die Lock-Datei: die genaue Version und ihr Fingerabdruck
Das Manifest sagt, was du willst: "pdf-invoice": "^5.0.0" heißt „5.0.0 oder ein neueres 5.x“. Die Lock-Datei sagt genau, was installiert ist: die Version jedes Pakets im Baum, auch der transitiven, und einen Hash seines Inhalts. Sie liegt in Git, und der Build installiert genau das.
Maven hat keine Lock-Datei. Dort gilt die Regel genauer Versionen ohne Bereiche (<version>5.0.2</version>, nicht [5.0,6.0)) und eines Ortes für die Versionen (<dependencyManagement> oder ein BOM). Gradle speichert Hashes der Pakete: gradle dependencies --write-locks schreibt gradle.lockfile mit den genauen Versionen, und --write-verification-metadata sha256 schreibt verification-metadata.xml mit dem Hash jedes Jars:
<component group="com.example" name="date-fmt" version="2.1.3"> <artifact name="date-fmt-2.1.3.jar"> <sha256 value="9f2c…41ab" origin="Generated by Gradle"/> </artifact></component>Die Prüfung ist einfach: ein Hash der heruntergeladenen Bytes und ein Vergleich mit dem Eintrag. Hier ist sie als Programm, mit einem Hash in der Form von npm, sha512- und Base64:
static String integrity(byte[] bytes) throws Exception { var sha512 = MessageDigest.getInstance("SHA-512"); byte[] digest = sha512.digest(bytes); return "sha512-" + Base64.getEncoder().encodeToString(digest);}
static String brief(String hash) { return hash.substring(0, 19) + "…";}static boolean verify(Tarball pkg, Map<String, String> lockFile) throws Exception { String expected = lockFile.get(pkg.name()); String actual = integrity(pkg.bytes()); if (!actual.equals(expected)) { System.out.println(pkg.name() + ": MANIPULIERT"); System.out.println(" lock: " + brief(expected)); System.out.println(" geladen: " + brief(actual)); return false; } System.out.println(pkg.name() + ": OK"); return true;}Map<String, String> lockFile = Map.of( "pdf-invoice@5.0.2", "sha512-IN0lH/YKriHbj87FMea7idgW4qdE8vgdeYTdEVVqz/GwAnMAzmU8X7TOO2TJ20CSQLaDtD9hKB7J5bw8JLos0A==", "font-kit@3.1.0", "sha512-19+g7wktnKQh+JGfMRNeNdrG3//S+BsOUeuBcAp8p10kuHCpM3HFgbQzKnzTQ+DRLcA2b1MmnHFfHYvjFrjm9g==", "date-fmt@2.1.3", "sha512-Jeb+bpiC7IqY/wWOY2a0umsIM6ce0XcMc8XlRr9udQktm8kZCNIV+qcJtad0Uip8hhhdn9P+8yE6p56qsZkZSw==", "pay-sdk@4.2.0", "sha512-qyQmG1zNAk4HmkwNd+yxZ0GkyOsQoLn9CGHMW2LsBWmkhwpMo7rXHYT2keUIu6n50HuYBjv411Twk69Wqc0nig==", "http-lite@1.8.0", "sha512-TI5SSzlpujK57oie/hoakeRXqVh5YuW22qwmU7I6moo75Gl3qXY5ZqRuEsy0sKdy6dz7uqhlJzDDliKF4tUQQA==");List<Tarball> downloaded = List.of( tarball("pdf-invoice@5.0.2", "pdf-invoice 5.0.2: renders invoices to PDF"), tarball("font-kit@3.1.0", "font-kit 3.1.0: loads and measures fonts"), tarball("date-fmt@2.1.3", "date-fmt 2.1.3: formats dates and times\n+ one line added by the attacker"), tarball("pay-sdk@4.2.0", "pay-sdk 4.2.0: talks to the payment provider"), tarball("http-lite@1.8.0", "http-lite 1.8.0: a small HTTP client"));int installed = 0;for (Tarball pkg : downloaded) { if (verify(pkg, lockFile)) installed++;}System.out.println("Installiert: " + installed + " von " + downloaded.size());pdf-invoice@5.0.2: OKfont-kit@3.1.0: OKdate-fmt@2.1.3: MANIPULIERT lock: sha512-Jeb+bpiC7IqY… geladen: sha512-sMR370edweyw…pay-sdk@4.2.0: OKhttp-lite@1.8.0: OKInstalliert: 4 von 5Die Pakete hier sind ein kurzer Text, aber für den Hash gibt es keinen Unterschied: Es sind Bytes. Eine zusätzliche Zeile in date-fmt, und der Hash ist völlig anders, schon ab dem ersten Zeichen.
Probier es aus
Sage die Ausgabe voraus
Die beiden Texte unterscheiden sich nur durch ein Leerzeichen am Ende. Was gibt das aus?
String a = integrity("total 49.90".getBytes(StandardCharsets.UTF_8));String b = integrity("total 49.90 ".getBytes(StandardCharsets.UTF_8));System.out.println(a.length());System.out.println(b.length());System.out.println(a.equals(b));SHA-512 ergibt immer 64 Bytes, und Base64 schreibt sie als 88 Zeichen; mit sha512- sind das 95, egal wie lang die Eingabe ist. Ein zusätzliches Leerzeichen ändert den ganzen Hash, also ist der Vergleich false.
Probier es aus
Ergänze
Welcher Algorithmus liefert den Hash in der Lock-Datei?
var sha512 = MessageDigest.getInstance("");
byte[] digest = sha512.digest(bytes);SHA-512: Das ist der Algorithmus in sha512- von npm und im contentHash von NuGet. Gradle und pip benutzen SHA-256, das auch kryptografisch ist.
Probier es aus
Finde den Fehler
Ein Kollege schreibt die Prüfung neu. Der Registry-Server liefert das Paket zusammen mit seinem Hash, also vergleicht der Kollege mit diesem. Welche Zeile ist der Fehler?
Wähle die Zeile mit dem Fehler.
Der erwartete Hash kommt vom selben Ort wie das Paket. Wer das Paket austauschen kann, kann den Hash daneben austauschen, und die Prüfung geht immer durch. Der erwartete Wert muss aus der Lock-Datei kommen: Sie liegt in deinem Git und wurde geschrieben, als das Paket geprüft wurde. lockFile wird nicht einmal benutzt.
Die Idee
Bekannte Schwachstellen und Aktualisieren
Die Lock-Datei schützt vor Austausch, friert aber auch die alten Fehler ein. Ein Paket ohne schädlichen Code kann eine Schwachstelle haben: einen Fehler, der einen Angreifer hereinlässt. Ein Beispiel ist Log4Shell (2021): Eine Zeile im Log eines Servers mit log4j-core vor 2.17.1 genügte, damit der Angreifer eigenen Code ausführte.
Schwachstellen werden in Datenbanken veröffentlicht (CVE, GitHub Advisory Database, OSV), und Werkzeuge vergleichen deinen Baum damit:
- JavaScript:
npm audit; - Python:
pip-audit; - .NET:
dotnet list package --vulnerable --include-transitive; - Java: OWASP Dependency-Check (ein Plugin für Maven und Gradle);
- für alle: Dependabot und Renovate öffnen einen Pull Request mit dem Update.
Die Hygiene des Aktualisierens:
- Klein und oft. Eine Version mit Tests dahinter zu aktualisieren ist sicher. Fünf Jahre auf einmal nicht. Hier helfen Tests als Sicherheitsnetz.
- Kritische sofort, besonders wenn der Fix schon öffentlich ist.
- Lies, was sich ändert, bevor du eine Hauptversion annimmst.
- Entferne, was du nicht brauchst. Jedes Paket ist Code, den du ausführst. Sechs Zeilen eigener Code sind manchmal billiger als ein Paket mit 40 Abhängigkeiten.
- Eine neue Abhängigkeit ist eine Entscheidung. Wer sie pflegt, wie viele Leute, wie viele Abhängigkeiten sie mitbringt, wann die letzte Version erschienen ist.
Genauer, für Neugierige
Bereiche. In npm erlaubt ^2.1.3 jedes 2.x.y ab 2.1.3, ~2.1.3 nur 2.1.y. Deshalb ergeben ohne Lock-Datei zwei Builds desselben Codes verschiedene Bäume. Bibliotheken schreiben Bereiche, um nicht alle auf eine Version zu zwingen; Anwendungen sperren den genauen Baum.
Installationsskripte. npm ci --ignore-scripts führt postinstall und Ähnliches nicht aus. Viele Projekte machen das in CI standardmäßig und erlauben Skripte nur für aufgelistete Pakete.
Eine eigene Registry. Firmen holen Pakete oft über eine eigene Proxy-Registry (Artifactory, Nexus, Azure Artifacts). Sie behält eine Kopie, lässt nur freigegebene Pakete durch und verfolgt, was wo benutzt wird.
SBOM. Eine Software Bill of Materials ist eine Liste jeder Komponente im Produkt, im Format CycloneDX oder SPDX. Kommt das nächste Log4Shell, ist die Antwort auf „sind wir betroffen?“ eine Suche darin und keine Woche Wühlen.
Signaturen. Ein Hash sagt „dieselben Bytes wie bei der Prüfung“. Eine Signatur sagt „von diesem Autor veröffentlicht“: npm provenance, Sigstore, signierte Pakete in NuGet und Maven Central.
So siehst du den Baum: npm ls --all, mvn dependency:tree, gradle dependencies, dotnet list package --include-transitive, pipdeptree.
Übung
Ein kleines Audit
Löse die Aufgabe in deinem eigenen Editor.
Installiert sind log-kit 2.14.1, http-lite 1.8.0, pay-sdk 4.2.0 und qr-code 2.9.0. Die Schwachstellendatenbank sagt (Paket, behoben in Version, Schweregrad): log-kit 2.17.1 critical, http-lite 1.8.2 high, pay-sdk 3.9.0 moderate, qr-code 2.10.0 low. Gib für jedes aus, ob es verwundbar ist und auf welche Version zu aktualisieren ist, und wie viele verwundbar sind:
log-kit 2.14.1: verwundbar (critical), aktualisiere auf 2.17.1http-lite 1.8.0: verwundbar (high), aktualisiere auf 1.8.2pay-sdk 4.2.0: in Ordnungqr-code 2.9.0: verwundbar (low), aktualisiere auf 2.10.0Verwundbar: 3Vorsicht bei qr-code: Als Text kommt "2.9.0" nach "2.10.0", weil 9 nach 1 kommt. Versionen werden Teil für Teil als Zahlen verglichen.
Weitere Aufgaben
- Schweregrad. Sortiere die verwundbaren nach Schweregrad: critical, high, moderate, low.
- Ein Lock aus Text. Lies die Lock-Datei als Zeilen
name@version hashund prüfe damit die heruntergeladenen Pakete aus der Lektion. - Typosquatting. Vergleiche den Namen eines neuen Pakets mit einer Liste beliebter und warne, wenn er sich um einen Buchstaben unterscheidet (eine Änderung, Einfügung oder Löschung).
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: record, switch und Muster · Tests als Sicherheitsnetz · Nicht vertrauenswürdige Eingaben · Geheimnisse im Code und in Git
- Aufgaben zum Thema: Sicherheit für Entwickler