Wichtigste Erkenntnisse
- npm provenance belegt, aus welchem Repository, welchem Commit und welchem Workflow ein Paket stammt, nicht jedoch, dass der Code sicher ausgeführt werden kann.
- Die Zertifizierungslücke ist die Diskrepanz zwischen dem, was die Herkunftsnachweise belegen, und dem, was man tatsächlich wissen muss, bevor man etwas installiert.
- Ein manipulierter Workflow oder ein böswilliger Commit führt dennoch zu einer gültigen Herkunftsangabe, und die meisten npm-Pakete enthalten überhaupt keine Herkunftsangabe.
- Das Neuerstellen von Paketen aus verifizierten Quellen in einer gehärteten Umgebung verlagert die Vertrauensbasis weg von der Pipeline des Herausgebers.
Wenn Sie in letzter Zeit auf npmjs.com gestöbert haben, ist es Ihnen wahrscheinlich schon aufgefallen. Ganz unten auf vielen Paketseiten gibt es nun einen Abschnitt zur Herkunft, in dem angegeben wird, dass das Paket auf GitHub Actions erstellt und signiert wurde, mit einem direkten Link zu dem Commit, aus dem es stammt. Es sieht aus wie ein Siegel. Man hat das Gefühl, dass das Problem gelöst ist.
Viele Teams betrachten diese Bestätigung als Ziel, und ehrlich gesagt kann ich das nachvollziehen. „Provenance“ war ein echter Fortschritt für das Ökosystem. Aber es beantwortet eine viel engere Frage, als die meisten Menschen annehmen, und genau in der Lücke zwischen der Frage, die es beantwortet, und der Frage, die einem tatsächlich am Herzen liegt, haben sich viele der jüngsten npm-Angriffe eingenistet.
Was npm provenance tatsächlich belegt
npm führte im April 2023 die Herkunftsnachweise (Provenance) ein, und mit der Einführung des „Trusted Publishing“ im Juli 2025 wurde dieser Vorgang nahezu automatisiert. Wenn ein Maintainer über GitHub Actions im Rahmen des „Trusted Publishing“ veröffentlicht, generiert die npm-CLI standardmäßig eine Herkunftsbescheinigung – ganz ohne zusätzliche Option. Diese Bescheinigung ist eine signierte SLSA-Erklärung, die im öffentlichen Transparenzprotokoll von Sigstore erfasst wird und den Tarball mit einem Quell-Repository, einem bestimmten Commit und dem Workflow verknüpft, mit dem er erstellt wurde. Wenn Sie auf der Verbraucherseite stehen, überprüft der Befehl „npm audit signatures“ diese Bescheinigungen in Ihrem gesamten installierten Baum.
Das sind nützliche Informationen. Vor der Einführung von „Provenance“ war der Link zum Repository auf einer Paketseite einfach das, was der Betreuer in package.json. Es gab keine Bestätigung dafür und es war auch nicht an einen Commit gebunden. Und da „npm publish“ alles packt, was sich gerade im Arbeitsverzeichnis des Herausgebers befindet, könnte ein Tarball Build-Ergebnisse, vereinzelte Dateien oder etwas weitaus Schlimmeres enthalten, das in Git überhaupt nie existiert hat. Bei Paketen, die „provenance“ nutzen, schließt diese Funktion diese Lücke. Man weiß, dass der Tarball aus einem bestimmten CI-Lauf für einen bestimmten Commit stammt.
Was du nicht weißt, ist, ob irgendetwas in diesem Lauf dein Vertrauen verdient hat.
Die Bescheinigungslücke
Es ist hilfreich, diesem Phänomen einen Namen zu geben, daher werde ich es „Attestationslücke“ nennen. Die Attestationslücke ist die Differenz zwischen dem, was eine Herkunftsbescheinigung belegt (welches Repository, welcher Commit und welcher Build-Workflow ein Paket erzeugt haben), und dem, was man vor der Installation tatsächlich wissen muss, nämlich ob dieses Artefakt sicher ist und originalgetreu aus Quellcode erstellt wurde, den man bedenkenlos ausführen würde.
Die Herkunft ist eine Aussage über den Ursprung. Die Sicherheit ist eine Aussage über den Inhalt. Eine Bescheinigung kann kryptografisch einwandfrei sein und dennoch nichts darüber aussagen, was der Code tut, wenn er auf Ihrem node_modules Ordner. Wenn es einem Angreifer gelingt, seinen Code in den Commit oder in den Build einzuschleusen, erhält er kostenlos eine gültige Bescheinigung.
Wie ein Paket die Herkunftsprüfung besteht und dich trotzdem verbrennt
Der einfachste Weg ist ein kompromittierter Betreuer. Wenn jemand ein GitHub-Konto mit Push-Zugriff auf ein Projekt übernimmt, muss er npm überhaupt nicht anfassen. Er führt einen Commit durch, der Release-Workflow läuft genau so ab, wie er vorgesehen ist, und die neue Version wird mit einer Herkunftsangabe ausgeliefert, die genau auf den Commit des Angreifers verweist. Alle Prüfungen werden bestanden, da an der Veröffentlichung selbst nichts Ungewöhnliches war. Das Problem liegt im Commit, und die Herkunftsangabe wurde nie dafür entwickelt, Commits zu bewerten.
Der zweite Weg führt über die Build-Pipeline selbst. Beim „Trusted Publishing“ werden langlebige Token gegen kurzlebige ausgetauscht, die vom CI-Workflow generiert werden – eine erhebliche Verbesserung. Das bedeutet auch, dass die Identität des Workflows zur Zugangsberechtigung wird. Andrew Nesbitt hat in seiner Untersuchung zu Sicherheitslücken in Python-Paketen bei GitHub Actions Fälle aufgedeckt, in denen der Angreifer am Ende im Besitz eines gültigen Tokens war, das vom tatsächlichen Workflow des Projekts generiert wurde – die einzige Situation, gegen die „Trusted Publishing“ nichts ausrichten kann. Das Ökosystem ist zwar anders, aber die Mechanismen lassen sich direkt auf npm übertragen.
Der dritte Weg ist der, den viele vergessen. Ihr Release-Build bezieht seine eigenen Abhängigkeiten ein – beispielsweise Bundler, Transpiler und Plugins –, die alle auf dem CI-Runner neu aufgelöst werden. Sollte eine dieser Komponenten zum Zeitpunkt der Ausführung des Release-Jobs kompromittiert sein, kann deren Schadcode in Ihrem dist Ordner und versenden Sie ihn in einem Paket, dessen Herkunft einwandfrei ist. Die Bescheinigung bezieht sich auf Ihren Commit. Sie bürgt nicht für die Hunderte von Paketen, die Ihr Build-Schritt auf dem Weg zur Erstellung dieses Tarballs installiert hat.
Eine fehlende Herkunftsangabe ist ein Signal, das niemand beachtet
Der nächste Punkt stört mich noch mehr. Die Herkunftsangabe ist optional, und der Großteil des Registers hat sich nicht dafür entschieden. Für Pakete, die aus privaten Quell-Repositorys erstellt wurden, ist sie nicht einmal verfügbar. Und npm install installiert gerne eine Version ohne Zertifikat, selbst wenn alle früheren Versionen dieses Pakets ein Zertifikat hatten.
Denken Sie nun einmal darüber nach, wie die großen Wurmkampagnen funktionieren. Shai-Hulud und die darauf folgenden Wellen hatten es auf npm-Token und GitHub-Anmeldedaten abgesehen, um legitime Pakete automatisch erneut veröffentlichen zu können. Wer ein gestohlenes Token besitzt, kann von überall aus veröffentlichen – was in der Regel bedeutet, dass kein CI-Lauf und keine Verifizierungsbescheinigung vorliegt. Theoretisch ist das ein eindeutiges Indiz. Als PyPI seine Analyse des Ultralytics-Hacks verfasste, stellte man fest, dass die zweite Welle bösartiger Veröffentlichungen auffiel, da sie weder mit Repository-Aktivitäten noch mit Veröffentlichungsbescheinigungen einherging.
Das funktioniert, wenn jemand nach dem Drop sucht. In den meisten npm-Konfigurationen tut das jedoch niemand. Ein Paket, das plötzlich seine Herkunftsangabe verliert, sieht genauso aus wie die Tausenden, die nie eine hatten, und die Installation läuft ohne Probleme ab.
Das Kompilieren aus dem Quellcode schließt eine andere Lücke
Wenn die Herkunftsangabe Aufschluss darüber gibt, wo etwas erstellt wurde, lautet die naheliegende nächste Frage, ob man es selbst erstellen und das gleiche Ergebnis erzielen kann. Das ist der Gedanke hinter einem unabhängigen Neuaufbau. Anstatt dem vom Herausgeber hochgeladenen Tarball zu vertrauen, nimmt man den Quellcode, von dem dieser behauptet, dass er daraus stammt, erstellt ihn erneut in einer Umgebung, die man selbst kontrolliert, und behandelt das Ergebnis als das, was installiert wird. Die Pipeline des Herausgebers ist damit nicht mehr die Vertrauensbasis.
Genau diesen Ansatz haben wir bei SecureChain verfolgt. Pakete werden aus verifizierten Quellen in abgesicherten, isolierten Umgebungen neu kompiliert – ohne geerbte Binärdateien und ohne undurchsichtige Upstream-Artefakte. Jedes Paket wird zudem auf bekannte Malware und verdächtige Codemuster überprüft, und alles, was die Prüfung nicht besteht, wird blockiert, bevor es den Laptop eines Entwicklers oder einen CI-Runner erreicht. Es fungiert als direkter Ersatz für die öffentliche npm-Registry, sodass sich npm, Yarn und pnpm genau so verhalten, wie es Ihr Team erwartet, und es kann vor Artifactory, Nexus oder GitHub Packages eingesetzt werden, falls Sie Pakete bereits auf diese Weise weiterleiten.
SecureChain liefert ebenfalls eine SLSA-Provenienz der Stufe 3, was den Anschein erwecken könnte, als würde ich mein eigenes Argument untergraben. Das tue ich jedoch nicht. Die Herkunftsangabe ist nur so aussagekräftig wie der Build, den sie beschreibt. Eine Upstream-Herkunftsangabe besagt lediglich, dass die CI des Betreuers ein Paket erstellt hat – und eine kompromittierte CI kann durchaus einwandfreie Pakete produzieren. Eine Herkunftsangabe, die mit einem unabhängigen Rebuild verbunden ist, besagt hingegen, dass das Artefakt aus einer gehärteten Build-Umgebung stammt, aus Quellcode, der vor der Einbindung überprüft wurde. In beiden Fällen handelt es sich um dasselbe SLSA-Format, doch es bürgt für einen völlig anderen Build.
Was ein System-Neuaufbau nicht für Sie leisten kann
Das Neuerstellen hat seine Grenzen, und es lohnt sich, diese offen anzusprechen. Befindet sich der Schadcode in der Quelle selbst, wird er bei einer originalgetreuen Neuerstellung ebenfalls originalgetreu reproduziert. Deshalb müssen das Neuerstellen und die Malware-Prüfung Hand in Hand gehen. Bei der einen Frage geht es darum, ob das Artefakt mit seiner Quelle übereinstimmt, bei der anderen darum, ob die Quelle etwas ist, das man auf seinem Rechner ausführen möchte. Keine der beiden Fragen schließt die Lücke für sich allein.
Es gibt auch ein Problem, das das gesamte Ökosystem betrifft. Nicht jedes npm-Paket lässt sich anhand der Inhalte seines Repositorys fehlerfrei kompilieren. Manche enthalten Dateien, die nie committet wurden, manche sind von Build-Konfigurationen abhängig, die niemand jemals dokumentiert hat, und bei einigen gibt es überhaupt keinen öffentlichen Quellcode. Das ist ein Problem für jeden, der versucht, die Registry in großem Maßstab zu überprüfen, und es ist ein stichhaltiges Argument dafür, von den Herausgebern mehr zu verlangen, nicht weniger.
Wo ich anfangen würde
Der kostengünstigste Einstieg besteht darin, „npm audit signatures“ in der CI auszuführen, damit Sie zumindest erkennen können, welche Ihrer Abhängigkeiten zertifiziert sind und welche nicht. Wenn Sie selbst Pakete veröffentlichen, wechseln Sie zum „Trusted Publishing“, damit Ihre eigenen Releases eine Herkunftsangabe enthalten. Das kostet Sie nur sehr wenig und macht es etwas einfacher, den Überblick über das Ökosystem zu behalten.
Führen Sie dann mit Ihrem Team ein umfassenderes Gespräch darüber, wem Ihr Vertrauen tatsächlich gilt. Derzeit liegt es bei den meisten JavaScript-Projekten bei den CI-Pipelines und npm-Konten Tausender Fremder, und „Provenance“ zeigt Ihnen lediglich, bei welchen genau. Dieses Vertrauen auf eine Quelle zu verlagern, die Sie kontrollieren – eine, die Pakete auch lange nach dem Ausscheiden ihrer Betreuer neu erstellt, überprüft und weiterhin mit Patches versorgt –, ist eine wesentlich stärkere Position. Genau an dieser Stelle setzt der „Endless Lifecycle Support“ von TuxCare an, sobald ein Paket das Ende seiner Lebensdauer erreicht hat.
Eine Herkunftsangabe ist sinnvoll, und ich würde mir wünschen, dass sie im Register viel häufiger verwendet wird. Sie gibt lediglich Auskunft darüber, woher ein Paket stammt – entscheidend für die Sicherheit Ihrer CI-Geheimnisse ist jedoch, was darin enthalten ist.
