- Zahlreiche npm-Pakete, darunter das Open-Source-Paket @ctrl/tinycolor, wurden mit bösartigen Versionen infiziert; Ursache war der Diebstahl eines npm-Tokens über einen GitHub-Actions-Workflow in einem gemeinsam genutzten Repository
- Der Angreifer nutzte ein npm-Token mit weitreichenden Rechten, um bösartigen Code in etwa 20 Paketen zu veröffentlichen; @ctrl/tinycolor hatte dabei mit 2 Millionen wöchentlichen Downloads eine besonders große Reichweite
- Die infizierten Versionen führten in der
postinstall-Phase eine bösartige Payload aus; die Security-Teams von GitHub und npm reagierten schnell und leiteten Lösch- und Bereinigungsmaßnahmen ein - Der Autor hat zur Vermeidung künftiger Vorfälle einen verschärften Sicherheitsplan erstellt, darunter die Umstellung auf Trusted Publishing (OIDC), die Minimierung von Token-Berechtigungen, verpflichtendes 2FA und die Nutzung von pnpm-Funktionen
- Der Vorfall zeigt die Anfälligkeit der Software-Lieferkette und macht deutlich, dass verbesserte Sicherheitsfunktionen und geänderte Sicherheitspraktiken im gesamten npm-Ökosystem notwendig sind
TL;DR
- Ein bösartiger GitHub-Actions-Workflow wurde in ein gemeinsam genutztes Repository gepusht und stahl ein npm-Token
- Mit diesem Token veröffentlichte der Angreifer bösartige Versionen von 20 Paketen; darunter war @ctrl/tinycolor, dessen hohe Downloadzahlen die Auswirkungen vergrößerten
- Persönliche Accounts oder Repositories wurden nicht direkt kompromittiert, und es gab weder Phishing noch lokal installierte Malware
- Durch die schnelle Reaktion der Security-Teams von GitHub und npm wurden die bösartigen Versionen entfernt; anschließend wurden saubere Versionen erneut veröffentlicht, um Caches zu bereinigen
Wie ich davon erfahren habe (How I Found Out)
- Am Nachmittag des 15. September informierte Community-Mitglied Wes Todd den Autor per Bluesky-DM über das Problem
- Die Security-Teams von GitHub und npm hatten bereits eine Liste der betroffenen Pakete zusammengestellt und mit der Entfernung begonnen
- Als erster Hinweis wurde der Name des bösartigen Branches „Shai-Hulud“ geteilt, benannt nach dem Sandwurm aus dem Dune-Universum
Was tatsächlich passiert ist (What Actually Happened)
- In dem Repository angulartics2, an dem der Autor vor langer Zeit mitgearbeitet hatte, gab es noch einen Mitwirkenden mit Admin-Rechten
- Ein in diesem Repository gespeichertes npm-Token wurde durch einen bösartigen GitHub-Actions-Workflow gestohlen
- Mit diesem Token veröffentlichte der Angreifer etwa 20 Pakete, darunter @ctrl/tinycolor
- Die Security-Teams von GitHub und npm entfernten die bösartigen Versionen schnell, und der Autor veröffentlichte anschließend neue vertrauenswürdige Versionen
Auswirkungen (Impact)
- Beim Installieren der bösartigen Versionen wurde ein
postinstall-Skript ausgeführt, das ein Sicherheitsrisiko darstellte - Betroffenen Nutzern wird empfohlen, die sofortigen Reaktionsanweisungen von StepSecurity zu befolgen
Veröffentlichungs-Setup und vorläufiger Plan (Publishing Setup & Interim Plan)
- Bisher erfolgte die automatische Veröffentlichung mit einer Kombination aus semantic-release + GitHub Actions
- Die npm-Funktion provenance wurde genutzt, konnte aber einen Angreifer mit einem gültigen Token nicht aufhalten
- Künftig ist die Einführung von Trusted Publishing (OIDC) geplant, um statische Tokens abzuschaffen
- Derzeit wurden alle Tokens widerrufen; zusätzlich werden 2FA verpflichtend gemacht, nur noch Tokens mit granularen Rechten erlaubt und die pnpm-Funktion minimumReleaseAge geprüft
Gewünschte Verbesserungen (Publishing Wishlist)
- Es braucht auf npm-Account-Ebene eine Option zur Erzwingung von OIDC-basiertem Trusted Publishing
- Notwendig sind eine Sperrfunktion für Veröffentlichungen ohne provenance sowie vollständige Integrationsunterstützung für semantic-release und OIDC
- Gewünscht wird eine Funktion in der GitHub-Oberfläche für manuell genehmigte Releases auf Basis von 2FA
- Schutzfunktionen auf dem Niveau von GitHub Environments sollten auch ohne Pro-Abonnement nutzbar sein
- Auf npm-Paketseiten sollte sichtbar sein, ob ein
postinstall-Skript vorhanden ist, und bei gelöschten Versionen sollte der Grund offengelegt werden
1 Kommentare
Hacker-News-Kommentare
In diesem Repository waren noch immer GitHub-Actions-Secrets vorhanden, also npm-Tokens mit weitreichenden Publish-Berechtigungen.
Einer der Vorteile von Trusted Publishing ist, dass man keine langlebigen Publish-Tokens mehr verwenden muss.
Stattdessen werden jetzt nur noch kurzlebige Tokens verwendet, die in der CI-VM erzeugt werden, und sie sind nur 15 Minuten gültig.
Das wird bereits in mehreren Ökosystemen eingesetzt, etwa bei PyPI, npm, Cargo und Homebrew.
Der Release-Prozess wird dadurch tatsächlich sogar etwas einfacher, daher würde ich allen empfehlen, das auszuprobieren.
Falls die Dokumentation noch immer unklar wirkt, kann man jederzeit um Hilfe bitten.
Die Maintainer der jeweiligen Ökosysteme wünschen sich ausdrücklich, dass sich diese Funktion verbreitet.
Siehe die offizielle Dokumentation zu Trusted Publishing
Dass Trusted Publishing inzwischen auch bei npm möglich ist, habe ich diesmal zum ersten Mal erfahren.
Zugehörige Meldung
Ich werde das dieses Wochenende direkt einrichten.
Es wäre gut, wenn es nun ein Flag im Repository gäbe, das anzeigt, dass ein Projekt solche Funktionen nutzt.
Dann könnte man Abhängigkeitspakete, die das nicht verwenden, leicht blockieren.
Mir scheint, dass der Punkt, MFA in den automatisierten Deployment-Prozess einzubeziehen, nicht genug Beachtung findet.
Wenn man per CI-Workflow publisht und den Publish-Vorgang über einen MFA-Prompt bestätigt, ist das grundsätzlich kein Problem, aber als ich mir das zuletzt angesehen habe, musste man dafür einen HTTPS-Tunnel zum Übermitteln des Codes öffnen, was umständlich war.
Ich würde mir wünschen, dass npm oder GitHub direkt eine einfache Möglichkeit bieten, während der CI MFA-Codes bereitzustellen und zu bestätigen.
Beim Veröffentlichen eines Pakets gibt es zwei Schritte: das Hochladen des Pakets zu npmjs und den Schritt, es tatsächlich für Nutzer freizugeben.
Derzeit sind diese beiden Schritte zu einer einzigen Aktion zusammengebunden.
Meiner Meinung nach sollte man das trennen, sodass das CI-System nur noch automatisch baut und hochlädt.
Um das hochgeladene Paket wirklich zu veröffentlichen, sollte sich dann ein Mensch direkt auf der npmjs-Website anmelden und den Publish-Vorgang mitsamt MFA manuell ausführen.
Eigentlich frage ich mich, ob Package Publishing als Konzept überhaupt nötig ist.
Wenn das VCS die „wahre Quelle“ ist, warum nutzt man es dann nicht direkt ohne separaten Publish-Prozess?
Go macht das tatsächlich so.
Pakete werden direkt per URL importiert, und das Versioning läuft über Tags.
Dadurch muss man nur dem VCS vertrauen, was die zusätzliche Angriffsfläche reduziert.
Man muss keine Archivdateien separat diffen, sondern nur Commits prüfen.
Das Problem ist, dass sich beim Verschieben eines Repositories der Import-Pfad ändert, aber auch das kann man als eine Art Vorteil sehen.
Abgesehen davon ist mir nicht ganz klar, welchen Nutzen ein separater Publish-Schritt überhaupt bringt.
Das wirkt wie ein Relikt aus der Zeit, als man tar-Archive per FTP hochgeladen hat.
Ich hatte früher einmal an dem Shared-Repository angulartics2 gearbeitet.
Dort gab es noch immer ein GitHub-Actions-Secret mit einem npm-Token, das weitreichende Publish-Berechtigungen hatte.
Einer der Mitwirkenden hatte außerdem Berechtigungen für mehrere Projekte, was vermutlich erklärt, warum mehrere Pakete gleichzeitig betroffen waren.
Ein neuer Branch namens Shai-Hulud wurde zusammen mit einem bösartigen GitHub-Action-Workflow per Force-Push eingespielt.
Da es sich um einen Mitwirkenden mit Admin-Rechten handelte, lief der Workflow ohne Review sofort an, und das npm-Token wurde geleakt.
Mit dem geleakten Token wurden bösartige Versionen von 20 Paketen veröffentlicht.
Die meisten davon sind keine weit verbreiteten Pakete, aber @ctrl/tinycolor ist ein populäres Paket mit rund 2 Millionen Downloads pro Woche.
Was ich noch immer nicht verstehe, ist, wie man mit dem npm-Token des angulartics2-Repositories überhaupt auch tinycolor veröffentlichen konnte.
Ich habe ebenfalls Admin-Rechte für das npm-Repository einer anderen Person, und die letzten Releases habe fast alle ich durchgeführt.
Seit ich Administrator bin, habe ich mehr Commits unter meinem Namen, weil ich die liegen gebliebenen Probleme aus der Vergangenheit bei der Gelegenheit gleich beheben wollte.
Ich war fast schon entschlossen, Pakete per GitHub Action zu veröffentlichen, hatte aber immer Sorge, dass ich bei einem direkten Deployment mit 2FA versehentlich etwas publiziere, das nicht dem Master-Stand entspricht.
Wegen solcher Probleme habe ich Diskussionen mit den anderen Administratoren immer wieder aufgeschoben, und wenn ich sehe, was jetzt passiert ist, fühlt es sich fast so an, als wäre es gut gewesen, dass ich gewartet habe.
Ich weiß nicht, was die richtige Lösung ist, aber Zugangsdaten Dritten anzuvertrauen scheint ganz sicher keine gute Antwort zu sein.
Falls meine Erklärung unklar war:
Dieses Token hatte globale Publish-Berechtigung für alle meine npm-Pakete.
Ich plädiere seit rund zehn Jahren für manuelle Releases.
Dafür habe ich immer viel Gegenwind bekommen, aber inzwischen wirkt das gar nicht mehr so abwegig.
Ich weiß, dass CI/CD attraktiv ist, aber wenn man diesen Vorfall und die jüngsten CF-Probleme betrachtet, gibt es immer mehr Hinweise darauf, dass durch Automatisierung im Gegenteil schwerwiegende Probleme sogar leichter entstehen können.
Als ich damals bei BigBank gearbeitet habe, mussten für Produktionsdeployments mindestens fünf Leute gemeinsam bereitstehen und viele Verfahren durchlaufen, aber man wusste dafür sehr genau, was man ausrollte.
Nicht wegen GitHub Actions oder automatischen Release-Skripten, sondern weil ich die alte Vorgehensweise für deutlich sicherer halte: lokal bauen, signieren, das Tarball hochladen und verifizieren.
Distributionssysteme, also etwa Paketierungssysteme wie Debian, haben teils zusätzliche Verifikationsschritte, was auch ein Grund ist, warum beim xz-Vorfall nicht das gesamte Internet kompromittiert wurde.
Zumindest sollte es verpflichtend sein, dass ein Mensch die Binärdateien signiert, bevor ein Release veröffentlicht wird.
Da ein Angreifer sich auch selbst als Maintainer hinzufügen und mit seinem eigenen Schlüssel signieren könnte, wäre es noch sicherer, wenn wie bei Distributions-Paketierungssystemen zusätzlich ein vertrauenswürdiges Schlüsselmanagement existierte.
Wenn mein Threat Model darauf basiert, dass schon ein einzelnes kompromittiertes GitHub-Konto oder ein einzelner API-Key ausreicht, um die gesamte Nutzerbasis zu kompromittieren, sollte ich mich wirklich fragen, ob das vernünftig ist.
2FA fürs Publishing ist gut, aber noch sicherer wäre es, wenn mehrere Autoren per kryptografischer Signatur zustimmen müssten.
Ein Angriff sollte nicht schon erfolgreich sein, wenn nur eine einzelne Person kompromittiert wird.
Viele Pakete haben nur einen einzigen Autor.
Mehrere Autorensignaturen zu verlangen ist gut, aber schon wenn Commits, Tags und Artefakte in irgendeiner Form signaturgeprüft würden, ließen sich die meisten Angriffe verhindern.
Distributions-Paketierung unterstützt Signaturprüfung sehr gründlich, aber Sprach-Paketmanager haben bei solchen Verifikationsmechanismen Schwächen.
Zum Beispiel wird der offizielle Release-Prozess von runc vollständig mit Maintainer-Schlüsseln signiert, die auf Yubikeys und Ähnlichem gespeichert sind.
Auch Distributionssysteme verwalten separate Keyrings und verifizieren offizielle Quellen und Binärdateien.
Wenn es solche Abläufe gegeben hätte, wäre dieser Angriff meiner Meinung nach an mehreren Stellen gestoppt worden.
Auch wenn in der CI direkt gebaut werden kann, braucht es am Ende eine Struktur, in der der Maintainer selbst signiert.
Falls Sprach-Paketmanager solche Workflows nicht haben, ist Trusted Publishing immerhin die weniger schlechte Alternative.
Allerdings kann bei einem kompromittierten GitHub-Konto, etwa durch Cookie-Diebstahl, trotzdem direkt veröffentlicht werden.
GitHub unterstützt bei Trusted Publishing zwar Sicherheitsoptionen wie Timeouts, aber ein Angreifer könnte sie auch abschalten.
Selbst wenn mein Konto kompromittiert wird, akzeptiert die Distribution keine Änderungen, die mit einem Schlüssel signiert wurden, den ich nicht signiert habe, was vergleichsweise sicherer ist.
Zur Einordnung: Ich bin bei SUSE, wünsche mir aber, dass die Unterstützung für Artefaktverifikation etwa bei openSUSE, Arch und Gentoo noch weiter zunimmt.
Zugehörige Links:
runc.keyring
keyring_validate.sh
release_sign.sh
runc.keyring von openSUSE
Ich hasse Tokens wirklich.
Tokens sind letztlich nichts anderes als statische Passwörter.
Ich finde, wir brauchen ein ordentliches Authentifizierungsverfahren.
Ein halbwegs sinnvoller Ansatz ist zum Beispiel, GitHub als Token-Anbieter für AWS zu verwenden.
GitHub-AWS-OIDC-Integration
Das ist allerdings eher ein Sonderfall.
Machine-to-Machine-OIDC-Flows können sicher sein, wenn sie sauber implementiert sind, aber die Einrichtung ist viel zu kompliziert.
Und am Ende fühlt sich auch OIDC nur wie ein „komplizierteres Token“ an.
In einer automatisierten Umgebung ohne menschliche Prüfung bleibt immer irgendwo etwas, das geleakt werden kann, egal ob Token oder Token-Erzeuger.
Auch in diesem Wurm-Fall war OIDC keine grundlegende Lösung.
Wenn ein GitHub-Workflow kompromittiert ist, wird der Umgebung ohnehin eine temporäre Identität injiziert, egal ob mit OIDC oder ohne.
Entscheidend ist letztlich ein System, in dem nicht autorisierte Nutzer keinen Workflow mit Zugriff auf Secrets ausführen können.
Wenn man Berechtigungen fein granular aufteilen will, kann es wirksamer sein, statt OIDC einfach den Scope der Tokens zu verkleinern.
Der ursprüngliche Sinn von Tokens ist, dass ihre Lebensdauer und ihr Berechtigungsumfang (authZ) begrenzt sind.
In den meisten Fällen ist das aber in der Praxis nicht so, und sie werden einfach statisch wie Passwörter verwendet.
Es gibt Alternativen wie OAuth oder biscuits mit fein granularen Berechtigungen, aber sie werden in der Praxis kaum genutzt.
Trusted Publishing wird inzwischen von mehreren Paket-Registries unterstützt, darunter npm.
Zugehörige Meldung
Andere haben es schon angesprochen, aber Tokens sollten nur mit kurzer Lebensdauer oder erst nach manueller Authentifizierung ausgegeben werden, etwa per MFA oder Passphrase.
mTLS (TLS-Client-Zertifikate) ist wahrscheinlich die Richtung, die der richtigen Antwort am nächsten kommt.
Kennt jemand ein öffentliches Tool oder Skript, um verwundbare npm-Pakete zu prüfen?
Auf der StepSecurity-Seite scheint es so etwas nicht zu geben.
Man kann nicht alles verhindern, aber es ist eine gute Idee, provenance-action einzuführen.
provenance-action
Für bekannte Probleme ist
npm auditdie Standardwahl.Ich meinte eher, dass ich mir beim lokalen Publishing Sorgen über Fehler wie den falschen Branch oder einen ausgelassenen Build gemacht habe.
Ich frage mich, was passiert, wenn ein CI-Job per Force-Push tief in die Git-Historie eingreift und Änderungen vornimmt.
Der Status quo funktioniert nicht mehr richtig.
Man kann natürlich die technischen Vorteile von OIDC-Tokens, Zero-Trust-Lösungen und Ähnlichem loben,
aber ein erheblicher Teil der Maintainer von npm-Bibliotheken mit Millionen von Downloads wird sich in der Praxis nicht um Sicherheit kümmern, bis sie tatsächlich gehackt werden oder npm die Veröffentlichung schlicht blockiert.
Und dann kommen noch unrealistische Forderungen wie „alle Abhängigkeiten abschaffen und nur noch die Standardbibliothek nutzen“.
Weniger Abhängigkeiten zu haben ist gut, aber das löst die bereits bestehenden Probleme überhaupt nicht.
Realistisch bleiben nur zwei Wege: Entweder zehntausende oder hunderttausende Leute verlassen npm und bauen ihren Code komplett um, oder npm erzwingt für stark heruntergeladene Pakete Regeln wie 2FA und OIDC und blockiert andernfalls die Veröffentlichung vollständig.
Welcher dieser beiden Wege realistischer umsetzbar ist, liegt auf der Hand.
Andernfalls wird der Ruf von npm völlig abstürzen, und wir landen bei XKCD 927.