← Zurück zu allen Beiträgen
Veröffentlicht am · von Renaud Deraison

Der alte Name konnte immer noch veröffentlichen

Am 13. Oktober hat Bloom Security SlugFest veröffentlicht: Das Trusted Publishing von npm erkannte ein GitHub-Repository an seinem Eigentümer/Namen-String, sodass jeder, der sich den alten Namen eines umbenannten oder gelöschten Repositorys sicherte, neue Versionen fremder Pakete veröffentlichen konnte, ganz ohne gestohlene Zugangsdaten. Bloom fand mehr als 1.500 betroffene Pakete mit über 10 Millionen Downloads und nennt KI-Agenten als die Installierenden, die eine neue Version übernehmen, ohne dass jemand hinsieht. In Bromure Agentic Coding existiert eine so junge Version für den Agenten nicht, die Installation läuft in einer VM mit Platzhaltern, und die Umbenennung, die die Lücke öffnet, wartet auf einen Klick.

Das Trusted Publishing von npm merkte sich den Namen des Repositorys und vergaß, welches Repository es war. Als der Maintainer es umbenannte, konnte die nächste Person, die ein Repository unter diesem Namen anlegte, veröffentlichen.

Am Montag, dem 13. Oktober, hat Golan Myers von Bloom Security SlugFest veröffentlicht, eine Schwachstelle im Trusted Publishing von npm. The Hacker News berichtete noch am selben Tag. Myers erklärt sie mit einem Schloss: „Es ist wie ein Schloss, das die Adresse an der Tür prüft statt den Besitzer des Hauses; wenn der Besitzer auszieht, bekommt der Nächste, der einzieht, den Schlüssel.“

Bloom zählte mehr als 1.500 betroffene Pakete, insgesamt über 10 Millionen Mal heruntergeladen und mehr als eine Million Mal im Monat. Der Angriff braucht „kein Insiderwissen, keinen Diebstahl von Zugangsdaten und keine Race Condition“. Bloom hat die Pakete nicht genannt: „Wir veröffentlichen keine Liste der betroffenen Pakete, da das Problem noch weit verbreitet ist.“

Bloom sagt auch, wer diese Pakete heute installiert. Nach seiner Zählung installiert ein Coding-Agent im Schnitt 70 Pakete im Monat, ein Entwicklerrechner etwa 350. In Blooms Worten wird eine bösartige Version, die über einen zurückeroberten Namen veröffentlicht wurde, „gezogen und läuft ohne Menschen in der Schleife“.

Wie Trusted Publishing das Repository prüfte

npm hat Trusted Publishing im Juli 2025 für alle geöffnet. Statt ein npm-Token in der CI zu speichern, führt ein Maintainer npm trust github --allow-publish aus und teilt npm mit, welches GitHub-Repository und welche Workflow-Datei das Paket veröffentlichen dürfen. Beim Veröffentlichen stellt GitHub Actions ein kurzlebiges OIDC-Token aus, das den Workflow-Lauf beschreibt, npm gleicht es mit der Bindung ab und gibt eine Veröffentlichungsberechtigung zurück. Es gibt kein langlebiges Geheimnis, das man stehlen könnte.

Die Bindung speicherte das Repository als Slug, owner/repo, plus den Namen der Workflow-Datei. Die numerischen repository_id und repository_owner_id, die GitHub auf Lebenszeit vergibt, speicherte sie nicht. npm hielt sie erst nach der ersten erfolgreichen Veröffentlichung fest. Bis dahin passte jedes Repository, das auf den richtigen Namen hörte und die richtige Workflow-Datei ausführte.

Namen auf GitHub werden frei. Ein Maintainer benennt ein Repository um, überträgt es an eine Organisation, löscht es oder benennt sein Konto um. Der alte Slug wird verfügbar, und jemand anderes kann ihn registrieren. Blooms Schritte:

  1. Ein Repository unter dem freigewordenen Slug anlegen. GitHub vergibt eine neue numerische ID.
  2. Einen Workflow mit dem Dateinamen aus der Bindung und der Berechtigung id-token: write hinzufügen und ausführen.
  3. GitHub stellt ein OIDC-Token aus, dessen Repository-Claim der alte Slug ist.
  4. Der Austausch-Endpunkt von npm gleicht Slug und Dateinamen ab und stellt eine Veröffentlichungsberechtigung aus. Die numerischen IDs stehen im Token, aber npm hatte nichts, womit es sie vergleichen konnte.
  5. Eine neue Version eines Pakets veröffentlichen, das einem nicht gehört, mit seinem Namen, seinen Downloadzahlen und jedem Projekt, das davon abhängt.

