Eine Express-API namens api-service kann Tests bestehen und ein OCI-Image erstellen, während sie dennoch Code ausliefert, dessen Freigabe nie beabsichtigt war. Ein Entwickler fügt expresss anstelle von express in die package.json ein; die öffentliche Registry löst das Paket mit dem ähnlichen Namen auf, der GitHub-Actions-Runner installiert es, und das Release-Image enthält es. Der Angreifer benötigte dafür weder Schreibzugriff auf das Repository noch einen gestohlenen Deploy-Key oder Zugriff auf den Produktionscluster. Die für Verteidiger entscheidende Frage lautet hingegen: Wie verhindern wir, dass nicht überprüfter Code durch die Auflösung von Abhängigkeiten in einen Build gelangt? Wir verfolgen den Weg von api-service vom Manifest über den CI-Build bis hin zum veröffentlichten Image.
Der Build-Prozess überschreitet mehrere Vertrauensgrenzen, bevor das Image bereitgestellt wird. Eine öffentliche Paket-Registry liefert Code basierend auf einem Namen im Manifest, der CI-Runner führt die Installation durch, und die Artefakt-Registry speichert das resultierende Image für die spätere Bereitstellung. Ein schädliches Paket kann versuchen, Code während des Builds auszuführen, eine schädliche transitive Abhängigkeit einzuschleusen oder ein dem Runner zugängliches Veröffentlichungs-Token zu stehlen. Der folgende Ablauf macht diese Grenzen sichtbar:
Dieses Diagramm veranschaulicht, warum die Auswahl von Abhängigkeiten eher eine Entscheidung bezüglich der Lieferkette (Supply Chain) als ein bloßes Detail der lokalen Programmierung darstellt. Sobald der Runner ein Paket abruft, werden dessen Inhalte Teil der Build-Eingabe; dabei können Installations-Hooks ausgeführt werden, sofern die Pipeline diese nicht einschränkt.
Die Manifest-Datei bildet die erste Barriere, da npm install jeden deklarierten Paketnamen als Anweisung interpretiert, den entsprechenden Code aufzulösen und abzurufen. Im Fall von api-service wird express in der Version 4.18.3 erwartet, während der erfundene Name expresss (Version 1.0.0) einen Tippfehler darstellt, der auf ein anderes Paket verweist. Bei diesem Angriff handelt es sich um Typosquatting: Ein Angreifer veröffentlicht ein Paket, dessen Name einer vertrauenswürdigen Abhängigkeit ähnelt, und spekuliert darauf, dass es aufgrund eines menschlichen Fehlers ausgewählt wird.
Eine Lock-Datei (z. B. package-lock.json) protokolliert die aufgelöste Paketversion, Registry-Metadaten sowie den Integritäts-Hash für einen bestimmten Abhängigkeitsgraphen. Diese Aufzeichnung sorgt zwar dafür, dass spätere Installationen reproduzierbar sind, doch geschieht dies erst, nachdem das fehlerhafte Paket bereits akzeptiert und in das Repository übernommen (committed) wurde. Das Versäumnis bei der Überprüfung ist daher an zwei Stellen sichtbar: Die package.json enthält eine unerwartete direkte Abhängigkeit, und die package-lock.json weist einen neuen aufgelösten Eintrag sowie einen Integritätswert auf. Welcher Paketname und welcher Eintrag in der Lock-Datei sollten dazu führen, dass das Hinzufügen von expresss bei der Überprüfung abgelehnt wird?
Dieser Code zeigt, dass eine erfolgreiche Installation kein Beweis dafür ist, dass die beabsichtigte Abhängigkeit ausgewählt wurde. Der Installer löste expresss genau wie angefordert auf und speicherte das Ergebnis in der Lockfile; die deterministische Wiederholung dieses Fehlers bleibt dennoch ein Fehler. Die Überprüfung von Änderungen an Abhängigkeiten (Diffs) bedeutet, einen unbekannten Namen, eine unerwartete Version oder einen neu hinzugefügten Integritätseintrag so zu behandeln, als würde dieser Code in das Release einfließen.
Prüfregel Eine Lockfile belegt, was der Installer ausgewählt hat. Sie belegt nicht, dass diese Auswahl autorisiert oder sicher war.
Ein falsch geschriebener öffentlicher Name ist der Ausgangspunkt im Manifest, doch ein privates Paket kann auch dann ersetzt werden, wenn der Name korrekt ist. Dependency Confusion (Abhängigkeitsverwechslung) tritt auf, wenn ein Build-Prozess ein Paket aus einer nicht beabsichtigten Registry auflöst – meist, weil ein öffentliches Paket mit demselben Namen oder einer höheren, passenden Version Vorrang vor einem internen Paket hat. Der Angreifer benötigt lediglich die Möglichkeit, Pakete in einer Registry zu veröffentlichen, auf die der Build-Prozess zugreift; Zugriff auf das private Quellcode-Repository ist nicht erforderlich.
Für api-service muss eine interne Abhängigkeit wie @acme/auth-client immer aus der privaten Registry der Organisation aufgelöst werden. Ein weiter Versionsbereich lässt zukünftige Versionen zu, die den deklarierten Bereich erfüllen, während die Priorität der Registry bestimmt, wo der Installer sucht. Eine Lockfile legt das Ergebnis für einen festgeschriebenen Build fest, kann jedoch weder eine Registry-Zuordnung korrigieren, die bei einem erneuten Build auf die falsche Quelle verweist, noch vor einer bewussten Aktualisierung der Lockfile schützen, die von Prüfern nicht kontrolliert wird. Das Kontrollziel ist die deterministische Auflösung aus der beabsichtigten Registry für jeden Paket-Scope.
Der Vergleich unterscheidet zwischen der Grenze, die die jeweilige Bedrohung überschreitet, und dem Nachweis, der sie aufdeckt: