PyPI-Lieferkettenangriff: Die LiteLLM-Lektion
Das manipulierte LiteLLM-Paket auf PyPI enthielt einen Credential-Stealer. Was Hash-Pinning und Lockfiles verhindern – und was nicht.

Drei Dateien. Mehr Spuren hinterließ einer der fiesesten Supply-Chain-Angriffe auf Python, die wir bisher gesehen haben: eine 12 Byte große cook.pth, eine 22 KB große cook.py und eine 25 KB große sitecustomize.py. Als LiteLLM 1.107.0 auf PyPI erschien, steckten diese drei Dateien in allen sieben Wheels des Releases. Und weil Pythons Startmechanik nun mal so funktioniert, liefen sie in jedem Python-Prozess auf einem infizierten Host – in deiner App, im CI-Runner, in pip selbst und sogar im Language Server deines Editors. Du musstest litellm nicht importieren. Du musstest es nicht einmal ausführen. Es reichte, irgendeinen Python-Interpreter zu starten.
Das LiteLLM-Team bestätigte die Kompromittierung, und PyPI zog das Release innerhalb weniger Stunden zurück. Das Zeitfenster war trotzdem lang genug. Die bösartigen Docker-Images – inklusive des Tags latest – waren rund zweieinhalb Stunden verfügbar. Wenn deine CI in dieser Zeit gepullt hat, hatte der Stealer bereits SSH-Schlüssel, Cloud-Zugangsdaten, Browserpasswörter, CI/CD-Secrets und Tokens für KI-Tools abgegriffen, verschlüsselt und an eine Domain des Angreifers geschickt. Das war kein Typosquatting und kein Look-alike-Paket. Es war das echte Paket, gebaut von der echten Pipeline und signiert vom echten Release-Prozess. Genau das sollte deine Sicht auf das Vertrauen in Abhängigkeiten verändern.
Wie drei Dateien die offizielle Release-Pipeline kaperten
Der Angreifer hat weder PyPI gehackt noch einen Commit in das öffentliche Repository geschmuggelt – die Upstream-Quellen auf GitHub sind sauber. Laut dem Incident Report von LiteLLM besaß der Angreifer die GitHub-CLI-Zugangsdaten dreier Entwickler, offenbar aus den Logs einer internen LiteLLM-Instanz, die aus dem Internet erreichbar war. Mit dem Token eines Organisations-Owners umging er den Branch-Schutz und richtete den privaten Release-Workflow auf ein Look-alike-Repository: Berri-AI/litellm statt BerriAI/litellm. Die Zugangsdaten eines zweiten Entwicklers erfüllten das Freigabe-Gate. Die offizielle Pipeline baute, signierte und veröffentlichte dann die Artefakte des Angreifers.
Die eigentliche Injektion war erstaunlich klein. Eine einzige zusätzliche Zeile in der pyproject.toml des sdist:
[tool.maturin]
python-source = "."
data = "cook" # <-- injected, copies cook/ into the wheel
python-packages = ["litellm_cli"] # legitimate, present upstream
Diese data = "cook"-Direktive weist maturin an, ein Verzeichnis cook/ in den purelib-Pfad des Wheels zu packen, und pip installiert es brav in die site-packages. Der sdist selbst ließ den Ordner weg. Wer das Release überflog, sah also nur eine kryptische Zeile und sonst nichts. Die Payload steckte in den Build-Artefakten, nicht im Quellcode. Ein Blick in die Git-Historie – die wir meist als Wahrheit betrachten – hätte dir gar nichts gezeigt.
Warum .pth-Dateien und sitecustomize.py ein Malware-Traum sind
Zwei Eigenschaften von Pythons Startsequenz machten diesen Angriff ungewöhnlich wirkungsvoll – und ehrlich gesagt sind es Features, über die die meisten Python-Entwickler nie nachgedacht haben. Erstens: Jede .pth-Datei im site-packages-Verzeichnis wird beim Start des Interpreters verarbeitet, und Zeilen, die mit import beginnen, werden ausgeführt. Die 12 Byte große cook.pth enthielt genau eine Zeile – import cook – und das reichte, um den Stealer in jedem Python-Prozess der Maschine laufen zu lassen. Zweitens: Python importiert beim Start automatisch ein Modul namens sitecustomize, sofern es eines im Pfad findet. Die Angreifer legten ihr eigenes ab. Es startete einen losgelösten Daemon, der stündlich eine auf GitHub gehostete Textdatei abrief, den dort hinterlegten Code herunterlud und mit exec() ausführte.
Das ist eine größere Sache als ein typisches bösartiges Paket mit einem manipulierten setup.py oder einem bösen Import in __init__.py. Die laufen beim Installieren oder Importieren des Pakets. Diese hier lief, sobald du irgendetwas in Python ausgeführt hast. Dein pytest-Lauf in der CI. Das pip list, mit dem du prüfen wolltest, was passiert ist. Der Black-Formatter in deiner IDE. Jeder einzelne Aufruf hat die Falle wieder scharf gemacht.
Lohnt sich ein Blick darauf, was der Stealer tatsächlich eingesammelt hat, denn die Liste liest sich wie eine Landkarte des modernen Entwickleralltags: AWS-, Azure- und gcloud-Zugangsdaten, kubeconfigs, GitHub-CLI-Tokens, SSH- und GPG-Schlüssel, Browser-Passwortspeicher, Shell-Historien, CI-Secrets von GitLab-Runnern und Jenkins, Terraform-Zugangsdaten, Umgebungsvariablen anderer Prozesse aus /proc/<pid>/environ, Instance-Metadaten-Zugangsdaten von AWS und – mit bitterer Ironie – Tokens für Claude Code, Cursor, Copilot und ein Dutzend weiterer KI-Coding-Tools, dazu die LLM-Provider-Schlüssel, die in der eigenen Proxy-Datenbank von LiteLLM lagen. Ein einziges pip install, und der Angreifer hat praktisch deine gesamte Identität als Ingenieur in der Hand.
Du musstest litellm nicht importieren. Schon der Start eines beliebigen Python-Interpreters auf dem Host reichte, um den Stealer auszuführen.
Hash-Pinning und Lockfiles: notwendig, aber nicht ausreichend
Jede Checkliste zur Supply-Chain-Sicherheit beginnt mit Lockfiles und Hash-Pinning, und das zu Recht. Wenn du Hashes mit pip install --require-hashes pinnst oder ein Tool wie uv oder Poetry mit gesperrten Hashes nutzt, wird ein Wheel abgelehnt, das sich nach deiner Prüfung still verändert hat. Das ist echter Schutz gegen eine ganze Klasse von Angriffen: Manipulation der Registry, Mirror-Poisoning, Austausch von Artefakten.
Aber ehrlich gesagt bringt es hier nichts, zumindest nicht für alle, die zuerst upgegradet haben. Hash-Pinning garantiert, dass das Artefakt, das du morgen installierst, byte-identisch mit dem von heute ist. In diesem Angriff waren die bösartigen Wheels die offiziellen Artefakte – aus erster Hand, von der Pipeline gebaut, konsistent gehasht. Sobald jemand sein routinemäßiges Upgrade gefahren, die neue Version aufgelöst und das aktualisierte Lockfile committet hat, waren die bösartigen Hashes mit einem grünen CI-Check im Repo festgeschrieben. Pinning schützt dich vor Veränderungen, sagt aber nichts über Absichten aus.
Wo Pinning in so einem Fall wirklich glänzt, ist der Blast Radius. Teams mit gepinnten Hashes und bewussten Upgrade-Rhythmen springen nicht automatisch in jedem Release, sobald es erscheint. Wenn deine Richtlinie lautet „Abhängigkeiten werden wöchentlich per PR aktualisiert, mit einer Karenzzeit von einigen Tagen für alles, was kein kritischer Fix ist“, dann war das Entfernen auf PyPI um 13:23 UTC schon vorbei, bevor dein Upgrade-Fenster überhaupt öffnete. Langweilige Prozesse schlagen clevere Tools öfter, als wir zugeben möchten.
Behalte also deine Lockfiles. Erzeuge sie mit Hashes. Hör aber auf, dir einzureden, sie seien eine Verteidigung gegen eine kompromittierte Maintainer-Pipeline. Sie schützen vor Drift und Manipulation und sind eine Bremse gegen schlechte Releases. Mehr nicht.