Bloom hat die Kette durchgängig gegen einen npm-Scope und Repositorys durchgespielt, die es selbst kontrolliert, und dabei aus einer zweiten Organisation veröffentlicht, bei der nur der Slug passte. Das Trusted Publishing von GitLab hatte dieselbe Lücke.

Das so entstandene Release ist obendrein signiert. Trusted Publishing aus einem öffentlichen Repository erzeugt eine Provenance-Attestierung, und diese würde den Workflow-Lauf im Repository des Angreifers unter dem Slug nennen, den der Maintainer konfiguriert hat. Im August brachte ein Zwei-Wort-Kommentar zu einem Pull Request die eigene Release-Pipeline eines Projekts dazu, zehn vergiftete Versionen mit gültiger Provenance zu veröffentlichen. SlugFest braucht die Pipeline des Opfers gar nicht.

Ein freier Name, ein passender Workflow, eine Berechtigungnpm-trust-Bindungrepo: acme/sdkworkflow: release.ymlrepo_id: (fehlt)Maintainer benennt umacme/sdk → acme/sdk-v2„acme/sdk“ ist freifür jeden NeuenNeues Angreifer-Repo acme/sdkrepository_id: new.github/workflows/release.ymlpermissions: id-token: writeOIDC-Token: repository = acme/sdknpm-Austausch: Slug ✓ Workflow ✓IDs: nichts zum Vergleichen→ Berechtigung ausgestelltnpm-Registry@acme/[email protected]als latest getaggtmit ProvenanceEin Coding-Agent:npm install @acme/sdklöst latest auf,führt es aus, unbeobachtetNamen erfunden. Bloom fand mehr als 1.500 Pakete in diesem Zustand und hat die Liste nicht veröffentlicht.
Die SlugFest-Kette. Die Bindung speichert einen Namen, eine Umbenennung gibt den Namen frei, und ein neues Repository unter dem alten Namen besteht die Prüfung von npm und veröffentlicht unter dem ursprünglichen Paket.

Vier Monate bis zum Fix

Bloom meldete die Lücke am 6. Juni über das HackerOne-Programm von GitHub. GitHub bestätigte sie am 9. Juni. Die letzte inhaltliche Antwort kam am 8. Juli: Das Team arbeite „daran, das Problem zu beheben und sein Risiko und seinen Schweregrad zu bestimmen“. Sechs Nachfragen und eine Vermittlungsanfrage an HackerOne blieben unbeantwortet.

GitHubs Changelog vom 2. Oktober enthält den Fix. Eine Trusted-Publishing-Bindung, die innerhalb von 48 Stunden nach ihrer Erstellung nichts veröffentlicht hat, läuft jetzt ab und kann keine Veröffentlichung mehr autorisieren. Nach der ersten erfolgreichen Veröffentlichung ist die Bindung an die numerische Identität des Repositorys geknüpft. Ein Wechsel des Repositorys erfordert eine neue Bindung mit einem neuen 48-Stunden-Fenster. npm lehnt außerdem jetzt Tokens ab, die von issue_comment-Workflows ausgestellt wurden, dem Auslöser des Angriffs im August.

Der Changelog sagt nicht, was mit Bindungen geschieht, die vor dem 2. Oktober angelegt wurden. Bloom rät Maintainern, npm trust list für jedes Paket auszuführen, zu prüfen, ob jeder Slug noch auf ein Repository zeigt, das sie kontrollieren, und die Bindung nach jeder Umbenennung oder Übertragung neu anzulegen.

Wer Pakete installiert statt veröffentlicht, hat von diesem Rat nichts. Die Trust-Bindung eines Pakets ist von npm install aus nicht zu sehen. Die 1.500 Namen sind unveröffentlicht. Ein bösartiges Release aus einem zurückeroberten Slug sieht aus wie jedes andere, Provenance inklusive. Blooms eigener Satz zum Agenten-Fall: „Es muss nur das Paket sein, das ein Agent auswählt.“

Dieselbe Installation auf einer Bromure-Maschine

