Jeder hätte dieses Paket veröffentlichen können
Am 28. August öffnete jemand einen Pull Request gegen einen verbreiteten OpenAPI-Codegenerator, schrieb einen Kommentar aus zwei Wörtern darunter, und der eigene Release-Workflow des Projekts veröffentlichte zehn vergiftete Versionen auf npm — mit gültigen Provenance-Attestierungen. Der Angreifer besaß nie eine Zugangsberechtigung. Die erste Welle enthielt überhaupt kein Installationsskript und versteckte ihre Nutzlast dort, wo node-gyp Python auswertet. Das ganze Fenster dauerte drei Stunden und elf Minuten, und die Mindestalter-Sperre von Bromure Agentic Coding, standardmäßig auf zwei Tage, schlägt diese Zeit, ohne irgendetwas über den Angriff zu wissen.
Jemand öffnete einen Pull Request aus einem Fork und hinterließ einen Kommentar aus zwei Wörtern. Die Release-Pipeline des Projekts las den Kommentar, checkte den fremden Code aus, führte ihn aus und signierte, was dabei herauskam. Gestohlen hat diese Person dafür nichts.
@7nohe/openapi-react-query-codegen verwandelt ein OpenAPI-Schema in
TanStack-Query-Hooks. Sie richten es auf eine Spezifikation, und es schreibt
Ihnen den Client-Code. Aikido zählt mehr als 150.000 Downloads pro Woche. Man
nimmt so ein Paket, weil es einem einen Nachmittag spart, und der Coding-Agent
installiert es, ohne zweimal nachzufragen.
Am 28. August begann es, einen Zugangsdaten-Dieb auszuliefern.
Aikido Security veröffentlichte die Analyse noch am selben Tag, und SafeDep veröffentlichte eine eigene mit dem Mechanismus des Workflows und einer Minute-für-Minute-Chronologie. Die Schadsoftware benennt sich in ihren eigenen Zeichenketten selbst: Trinitite. Beide Teams ordnen ihr Vorgehen der Familie Mini Shai-Hulud zu, jenem sich selbst vermehrenden npm-Wurm, der sich das ganze Jahr über durch Maintainer-Konten arbeitet.
Interessant ist sein Weg auf npm.
Die Release-Pipeline nahm eine Anweisung von einem Fremden entgegen
Die release.yml des Projekts hatte einen issue_comment-Trigger. Das ist ein
GitHub-Actions-Ereignis, das auslöst, wenn jemand ein Issue oder einen Pull
Request kommentiert, und es ist eine verbreitete Bequemlichkeit: Maintainer
verdrahten es, um ein Release durch ein getipptes Wort statt durch Klicks in
einer Oberfläche auszuliefern.
Dieser hier hatte keine Prüfung der Autorenzugehörigkeit. GitHub teilt einem Workflow mit, ob die kommentierende Person der Eigentümer des Repositorys ist, ein Mitglied, ein früherer Beitragender oder eine völlig fremde Person — und der Workflow muss hinsehen. Dieser sah nicht hin.
SafeDep hat die Abfolge nachgezeichnet. Einen Pull Request aus einem Fork
öffnen. npm publish darunter kommentieren. Die Pipeline wacht auf, checkt
den Code im Fork des Angreifers aus und führt pnpm install aus, was den
preinstall-Hook des Angreifers auf dem Runner des Projekts selbst ausführt.
Dieser Job trug id-token: write — genug, um ein npm-OIDC-Token für Trusted
Publishing zu prägen.
Zehn bösartige Versionen gingen hinaus, jede mit einer gültigen Provenance-Attestierung, weil die Pipeline des Projekts jede einzelne von ihnen gebaut und veröffentlicht hat. Aikidos Satz ist der, den man behalten sollte: „Wenn der Workflow selbst kompromittiert ist, wird dieses Zertifikat zu einem unzuverlässigen Vertrauenssignal.“
Prüfen Sie die Provenance eines Pakets, und Sie erfahren genau eines: ob es aus der Pipeline jenes Repositorys stammt. Hier tat es das. Niemand hat etwas gefälscht, und man kann eine korrekte Signatur über Schadsoftware halten, ohne dass eine der beiden Tatsachen falsch aussieht. Im Mai haben wir dasselbe Ergebnis über ein gestohlenes OIDC-Token eintreffen sehen. Diesmal hat der Angreifer den Diebstahl übersprungen.
Die erste Welle hatte kein Installationsskript zu finden
Scannen Sie jene ersten acht Versionen nach gefährlichen Installations-Hooks,
und Sie kommen leer zurück. Ihre package.json-Dateien deklarieren nichts, das
läuft.
Die Nutzlast steckte in binding.gyp, der Build-Beschreibungsdatei, die
node-gyp zum Kompilieren nativer Module verwendet. Aikido erklärt den Trick:
„Wenn npm install ein Paket verarbeitet, das eine binding.gyp enthält,
ruft es node-gyp auf, um das native Modul zu kompilieren. node-gyp wertet
das Feld conditions in der Datei mit Python aus, was bedeutet, dass dort
beliebige Python-Ausdrücke platziert werden können und diese während der
Installation ausgeführt werden, selbst ohne jedes in package.json deklarierte
preinstall-Skript.“
Der Angreifer schrieb also einen Python-Ausdruck in eine Build-Datei. Er läuft
durch Pythons internen Unterklassenbaum, bis er catch_warnings erreicht,
nutzt diese Klasse, um an __builtins__ zu kommen, importiert von dort os
und ruft os.system() auf eine im Tarball mitgelieferte JavaScript-Datei auf.
SafeDep merkt an, dass der Durchlauf in Unicode-Escapes geschrieben ist, sodass
auch ein grep nach os.system leer zurückkommt.
Das JavaScript darunter packt drei Schichten aus — XOR, dann AES-128-GCM, dann
ein kommerzieller Obfuskator —, bevor es Bun 1.4.0 von GitHub in ein temporäres
Verzeichnis mit dem Präfix trinnyyyy- zieht, um die eigentliche Nutzlast
auszuführen.
Neunzehn Minuten später schickte der Angreifer eine zweite Welle, bei der
preinstall als Rückfalloption wieder in der package.json stand. SafeDep
datiert diese Welle auf weniger als eine Minute nach dem ersten öffentlichen
Sicherheitsbericht. Wer das betrieb, las die Veröffentlichungen und lieferte
dagegen an.
Drei Stunden und elf Minuten
SafeDeps Chronologie läuft in UTC. Das letzte saubere Release, 3.0.2, ging am 11. August hinaus. Die erste Welle landete am 28. August um 20:00 und endete um 20:02. Die zweite lief von 20:19 bis 20:21. npm zog alle zehn Versionen gegen 23:11 zurück.
Das ergibt ein Expositionsfenster von drei Stunden und elf Minuten. Ein Projekt
auf ^3.0.0, dem gewöhnlichen Caret-Bereich, den npm für Sie schreibt, löste
geradewegs hinein auf. Wenn Sie in diesem Fenster eine frische Installation
gefahren haben, oder Ihre CI, oder Ihr Coding-Agent, haben Sie den Wurm
bekommen.
Einmal gestartet, durchkämmte er das Home-Verzeichnis mit mehr als 150
Glob-Mustern: private SSH-Schlüssel, .env-Dateien, ~/.docker/config.json,
AWS-, Azure- und GCP-Zugangsdaten, npm-, PyPI- und RubyGems-Token,
Kubernetes-Service-Account-Token, Kryptowährungs-Wallets. Auf derselben Liste
standen ~/.claude.json, ~/.claude/ und ~/.claude/mcp.json — die
Konfiguration des Coding-Agents selbst und die Adressen jedes MCP-Servers, mit
dem er spricht.
In Repositorys, die er erreichen konnte, schrieb er eine
.claude/settings.json mit einem SessionStart-Hook, der setup.mjs
jedes Mal ausführt, wenn jemand das Projekt in Claude Code öffnet. Mit den
gerade eingesammelten Registry-Token veröffentlichte er sich selbst erneut in
jedem Paket, das sein Opfer auf npm, PyPI und RubyGems betreute. Das ist der
Wurm-Anteil: Jede Person, bei der er landete, wurde zur Kandidatin für das
nächste Release.
Zwei Tage schlagen drei Stunden
Setzen Sie nun ein Bromure-Agentic-Coding-Profil in den Weg.
Bromure führt jedes Profil als eigene virtuelle Maschine auf Apple Silicon aus, und jedes Paket, das der Agent holt (npm, PyPI, Cargo, RubyGems, Maven, NuGet, Go-Module, Packagist), läuft durch einen Proxy auf der Mac-Seite des Hypervisors, bevor ein einziges Byte den Gast erreicht.
Das Erste, was dieser Proxy prüft, ist das Alter der Version. Supply Chain →
die Mindestalter-Sperre ist standardmäßig an, mit zwei Tagen als Untergrenze.
Bewegliche Referenzen (latest, Caret-Bereiche, Tilde-Bereiche) lösen
kommentarlos auf die neueste Version auf, die älter als die Grenze ist. Pinnen
Sie etwas zu Frisches, bekommen Sie ein 451 mit einer klaren Fehlermeldung.
Halten Sie das gegen ein feindliches Fenster von drei Stunden und elf Minuten.
Führen Sie am 28. August um 20:30 UTC mitten im Angriff npm install aus, mit
^3.0.0 in Ihrem Manifest, und Ihr Agent löst auf 3.0.2 vom 11. August auf.
Er erfährt nie, dass 3.0.3 existiert.
Die Sperre kam ohne Signatur, ohne Reputationswert und ohne Sicherheitsmeldung dorthin. Sie las ein Veröffentlichungsdatum. Das zählt hier, denn die zweite Welle ging binnen einer Minute nach der ersten öffentlichen Warnung hinaus, und jede Verteidigung, die auf einen Bericht wartet, ist zu spät ins Rennen gegangen.
Was der Durchlauf findet, wenn er trotzdem läuft
Nehmen wir an, Sie haben die Sperre für ein Paket abgeschaltet, oder die Version war älter als die Grenze, bevor es jemand bemerkte. Die Nutzlast muss noch vier Dinge tun, und in einem Bromure-Profil trifft jedes davon auf eine Kontrolle, die auf dem Mac lebt, wo der Gast sie nicht erreicht.
Sie muss laufen. Supply Chain → Entfernen von Installationsskripten
nimmt preinstall, install, postinstall und prepare im Fluge aus
npm-Tarballs heraus, schreibt den Tarball neu und aktualisiert den
Metadaten-Hash der Registry, sodass npms eigene Prüfung weiterhin besteht. Damit
ist die zweite Welle erledigt. Sie können weitere Prüfungen darüberlegen:
OSV-Schwachstellenabfragen und Paketfilterung über socket.dev, dessen
Prüfung zum Blockieren kompromittierter Pakete genau diese Klasse abdeckt
(bösartige Installationsskripte, Schadsoftware), oder Delpi als
Ersatz-Registry. Keine davon fragt, ob eine Attestierung existiert — was zählt,
wenn die Attestierung echt ist.
Sie muss etwas finden. Der 150-Glob-Durchlauf läuft gegen /home/ubuntu in
einer virtuellen Maschine, nicht gegen das Home-Verzeichnis Ihres Macs.
Ordnerfreigaben sind explizit und auf acht begrenzt. Die Zugangsdaten, die er
findet, sind Attrappen: Token im Gast sind brm_…-Platzhalter, die der
Host-Proxy auf der Leitung gegen die echten Werte tauscht, sodass das echte
Material nie in den Adressraum der VM gelangt. Private SSH-Schlüssel
sind überhaupt nicht im Gast: Ein
ssh-agent pro Profil auf dem Host übernimmt das Signieren, und
~/.docker/config.json enthält gefälschtes Base64. Die kubeconfig ist
synthetisch, mit Wegwerf-Client-Zertifikaten. AWS-Anfragen werden hostseitig neu
signiert, sodass alles, was den Proxy umgeht, als
InvalidSignatureException zurückkommt statt authentifiziert. Schalten Sie
Genehmigung zur Nutzung erforderlich für eine Zugangsberechtigung ein, und
jeder Austausch öffnet einen Dialog auf Ihrem Mac mit einer zeitlich begrenzten
Freigabe.
Sie muss nach draußen telefonieren. Guardrails → Ausgehende Verbindungen
ist ein Regelwerk im pf-Stil: eine Aktion, ein Protokoll (tcp, udp, web
oder any), ein Hostname oder ein IPv4-CIDR, Ports, und für web die
HTTP-Methoden, die Sie zulassen. Regeln greifen von oben nach unten, der erste
Treffer gewinnt, und Nicht zugeordneter Verkehr auf Verweigern macht aus der
Liste eine Erlaubnisliste. Dieselbe Richtlinie wird von unabhängigen Schichten
zweimal ausgewertet: vom virtuellen Switch nach Ziel-IP und per DNS
mitgelesenem Hostnamen über alle Protokolle hinweg, und vom Host-Proxy nach
TLS-SNI und Methode. Eine Bun-Binärdatei zu holen und gestohlene Geheimnisse in
ein Ablage-Repository zu schieben, brauchen beide ein Ziel — und Ziele müssen
auf der Liste stehen.
Sie muss veröffentlichen. Guardrails → GitHub auf Nur lesen behandelt
git-receive-pack als Schreibzugriff und blockiert REST-Schreibvorgänge; es
liefert ein hartes 403, das der Agent als gewöhnlichen API-Fehler liest. Kein
vergifteter Commit, keine tote Ablage, kein weiteres Release. Dieselben Modi
decken GitLab, Bitbucket, Kubernetes, AWS, DigitalOcean und Container-Registrys
ab.
Der SessionStart-Hook, den er für das nächste Mal hinterlassen wollte, liegt
in einem Home-Verzeichnis, das Home löschen… wegwischt, unter Resources →
Storage. Auf Basis zurücksetzen… tut dasselbe für alles, was außerhalb
geschrieben wurde. Jede Entscheidung auf dem Weg, jedes erlaubte oder
verweigerte Ziel, jedes Paketurteil, jeder Zugangsdaten-Tausch landet im
Fenster des Sicherheitsprotokolls auf dem Host, wo Gast-Code nichts
bearbeiten kann, weil Gast-Code es nicht erreicht.
Warum das immer wieder passiert
Trusted Publishing und Provenance sind es wert, zu existieren. Sie haben den Diebstahl des npm-Tokens eines Maintainers unterbunden, den vor einem Jahr vorherrschenden Angriff. Die Kompromittierung dieser Woche ging ohne jede Berührung eines Tokens darum herum: Eine fremde Person konnte den Release-Prozess erreichen, also signierte die Pipeline eine wahrheitsgemäße Attestierung über feindlichem Code.
Da können Sie sich nicht herausauditieren, denn der nächste Workflow, der versagt, wird jemand anderem gehören. Was Sie tun können, ist aufzuhören, die erste Maschine zu sein, die eine neue Version von irgendetwas ausführt.
Öffnen Sie ein Profil, sehen Sie unter Supply Chain nach und prüfen Sie, ob die Mindestalter-Sperre an ist. Sie sollte es sein, auf zwei Tagen, ohne dass Sie sie angefasst hätten. Dann investieren Sie zwei weitere Minuten: OSV-Schwachstellenprüfung an, socket.dev oder Delpi ausgewählt, falls Sie einen Schlüssel haben, Entfernen von Installationsskripten an. Setzen Sie in den Guardrails Nicht zugeordneter Verkehr auf Verweigern mit einer kurzen Erlaubnisliste, und stellen Sie GitHub auf Nur lesen für jedes Profil, das nichts ausliefert. Schalten Sie unter Zugangsdaten Genehmigung zur Nutzung erforderlich für alles ein, was Geld ausgeben kann.
Dann lassen Sie den Agenten installieren, was er braucht. Er bekommt 3.0.2. Installieren Sie Bromure Agentic Coding und lassen Sie die Mindestalter-Sperre in Ruhe.