Provenance-Attestierung hätte das hier erkannt
Die unbequeme Kehrseite: Dieser Angriff hatte eine offensichtliche, erkennbare Auffälligkeit, und die dafür nötigen Tools existieren bereits heute. Die bösartigen Wheels wurden nicht aus dem öffentlichen Repository gebaut, sondern aus einem Look-alike-Repo, das der Angreifer angelegt hatte. Jeder Prüfschritt, der fragt „Wurde dieses Artefakt aus dem Commit und Repository gebaut, die es angibt?“, wäre laut gescheitert.
Genau das liefert Provenance-Attestierung. PyPI unterstützt PEP 740-Attestierungen: Wird ein Paket über Trusted Publishers aus GitHub Actions veröffentlicht, kann der Workflow eine signierte Aussage erzeugen, die das Artefakt an genau das Repository, den Workflow und den Commit bindet, aus dem es stammt. Eine Prüfung auf Konsumentenseite wird dann mechanisch: Attestierung verifizieren, bestätigen, dass das Quell-Repo BerriAI/litellm ist und nicht Berri-AI/litellm. Sigstore-Bundles, SLSA-Provenance und Verifikation im Stil von pip-audit gehören alle zu dieser Familie.
Die Lücke heute ist Verbreitung und Durchsetzung. Attestierungen sind für Publisher optional, und fast niemand prüft sie beim Installieren, selbst wenn sie existieren. Genau das muss sich ändern, und es passt zu einem Punkt, den wir schon einmal gemacht haben: Vertrauensinfrastruktur funktioniert nur, wenn tatsächlich jemand sie prüft. Eine Signatur, die niemand verifiziert, ist reine Dekoration. Wenn deine Organisation einen eigenen Paket-Index oder Proxy betreibt – und ab einer Handvoll Ingenieuren solltest du das tun –, ist „Artefakte ohne überprüfbare Herkunft aus der erwarteten Quelle werden abgelehnt“ eine Richtlinie, die du zentral und einmal für alle Entwickler durchsetzen kannst.
Es gibt außerdem eine subtilere Lektion. Der Angreifer brauchte drei Entwickler-Zugangsdaten: eine, um den Workflow umzuleiten, eine zur Freigabe und eine, damit niemand Verdacht schöpft. Das ist ein systemisches Versagen bei der Credential-Hygiene, kein einzelner Fehler. Kurzlebige, OIDC-basierte Tokens von Trusted Publishern auf PyPI hätten die Angriffsfläche drastisch verkleinert – denn es gibt kein langlebiges Token, das in einer Log-Datei auf den Diebstahl wartet. Wenn dein Projekt noch mit API-Tokens veröffentlicht, die als CI-Secrets hinterlegt sind, ist das hier dein Anstoß.
Sandboxed Installationen und die Argumente für Karenzzeiten
Nimm den schlimmsten Fall an: Ein bösartiges Release rutscht an deiner Lockfile-Hygiene und deinen Provenance-Prüfungen vorbei, weil irgendwann eines durchrutscht. Was bleibt dann? Eindämmung. Die Frage ist nicht mehr „Wie verhindern wir schlechte Pakete?“, sondern „Wenn ein schlechtes Paket läuft, woran kommt es heran?“
Für die CI ist die Antwort tatsächlich erreichbar. Builds sollten in ephemeren Umgebungen laufen, ohne langlebige Zugangsdaten, mit eng gefassten Tokens und Egress-Filterung. Das Verhalten des LiteLLM-Stealers ist im Netzwerk laut, wenn man hinhört: ein POST an eine unbekannte Domain mit einem eigenen Auth-Header, ein Abruf einer Raw-GitHub-URL in einer stündlichen Schleife und – das größte Warnsignal – ein Python-Prozess, der 169.254.169.254 abfragt, den Instance-Metadaten-Dienst von AWS. Anwendungscode hat fast nie einen legitimen Grund, IMDS direkt anzusprechen; das übernimmt das SDK. Alarmiere bei Python-Prozessen, die diesen Endpunkt berühren, und du fängst nicht nur diesen einen Stealer, sondern eine ganze Familie von Credential-Dieben. Wir haben bereits über die blinden Flecken beim Logging der Cloud-Authentifizierung geschrieben – das ist einer davon, und er lässt sich günstig schließen.
Auf Entwicklerrechnern ist es schwieriger, und ich werde so tun, als wäre es nicht so. Ein Laptop, auf dem derselbe Benutzer Editor, Browser, SSH-Agent und beliebige pip installs laufen lässt, ist ein Buffet für jeden Stealer. Die pragmatischen Schritte: Explorative Installationen in Containern oder Devcontainers statt in der Host-Umgebung; Paket-Installationen innerhalb des Projekt-Devcontainers halten, damit eine bösartige sitecustomize.py nie im site-packages deines Hosts landet; und ~/.aws/credentials als Risiko behandeln. SSO mit kurzlebigen Tokens (aws sso login, gcloud auth) bedeutet, dass deutlich weniger auf der Festplatte liegt, das man stehlen könnte. Nichts davon ist exotisch. Es ist nur Reibung – und Reibung ist genau der Sinn der Sache.
- Pinne Versionen und Hashes im Lockfile, behandle Upgrades aber als bewusste Ereignisse, nicht als Hintergrundrauschen.
- Führe eine Karenzzeit von 48 bis 72 Stunden ein, bevor nicht-kritische Abhängigkeits-Upgrades in Production-Builds landen. Die meisten bösartigen Releases werden innerhalb weniger Stunden zurückgezogen.
- Prüfe Provenance-Attestierungen, wo vorhanden, und bevorzuge Pakete, die über Trusted Publishers mit OIDC statt mit langlebigen API-Tokens veröffentlicht werden.
- Betreibe CI-Builds in ephemeren Umgebungen ohne Zugangsdaten mit Egress-Filterung; alarmiere bei jedem Python-Prozess, der Instance-Metadaten-Endpunkte anspricht.
- Installiere und führe Drittanbieter-Code auf Entwicklerrechnern in Containern aus und ersetze statische Cloud-Credential-Dateien durch kurzlebige SSO-Sitzungen.
- Prüfe deine Umgebung auf die IoCs, falls du im Zeitfenster etwas gezogen hast:
cook.pth,cook.pyoder eine fremdesitecustomize.pyim site-packages sowie/tmp/.pc_once_*oder/tmp/remote_code_loader.lockauf der Festplatte.
Was dieser Angriff über den eigentlichen Risikoort verrät
Beachte, was der Angreifer nicht versucht hat. Er hat keine Code-Schwachstelle ausgenutzt und keinen Zero-Day gebraucht. Er ist das weichste Ziel der modernen Software angegangen: die Menschen und Maschinen, die dauerhafte Zugangsdaten halten. Drei GitHub-CLI-Tokens, offenbar aus den Logs einer aus dem Internet erreichbaren internen Instanz, reichten, um eine Release-Pipeline zu übernehmen, die Millionen Downloads ausliefert. Dieses Muster beobachten wir in der Branche immer wieder: Secrets wuchern in Logs, Agents und Konfigurationsdateien, und jedes einzelne ist ein Generalschlüssel. Wenn du eine interne Instanz von irgendetwas betreibst und ins Internet exponierst, gehören seine Logs jetzt zu deinem Threat Model. Dieser Satz sollte ein paar Leute unruhig machen.
Außerdem gibt es ein Tempo-Problem, das man ansprechen sollte. LiteLLM ist ein schnelllebiges Projekt – Zehntausende Commits, häufige Releases – und steht im Zentrum des KI-Tool-Stacks, eingebunden über Frameworks und SDKs, von denen viele Teams nicht einmal wissen, dass sie davon abhängen. Transitive Reichweite bedeutet, dass du deine Betroffenheit nicht über die Wahl deiner direkten Abhängigkeiten steuern kannst. Eine SBOM ist in dieser Welt keine Compliance-Papierarbeit mehr; sie ist der Weg, „Sind wir betroffen?“ in Minuten statt Tagen zu beantworten. Tools, die eine Stückliste aus echten Build-Nachweisen erzeugen statt aus Manifesten, finden die Abhängigkeiten, die in deiner requirements.txt gar nicht auftauchen.