In Bromure Agentic Coding läuft der Agent in einer Linux-VM auf Ihrem Mac, und jede Anfrage der VM, registry.npmjs.org eingeschlossen, geht durch einen Proxy auf dem Host. Die Lieferketten-Richtlinie läuft in diesem Proxy, außerhalb der VM, sodass nichts, was der Agent oder ein Paket in der Maschine tut, sie ändern kann. Verfolgen wir das SlugFest-Release in eine Bromure-Maschine.

Eine so junge Version wird nicht gelistet

Die Altersschranke ist in jeder neuen Maschine standardmäßig aktiv, mit einem Minimum von zwei Tagen. Wenn npm die Registry nach @acme/sdk fragt, entfernt der Proxy jede Version, die in den letzten zwei Tagen veröffentlicht wurde, aus der Liste und setzt latest auf die neueste Version, die alt genug ist.

Der Agent führt npm install @acme/sdk aus, denselben Befehl wie überall sonst, und bekommt das letzte echte Release des Maintainers. Die Version des Angreifers steht nicht in der Liste, die der Agent sieht. Pinnt er sie per Nummer, weil eine Webseite oder ein Issue es ihm gesagt hat, bekommt die Tarball-Anfrage HTTP 451, und npm gibt den Grund im Terminal des Agenten aus:

Bromure Supply-Chain Security blocked this request: npm package @acme/[email protected]
published 3 hours ago — policy requires 2 days minimum

Die Schranke arbeitet allein mit dem Veröffentlichungsdatum, Sie brauchen also weder Blooms Liste noch eine Malware-Signatur. Wer einen Slug zurückerobert, muss eine neue Version veröffentlichen, und eine neue Version ist eine junge. Während der Agent die vorherige Version installiert, haben Maintainer, npm und Scanner zwei Tage Zeit, ein Release zu bemerken, das niemand im Team gemacht hat. Ein Maintainer, dessen Bindung nie benutzt wurde, hat guten Grund hinzusehen, wenn sie zum ersten Mal veröffentlicht.

Weitere Prüfungen nach zwei Tagen

Schalten Sie im Bereich Supply Chain der Maschine weitere Ebenen ein, und die Version trifft auf sie, sobald sie die Schranke passiert hat. Mit ausgewähltem socket.dev wird jeder Tarball geprüft, bevor er ausgeliefert wird, und als Malware markierte Pakete bekommen ein 451. Mit eingeschalteter OSV-Prüfung wird eine Version mit veröffentlichtem Advisory ab dem gewählten Schweregrad blockiert. Mit Strip preinstall / install / postinstall / prepare from npm tarballs on the fly entfernt der Proxy die Install-Hooks aus dem Tarball, bevor npm ihn sieht, und nimmt damit den Auslieferungsweg weg, auf den sich die meiste npm-Malware verlässt.

Die Nutzlast findet Platzhalter

Angenommen, ein Release kommt doch durch und läuft. Es läuft in der VM, und die Zugangsdaten dort sind Platzhalter. GH_TOKEN, ~/.git-credentials, der Anthropic-Schlüssel, die AWS-Schlüssel: Jeder ist ein String in der Form des echten, abgeleitet aus dem echten Wert und einem Salt pro Installation, und nutzlos außer über den Proxy auf Ihrem Mac, für Anfragen an den Host, zu dem er gehört. Eine Nutzlast, die sie an ihren eigenen Server schickt, schickt Strings, die überall an der Authentifizierung scheitern.

Auch diese Anfrage geht durch den Proxy, der jede ausgehende Anfrage nach Platzhaltern durchsucht, die an den falschen Host gehen. Ein ghp_-Platzhalter auf dem Weg zu einem Sammelserver bekommt ein 451, die VM wird angehalten, und auf dem Mac öffnet sich This environment may have been compromised mit dem Namen der Zugangsdaten, dem Host, zu dem sie gehören, und dem Host, an den sie geschickt wurden.

Ihre Umbenennung wartet auf einen Klick

SlugFest beginnt mit einer Umbenennung, einer Übertragung oder einem gelöschten Repository, und auch Agenten erledigen solche Aufräumarbeiten. In einer Bromure-Maschine steht GitHub standardmäßig unter Prompt before write. Ein Repository umzubenennen ist ein PATCH an die GitHub-API, eines zu löschen ein DELETE. Beides hält an einem macOS-Dialog an, Allow write on "github.com" from workspace "…"?, der Methode und Pfad so zeigt, wie sie über die Leitung gingen. Wenn ein Agent ein Projekt aufräumt und acme/sdk in acme/sdk-v2 umbenennt, sehen Sie es, bevor der alte Name frei wird, und wissen, welche npm-Bindung Sie neu anlegen müssen.

