Der verräterische Hinweis war der Zeitstempel
Am 8. Juli 2026 brachte npm seine größte Sicherheitsänderung seit sechzehn Jahren heraus — Installationsskripte standardmäßig deaktiviert. Innerhalb einer Woche verlagerten zwei separate npm-Kompromittierungen ihre Nutzlast so, dass sie beim Import des Pakets zündet statt bei der Installation, und umgingen damit direkt den neuen Standard und --ignore-scripts. Der Mechanismus, den das Ökosystem gerade ein Jahr lang zu beobachten gelernt hatte, hatte sich bereits verschoben. Das Einzige, was sich nicht verschob, war das Alter der bösartigen Version — und genau dieses Signal prüft Bromures Altersschwelle.
Das gesamte Ökosystem hat ein Jahr damit verbracht zu lernen, dass npm-Installationsskripte die Gefahr waren. Am 8. Juli schaltete npm sie standardmäßig ab. Drei Tage später verlagerte eine Kompromittierung ihre Nutzlast aus dem Installationsskript heraus; drei Tage danach kam eine zweite ganz ohne Installationsskript. Beide zünden, wenn man das Paket importiert. Der Fix und die Umgehung kreuzten sich in derselben Woche.
Ein Entwickler fügt am 14. Juli @asyncapi/generator zu einem Projekt
hinzu und führt npm install aus. Das Paket hat kein preinstall, kein
install, keinen postinstall-Hook — nichts, was ein Skript-Scanner
markieren könnte, nichts, was npms brandneuer Standard überspringen
müsste. Die Installation läuft sauber durch. Beim ersten Mal, wenn der
Code require('@asyncapi/generator') ausführt, startet ein in den
Einstiegspunkt des Pakets selbst eingebauter Loader einen abgetrennten
Node-Prozess, holt eine 8,2 MB große zweite Stufe über IPFS, entschlüsselt
sie und beginnt, nach Zugangsdaten zu jagen. Nichts an der Installation sah
falsch aus, weil der Teil, der falsch war, nicht bei der Installation
geschah.
Der Fix, der für den Angriff des letzten Jahres kam
Fast ein Jahr lang war die Geschichte der npm-Lieferketten-Angriffe
dieselbe: Ein kompromittiertes Paket trägt einen postinstall-Hook, und
npm install führt ihn automatisch auf Ihrem Rechner aus. Also brachte npm
am 8. Juli Version 12 heraus — die größte
Sicherheitsüberarbeitung in den sechzehn Jahren des Tools. Lifecycle-
Skripte aus Abhängigkeiten sind jetzt standardmäßig deaktiviert:
„preinstall-, install- und postinstall-Skripte aus Abhängigkeiten laufen
nicht, es sei denn, Sie erlauben es.“ Git-Abhängigkeiten und
Remote-URL-Quellen wurden ebenfalls Opt-in. Es ist eine echte
Verbesserung, und sie schließt eine Tür, durch die ein Jahr voller Würmer
marschiert ist.
Sie schließt eine Tür. Die Angreifer hatten die andere schon gefunden.
Die Nutzlast wanderte zur Importzeit
Am 11. Juli wurde das npm-Paket jscrambler
kompromittiert —
fünf bösartige Versionen, veröffentlicht über etwa drei Stunden. Die ersten
drei, 8.14.0 bis 8.17.0, „führten den Dropper aus einem preinstall-Hook
aus“, das klassische Muster. Dann, mitten in der Kampagne, wechselte der
Angreifer die Tür. „Ab 8.18.0 ist der Install-Hook vollständig verschwunden
— derselbe Dropper wird stattdessen als selbstausführende Funktion am
Anfang von dist/index.js injiziert“, sodass er „zündet, wenn das Paket
importiert oder sein CLI ausgeführt wird, statt bei der Installation“.
Socket benennt den Grund unverblümt: „Das ist eine bewusste Umgehung: Sie
hebelt Scanner aus, die nur preinstall/postinstall-Skripte prüfen, und
übersteht npm install --ignore-scripts.“
Drei Tage später wurde die npm-Organisation
@asyncapi
auf dieselbe Weise kompromittiert, aber von Anfang an dafür gebaut. Fünf
Versionen über vier Pakete — @asyncapi/specs 6.11.2-alpha.1 und 6.11.2,
@asyncapi/generator 3.3.1, @asyncapi/generator-components 0.7.1,
@asyncapi/generator-helpers 1.1.1 — wurden neu veröffentlicht, nachdem
ein Angreifer über einen fehlkonfigurierten GitHub-Actions-Workflow das
Bot-Token des Projekts abgegriffen hatte. Microsofts Befund ist der, der
hier zählt: „Alle betroffenen Pakete deklarierten keine preinstall-,
install- oder postinstall-Hooks in der package.json.“ Der Loader saß
stattdessen im normalen Einstiegspunkt jedes Pakets — index.js für die
Specs, eine validator.js tief in den Templates des Generators. Und der
Hinweis zur Eindämmung ist eindeutig: „Verlassen Sie sich nicht auf npm install --ignore-scripts als Gegenmaßnahme; diese Kampagne führt aus, wenn
das Modul importiert wird, nicht über einen Lifecycle-Hook.“
Zwei unabhängige Akteure, drei Tage auseinander, landen beide auf demselben Zug in genau der Woche, in der der Vorzeige-Fix des Ökosystems live ging. Das ist kein Zufall. Das passiert, wenn eine Verteidigung einen Mechanismus benennt: Der Mechanismus wird zum Standpunkt, und woanders zu stehen kostet den Angreifer ein paar Zeilen Code. Ein Scanner, der fragt „hat dieses Paket einen Install-Hook?“, bekommt von AsyncAPI ein wahrheitsgemäßes „nein“ und winkt es durch.
Das Eine, das sich nicht verschob
Nehmen Sie den Zustellmechanismus weg und schauen Sie, was über beide Kompromittierungen hinweg konstant blieb. Die jscrambler-Versionen waren ein paar Stunden live. Die stabile AsyncAPI-Version wurde um 08:30 UTC veröffentlicht, und die nachgelagerte Erkennung begann um 08:49 — neunzehn Minuten. Jede bösartige Version in beiden Vorfällen wurde deutlich innerhalb von zwei Stunden nach dem Hochladen zurückgezogen. Die Nutzlast wanderte vom Install-Hook zum Einstiegspunkt; die Zustellung wanderte von einem Lifecycle-Skript zu einem Laufzeit-Loader. Die eine Eigenschaft, die sich nicht verschob, ist, dass jede vergiftete Version brandneu war. Eine frisch veröffentlichte Version ist der gemeinsame Fingerabdruck einer Kompromittierung, denn das ist die Form des Angriffs: ein Token stehlen, veröffentlichen, erwischt werden, zurückgezogen werden. Das Fenster ist von Natur aus kurz.
Genau diese Eigenschaft prüft Bromures Altersschwelle,
und sie ist standardmäßig aktiv. Jeder Paketabruf — npm, PyPI, Cargo und
die übrigen — läuft durch den Man-in-the-Middle-Proxy des Hosts, bevor die
Installation des Agenten auch nur ein Byte sieht, und der Proxy verweigert
jede Version, die jünger ist als die Grenze, standardmäßig zwei Tage. Eine
gleitende Referenz wie latest oder ein semver-Bereich löst sich
stillschweigend auf die neueste Version auf, die alt genug ist; eine
gepinnte Referenz auf eine zu frische Version kommt als 451 mit einem
Bromure-Fehler zurück, der erklärt, warum. Sie fragt nie, wo die Nutzlast
steckt. Es ist ihr egal, ob der Code bei preinstall oder bei require()
zündet, ob der Hook vorhanden oder abwesend ist, ob --ignore-scripts
gesetzt ist. Eine vor neunzehn Minuten veröffentlichte Version wird
verweigert, weil sie neunzehn Minuten alt ist. Beide Kampagnen waren gegen
eine Zwei-Tage-Schwelle bei Ankunft tot — nicht weil Bromure die Nutzlast
erkannte, sondern weil es die Nutzlast überhaupt nie ansah.
Die Altersschwelle behauptet nicht, dass neue Pakete bösartig sind — die meisten sind in Ordnung, und eine zwei Tage alte Veröffentlichung ist immer noch sehr neu. Sie behauptet etwas darüber, wo sich das Risiko ballt. Das gefährliche Fenster einer Kompromittierung sind die ersten Stunden, nachdem ein gestohlenes Token eine Version hochgeschoben hat, bevor ein Maintainer es bemerkt und zurückzieht. Zwei Tage zu warten macht ein Paket nicht sicher, aber es rückt Ihre Installation aus genau dem Intervall heraus, in dem diese Angriffe leben, und tut das, ohne eine einzige Zeile des Codes zu inspizieren, den Sie gleich ausführen. Für ein Paket, das Sie am Tag seiner Veröffentlichung brauchen, nimmt die Schwelle eine Ausnahme pro Paket; alles andere wartet seine zwei Tage ab.
Wenn eine Version trotzdem alt genug ist
Eine Altersschwelle ist ein Filter, keine Mauer. Setzen Sie eine kürzere Grenze, nehmen Sie ein Paket aus, das Sie am ersten Tag brauchen, oder ziehen Sie eine bösartige Version zurück, die lange genug unentdeckt blieb, um über zwei Tage hinaus zu altern, und der vergiftete Code erreicht die Installation. Bromures Antwort für diesen Fall ist die, die die früheren Lieferketten-Beiträge beschreiben, und sie ist der Grund, warum die Altersschwelle es sich leisten kann, ein Filter statt der gesamten Verteidigung zu sein: Der Agent, der die Installation ausführt, ist nicht auf Ihrem Mac.
Er ist in einer wegwerfbaren Linux-VM, eine Hypervisor-Grenze entfernt.
Wenn also der AsyncAPI-Loader zündet und sein Credential-Harvester auf die
Suche geht — und er sucht gründlich, durchkämmt mehr als hundert
Umgebungsvariablen
darunter NPM_TOKEN, AWS_ACCESS_KEY und ANTHROPIC_API_KEY, dazu
.npmrc, ~/.aws/credentials, id_ed25519 und die kubeconfig auf der
Platte — findet er die Kopien der VM, die Platzhalter-Strings brm_… sind.
Die echten Werte leben auf dem Host. Wenn der Agent eine legitime Anfrage
stellt, die einen braucht, tauscht der Host-Proxy den falschen gegen den
echten Wert auf der Leitung, für diesen einen Hop, und tauscht ihn zurück;
das Geheimnis landet nie im Speicher der VM, wo der Harvester es abgreifen
könnte. Der ~/.ssh-Schlüssel, den die Nutzlast kopiert, ist ein
Wegwerf-Paar, das für dieses Profil erzeugt wurde. Und der C2-Upload, den
die Nutzlast versucht — nach 85.137.53.71, in diesem Fall — geht durch
denselben Host-Proxy, der jedes Ziel protokolliert, sodass ein
Node-Prozess, den Sie nie gestartet haben und der einen Socket zu einer
unbekannten IP öffnet, eine Zeile im Sicherheitsprotokoll ist, kein stiller
Transfer. Wenn das Fenster schließt, sind die VM und alles, was die
Nutzlast darin tat, weg.
Es ist dieselbe Miasma-Runtime-Familie, die Anfang dieses Jahres in Red Hats eigenen npm-Bereich hineinritt. Der Harvester ist ausgereift und wird über Kampagnen hinweg wiederverwendet; der Zustellmechanismus ist der Teil, der sich ständig ändert.
Die unbequeme Lektion genau dieser Woche ist, dass eine an einen Mechanismus gebundene Verteidigung eine Halbwertszeit von Tagen hat. Das Ökosystem verbrachte ein Jahr damit, Installationsskripte zur Sache zu machen, auf die man achten muss, brachte den Fix heraus und sah zu, wie zwei separate Akteure die Nutzlast verschoben, bevor die Release-Notes kalt waren. Dieses Rennen gewinnt man nicht, indem man den nächsten Mechanismus benennt, denn es gibt immer einen anderen Ort, an dem der Code zünden kann. Man gewinnt es, indem man etwas prüft, das der Angreifer nicht billig ändern kann — wie alt die Version ist — und indem man, was durchrutscht, dort ausführt, wo es nichts mitnehmen kann. Probieren Sie es aus.