Meine Meinung: Die Branche hat auf Supply-Chain-Angriffe bisher zu stark mit Präventionstheater reagiert – Scanner, die bekannte schlechte Versionen nachträglich markieren – und zu wenig auf die unspektakulären Kontrollen gesetzt, die diese Geschichte verändert hätten. Kurzlebige Publishing-Zugangsdaten. Provenance-Prüfung am Proxy. Karenzzeiten. Egress-Alarme an Metadaten-Endpunkten. Sandboxed Installationen. Keine dieser Maßnahmen ist ein Forschungsproblem. Es sind Konfigurationsentscheidungen, die heute verfügbar sind, und die meisten Teams haben davon keine einzige umgesetzt. Die LiteLLM-Angreifer brauchten eine schlechte Zeile in einer Build-Konfiguration und drei gestohlene Tokens. Die oben genannten Schutzmaßnahmen hätten sie gezwungen, vier unabhängige Ebenen statt einer zu überwinden. Das ist Defense in Depth – nicht mehr Scanner, sondern mehr unabhängige Wege, auf denen der Angreifer scheitern kann. Dahinter steht dieselbe Philosophie wie bei Systemen, die Katastrophen widerstehen, statt darauf zu hoffen, dass schon nichts passiert.
Die nächste kompromittierte Version wird bereits von jemandem geplant. Du wirst nicht verhindern können, dass sie veröffentlicht wird. Aber du kannst entscheiden, ob sie mit deinen AWS-Schlüsseln in Reichweite läuft, mit einem entsperrten SSH-Agent und einem klaren Netzwerkweg zu einer Domain des Angreifers – oder ob sie in einem ephemeren Container ohne Zugangsdaten und hinter einer Firewall stirbt. Diese Entscheidung fällt jetzt, in deiner CI-Konfiguration und deinem Devcontainer-Setup, nicht an dem Tag, an dem der Incident Report erscheint.


