„In den ersten zwölf Tagen, nachdem ich einen Proxy zwischen unsere Firma und die öffentlichen Paketregistrierungen gesetzt hatte, hat er 71 Downloads abgelehnt“, schreibt der Entwickler in seinem Bericht. Auf den ersten Blick klingt das nach einer Fehlkonfiguration, nach einem Werkzeug, das im Weg steht statt zu schützen. Die abgelehnten Pakete waren aber gewöhnliche Veröffentlichungen von vite, rollup, swc und caniuse-lite. Ihr einziges Vergehen: Sie waren zu neu.
Die Regel ist einfach. Eine neue Version aus npm oder PyPI wartet sieben Tage, bevor das Tor sie an irgendjemanden im Unternehmen ausliefert. Der Titel des Originalbeitrags übertreibt ein wenig, und das räumt der Autor selbst ein: pub-Pakete warten drei Tage, Maven und Go werden nur zwischengespeichert, und einige Quellen laufen gar nicht durch das Tor. Für den Kern der Sache spielt das keine Rolle. Es geht um Software-Lieferkettensicherheit mit einem einzigen Hebel: Zeit.
Interessant ist der Bericht, weil er nicht bei der Idee stehen bleibt. Er nennt Zahlen aus dem Produktivbetrieb, listet auf, was dabei kaputtgegangen ist, und benennt die Grenzen des Verfahrens. Wer über Dependency-Sicherheit und Paketmanager-Sicherheit nachdenken will, findet dort mehr als den üblichen Verweis auf die nächste Scanner-Suite.
Warum sieben Tage: fünf Angriffe und ihre Standzeiten
Der Auslöser war banal: Jeden Morgen landet eine Zusammenfassung neuer Sicherheitshinweise zu den Abhängigkeiten der eigenen Repositories im Chat, und in diesem Jahr kam gefühlt alle paar Wochen ein neuer Supply-Chain-Angriff dazu. Das Team hatte außerdem selbst schon einen Sicherheitsvorfall. Der Autor nahm sich deshalb fünf Übernahmen aus dem Jahr vor, für die es eine belastbare Zeitleiste gab, und notierte für jede nur eine Zahl: wie lange die vergiftete Version online stand, bevor sie entfernt wurde.
Bei axios 1.14.1 am 31. März übernahm ein Angreifer das npm-Konto eines Maintainers und veröffentlichte zwei Releases, die beim Installieren einen Remote-Access-Trojaner nachluden. Die Versionen waren rund drei Stunden erreichbar. Die Pakete lightning 2.6.2 und 2.6.3 auf PyPI hielten sich am 30. April nur 42 Minuten. Der Fall TanStack am 11. Mai umfasste 84 bösartige Versionen verteilt über 42 Pakete im @tanstack-Namensraum; npm entfernte das letzte Tarball vier Stunden und 35 Minuten nach dem ersten.
Am 4. August begann mit keyv 6.0.0 der ChainDrop-Wurm. Bis keyv zurückgesetzt war, vergingen etwa eine Stunde und 40 Minuten, während sich der Wurm innerhalb von vier Stunden selbst in hunderte weitere Pakete hineinschrieb. Der Fall litellm 1.107.0 auf PyPI am 21. Oktober war nach einer Stunde und 43 Minuten vorbei, und auch hier war der Schaden heikel: Die Nutzlast steckte in einer .pth-Datei und lief bei jedem Start von Python in dieser Umgebung, ganz ohne Import.
Alle fünf waren weg, bevor der erste Tag vorbei war. Entscheidend ist aber, was vorher passiert: Ein frischer Install greift eine neue Version innerhalb von Sekunden ab, und Huntress verzeichnete die erste axios-Infektion 89 Sekunden nach der Veröffentlichung. Eine Woche Wartezeit überdeckt dieses Fenster mit Tagen Reserve. Signierte Builds helfen dabei übrigens nicht, denn die bösartigen TanStack- und keyv-Versionen stammten aus den Release-Workflows der Projekte selbst und trugen gültige Provenance. Der Autor zitiert dazu einen Satz: Provenance beweise, welcher Commit gebaut wurde, nicht, dass dieser Commit autorisiert war.
Wie das Tor funktioniert: Wartezeit, Blocklist und Download-Log
Die Wartezeit ist nach Ökosystem gestaffelt und folgt dem Risiko. Bei npm, pnpm, Yarn und Bun sind es sieben Tage, bei pip und uv ebenfalls sieben, bei pub aus dem Dart- und Flutter-Umfeld drei. Maven, Gradle und Go bekommen keine Wartezeit, werden aber zwischengespeichert und lassen sich über die Blocklist sperren. CocoaPods, Google Maven und Docker-Images laufen gar nicht erst durch das Tor.
Zu neue Versionen werden aus der Versionsliste ausgeblendet, und ein Download antwortet mit einem 404, selbst wenn ein Lockfile genau diese Version festnagelt. Bei pub gibt es eine Ausnahme: Eine Version, die bereits in der pubspec.lock steht, wird trotzdem ausgeliefert. Der Paketmanager greift in der Regel einfach zur neuesten erlaubten Version, sodass im Alltag kaum etwas auffällt.
Dazu kommen zwei Dinge, die sich nicht per Konfigurationsschalter erledigen lassen. Die Blocklist besteht aus einer Zeile mit der exakten Version, etwa pkg:npm/chalk@5.6.1, einem Neustart von ein bis zwei Sekunden und der Folge, dass jeder Download dieser Version mit 403 antwortet, gecachte Kopien eingeschlossen. Sie ist bislang leer. Das Download-Log ordnet dagegen jeden Abruf einem Google-Konto, einem Service-Token oder dem Eintrag runners für die selbstgehosteten CI-Läufe zu. Wenn etwas schiefgeht, ist die Frage, wer es schon installiert hat, eine einzige Abfrage statt eines langen Chat-Verlaufs.
Drei Wege führen hinein: Menschen mit einem persönlichen Token von einer Seite hinter dem Google-Login, die eigenen CI-Runner über ihre IP-Adresse und gehostete CI über ein Service-Token. Auf den Firmen-Macs richtet eine Self-Service-App in Fleet die gängigen Paketmanager in einem Rutsch ein, damit niemand eine .npmrc von Hand schreibt.
Warum ein Proxy und nicht nur eine Einstellung im Repo
Viele Paketmanager können das inzwischen selbst, meist unter dem Namen Cooldown oder Mindestalter. npm kennt min-release-age seit Version 11.10, und pnpm 11 wartet sogar standardmäßig einen Tag. Der Autor hat sich trotzdem für den Proxy entschieden, und die Begründung ist Organisationslehre, nicht Technik: Eine Einstellung schützt genau das Repository, in dem sie liegt, und ein vergessenes Dockerfile oder der Laptop einer Kollegin liest sie nie.
Ein Proxy ist dagegen ein Ort, durch den alles hindurchmuss, und die eigenen CI-Runner dürfen nicht daran vorbei. Derselbe Ort trägt die Blocklist und das Download-Log, und keine Einstellung kann dir sagen, wer ein Paket bereits installiert hat. Außerdem bleibt eine Konfigurationsdatei für npm, PyPI, pub, Maven und Go überschaubar, wenn nur etwa 60 der 113 aktiven Repositories JavaScript sind, während 22 Gradle oder Maven nutzen, 16 pub und 14 PyPI.
Verdaccio war der erste Kandidat, weil es seit 6.4 einen Mindestalter-Filter mitbringt. Der filtert allerdings nur npm, und die eigene Dokumentation nennt ihn eine Darstellungsregel, keine Zugriffskontrolle. Gewählt wurde deshalb git-pkgs/proxy: ein einzelnes Go-Binary, das npm, PyPI, pub, Maven, Go und weitere Ökosysteme cacht und Cooldown wie Blocklist mitbringt. Es ist jung, Version 0.10 nach etwa neun Monaten, steht unter GPL-3 und hat kein eigenes Login, weshalb ein kleiner Token-Dienst und der Google-Login davor sitzen. Den Proxy selbst hält das Team inzwischen an dieselbe Regel – einmal gebrochen, als man zwei Tage nach Erscheinen auf v0.10.0 wechselte.
Wer klein anfangen will, braucht keinen Proxy. Die meisten Werkzeuge haben den Cooldown eingebaut, und die Werte sind über die Jahre gewachsen:
| Werkzeug | Sieben Tage | Ort | Seit |
|---|---|---|---|
| npm | min-release-age=7 | .npmrc | 11.10 |
| pnpm | minimumReleaseAge: 10080 (Minuten) | pnpm-workspace.yaml | 10.16 |
| Yarn | npmMinimalAgeGate: 1w | .yarnrc.yml | 4.10 |
| Bun | minimumReleaseAge = 604800 (Sekunden) | bunfig.toml | 1.3 |
| uv | exclude-newer = 7 days | uv.toml | 0.9.17 |
| pip | –uploaded-prior-to=P7D | Kommandozeile oder pip.conf | 26.1 |
| Dependabot | cooldown: default-days: 7 | dependabot.yml | 2025 |
| Renovate | minimumReleaseAge: 7 days | renovate.json | 2023 |
Zwölf Tage in Zahlen: 81.710 Downloads und 71 Absagen
Zwischen dem 20. Oktober und dem 1. November liefen 81.710 Paket-Downloads durch das Tor, verteilt auf 14 Konten aus Menschen, Service-Tokens und CI-Runnern. Den größten Anteil hatte npm mit 69.824, danach folgten pub mit 5.650, Maven mit 5.012, PyPI mit 675 und Go mit 549. Aus den Versionslisten verschwanden in derselben Zeit 6.116 Versionen, weil sie zu jung waren, davon 6.049 auf npm und 67 auf pub.
Die 71 Absagen sind der Preis des Verfahrens, und sie entstehen an einer bestimmten Stelle: Ein Lockfile pinnt bereits eine Version, die noch zu frisch ist. Die üblichen Verdächtigen waren vite 8.3.5, rollup 4.65.1, @swc/core 1.16.20, caniuse-lite und electron-to-chromium. Auf der Blocklist stand am Ende des Zeitraums kein einziger Eintrag, was der Autor gern so belassen würde.
Was der Proxy kaputt gemacht hat
Die Absage sieht im Terminal unspektakulär aus. npm ci bricht mit npm error code E404 ab, und für electron-to-chromium-1.5.472.tgz heißt es schlicht, die Version sei nicht gefunden. Das ist keine kaputte Registry, sondern die Wartezeit in Aktion: Irgendjemand hat die Version ohne das Tor aufgelöst, und der Lockfile-Eintrag zeigt nun auf etwas, das noch nicht ausgeliefert wird. Die Lösung ist entweder ein neu aufgelöster Lockfile oder Geduld.
Nicht jeder Lockfile folgt der Registry-Einstellung, und das ist der Fallstrick. npm- und pnpm-Locks sind unproblematisch, Yarn 1 holt jedoch die exakte URL aus der yarn.lock und umgeht das Tor, bis der Lock regeneriert wird. Das alte binäre bun.lockb enthält fest verdrahtete npmjs-URLs, uv sync –frozen lädt direkt vom Dateihost von PyPI, und pub get löst die pubspec.lock beim ersten Kontakt mit dem Tor neu auf, sodass Versionen nach oben wandern können. Poetry kennt gar keine Registry-Variable und braucht eine Source in der pyproject.toml plus ein neues Lock. Bei pnpm 11 heißt die Variable PNPM_CONFIG_REGISTRY und nicht NPM_CONFIG_REGISTRY.
Auch die Automatisierung muss mitziehen: Dependabot und Renovate dürfen nicht schneller sein als das Tor, sonst scheitert jedes Update-PR an einer Version, die jünger als eine Woche ist. Dependabot steht standardmäßig bei drei Tagen und braucht cooldown mit default-days sieben, Renovate die entsprechende Einstellung. Ein weiterer blinder Fleck: npm audit geht unverändert an das Tor vorbei zu npmjs.org und weiß von der Wartezeit nichts. Und wenn der Proxy ausfällt, kann die CI nichts installieren, weil die selbstgehosteten Runner nicht außen herum dürfen – das ist Absicht und macht das Tor zu einem genauso kritischen Baustein wie die Runner selbst.
Was eine Woche nicht leistet
Ein Cooldown hilft nur, wenn innerhalb dieser Woche jemand hinsieht. Meistens tut das jemand: Die Auswertung eines Bloggers zählte 1.336 gekaperte npm-Pakete anhand von OSV-Hinweisen und eigener Definition, und ein siebentägiger Cooldown hätte rund 85 Prozent davon blockiert. Die restlichen Fälle zeigen, wo der Ansatz endet, und sie sind konkret.
Im März veröffentlichte ein übernommenes npm-Konto bösartige Versionen von react-native-international-phone-number. Als der Autor am 1. November die Registry prüfte, war 0.12.3 noch 228 Tage nach der Veröffentlichung als latest getaggt, und das Paket kam laut Endor Labs in den vorangegangenen 30 Tagen auf etwa 134.000 Downloads. Die schädliche Abhängigkeit ist inzwischen entfernt, die Version tut also vermutlich nichts mehr. Der Hintertür-Code in xz-utils wiederum erschien am 24. Februar 2024 mit Version 5.6.0 und wurde am 29. März gefunden, also nach 34 Tagen; eine Woche Wartezeit wäre 27 Tage vor dem Fund abgelaufen.
Dazu kommt ein struktureller Punkt: Ein solcher Filter schaut nie in ein Paket hinein, sondern nur auf Veröffentlichungsdaten. Eine vergiftete Version, die eine Woche lang niemandem auffällt, kommt am achten Tag durch. Die Wartezeit kauft also keine Sicherheit, sondern Zeit. Zeit, in der andere genauer hinschauen. Wenn sie es tun, nimmt die Blocklist die Version heraus, und das Download-Log sagt, wer sie zuerst geholt hat. Der Filter fängt die wahrscheinlichen Angriffe ab und macht die seltenen leisen Fälle wenigstens nachvollziehbar. Mehr verspricht er nicht.
vite 8.3.5 kam übrigens doch noch an, veröffentlicht an einem Mittwoch und durchgelassen am Mittwoch darauf. Dem Proxy ist egal, wie beliebt ein Paket ist. Er zählt nur das Alter. Wer anfangen will, kann das im Repo tun oder gleich einen Proxy davorstellen.
Quelle: brunoj.com