Wenn die Liste erscheint

Bloom wird die betroffenen Pakete irgendwann veröffentlichen. Dann wird jemand fragen, welche Ihrer Maschinen eines davon installiert hat, und wann. Die Security Timeline zeichnet jeden Paketabruf jeder Maschine auf, unabhängig von der Richtlinie: npm · @acme/[email protected], die Maschine, die Uhrzeit und ob es heruntergeladen, von der Altersschranke zurückgehalten oder blockiert wurde. Filtern Sie nach dem Paketnamen und exportieren Sie das Ergebnis als CSV. Das Protokoll liegt auf dem Mac, wo der Agent es nicht bearbeiten kann.

In Bromure: Für den Agenten gibt es die neue Version noch nichtMaschinen-VM (Ubuntu)$ npm install @acme/sdk+ @acme/[email protected]$ npm i @acme/[email protected]451 published 3 hours agoGH_TOKEN=ghp_… (Platzhalter)ANTHROPIC_API_KEY=sk-ant-api03-brm-…kein echtes Geheimnis in der VMProxy, auf dem MacAltersschranke (Standard 2 Tage):jüngere Versionen weg, latest → 4.1.3Optional: socket.dev, OSV,Install-Skripte entfernenPlatzhalter an fremden Host → 451, VM hältPATCH /repos/acme/sdk (Umbenennung)→ Prompt-before-write-DialogRichtlinie auf dem Host, außer Reichweite des Agentennpm-Registry4.2.0 3 Stunden altvom zurückeroberten Slug4.1.3 5 Monate altRelease des Maintainerszwei Tage, in denenMaintainer, npm undScanner es merkenSecurity Timeline, auf dem Macnpm · @acme/[email protected] · heruntergeladen · Maschine, Zeit · nach Paket filtern, CSV-Export, wenn die Liste da ist
Dieselbe Installation auf einer Bromure-Maschine. Die frische Version fehlt in der Liste, die der Agent sieht, eine gepinnte Anfrage bekommt ein 451, eine Nutzlast, die doch läuft, findet nur Platzhalter, und jeder Abruf wird protokolliert.

Die neue Version wird nicht gelistet

Die Altersschranke, standardmäßig auf zwei Tage gesetzt, entfernt frische Versionen aus der Registry-Liste und setzt latest zurück. Der Agent installiert das letzte echte Release des Maintainers, ohne zu wissen, dass etwas passiert ist.

Gepinnte Anfragen bekommen ein 451

Fragt der Agent die neue Version per Nummer an, wird der Download verweigert, und npm zeigt das Alter des Pakets und die Richtlinie im Terminal des Agenten an.

Die Nutzlast findet Platzhalter

Alles, was in der VM läuft, findet Platzhalter-Tokens. An den falschen Host geschickt, bekommt einer ein 451, die VM hält an und der Mac zeigt eine Warnung.

Umbenennungen und Installationen sind protokolliert

Prompt before write hält die Repository-Umbenennung an, die einen Slug freigibt. Die Security Timeline zeichnet jeden Paketabruf auf, nach Maschine und Version.

Der nächste Name

GitHubs Fix knüpft eine Bindung nach ihrer ersten Veröffentlichung an die numerische Identität des Repositorys und lässt Bindungen ablaufen, die nie veröffentlichen. Das schließt diese Lücke. Trusted Publishing schützt die Veröffentlichungsberechtigung und sagt Ihnen, welche Pipeline das Release gebaut hat. Im August war die Pipeline die des Opfers, ausgelöst durch den Kommentar eines Fremden. Diesmal hörte das Repository eines Fremden auf den richtigen Namen. Beide Releases trugen Provenance.

Ihre Agenten installieren, was der Resolver zurückgibt, nach Blooms Zählung 70 Pakete im Monat. Lassen Sie sie in einer Bromure-Maschine laufen, wo ein zwei Tage altes Release noch nicht existiert und eine Nutzlast, die doch läuft, nur Platzhalter findet. Mehr zur Altersschranke lesen Sie in Jeder hätte dieses Paket veröffentlichen können und Das Widerrufen des Tokens war der Auslöser.