LiteLLM 1.107.0: Eine Zeile Build-Konfiguration macht jeden Python-Start zum Datendieb
Python führt beim Start jedes Interpreters Zeilen aus .pth-Dateien in site-packages aus, sofern sie mit import beginnen, und lädt danach ein Modul namens sitecustomize, falls es eines findet. Die kompromittierten Wheels von LiteLLM 1.107.0 brachten genau diese beiden Dateien mit, und dafür reichte eine zusätzliche Zeile in der Build-Konfiguration. Der Anwendungscode blieb unverändert, das Git-Tag sauber, und die Pipeline signierte das Ergebnis wie jedes andere Release. Trotzdem lief der Datendieb in jedem Python-Prozess der Umgebung, auch in pip selbst, ohne dass irgendwer import litellm schrieb. Wer verstehen will, warum saubere Quellen und Signaturen hier nicht geschützt haben, muss sich die Strecke zwischen Repository und site-packages ansehen.
TL;DR — 90 Sekunden
- Was: Am 21. Oktober 2026 standen auf PyPI sieben manipulierte Wheels von
litellm==1.107.0, dazu Images mit denselben Dateien auf Docker Hub und GHCR. Sie legtencook.pth,cook.pyundsitecustomize.pydirekt in site-packages ab. Diese Dateien starten bei jedem Python-Start einen Credential-Stealer und einen Dienst, der stündlich Code nachladen soll. - Schwere: Eine CVE gibt es nicht, und laut Phoenix Security ist auch keine zu erwarten. Wer die Version ausgeführt hat, muss davon ausgehen, dass alle erreichbaren Zugangsdaten abgeflossen sind. Laut Wiz haben die Angreifer erbeutete AWS-Instanzrollen bereits genutzt.
- Wen es trifft: Alle, die litellm zwischen 11:39 und 13:23 UTC von PyPI installiert oder eines der Tags
v1.107.0,latest,main-latestundmain-stablezwischen 11:46 und 14:20 UTC gezogen haben, meist über offene Versionsbereiche oder schwebende Tags. - Was zu tun ist: Aus der Shell zuerst den Nachlade-Prozess beenden und die drei Dateien löschen, ohne vorher
pipzu starten. Danach die Umgebung neu bauen, mitlitellm==1.106.2als letzter von allen Analysen bestätigter sauberer Version oder dem Imagev1.106.2per Digest, und alle erreichbaren Secrets rotieren. - Was vorher geschützt hätte: Lockfiles mit Hashes, eine Karenzzeit von einigen Tagen für neue Versionen, Images per Digest, eine Egress-Sperre und ein IMDS-Hop-Limit von 1 auf den Nodes. Signaturen und
--only-binaryhalfen in diesem Fall nicht.
Startup-Hooks statt Importpfad
LiteLLM ist ein Gateway, das Anfragen über eine einheitliche Schnittstelle an viele LLM-Anbieter weiterreicht und deren API-Keys zentral verwaltet. Gebaut werden die Wheels mit maturin, das zeigt der Abschnitt [tool.maturin] in der pyproject.toml, an dem der Angreifer ansetzte. Ein Wheel ist ein ZIP-Archiv mit festgelegter Struktur. Neben dem Paketcode darf es ein Verzeichnis <name>.data enthalten, dessen Unterordner wie purelib oder scripts auf Installationspfade abgebildet werden. Laut Wheel-Spezifikation verschiebt der Installer jeden dieser Teilbäume an sein Ziel. Was unter purelib liegt, landet also auf oberster Ebene von site-packages, gleichberechtigt neben allen anderen Paketen. Die maturin-Doku beschreibt die Option data entsprechend als Pfad zu einem solchen Datenverzeichnis.
Warum das gefährlich wird, zeigt das Modul site, das Python bei jedem Start lädt. Es durchsucht die site-packages-Verzeichnisse nach Dateien mit der Endung .pth, alphabetisch nach Dateinamen sortiert. Meist enthalten sie nur zusätzliche Suchpfade. Zeilen, die mit import und einem Leerzeichen oder Tabulator beginnen, führt Python laut Dokumentation jedoch aus. Danach versucht site, ein Modul sitecustomize zu importieren. Beides sind dokumentierte Erweiterungspunkte. Sie laufen, bevor Anwendungscode an die Reihe kommt, und unabhängig davon, was das Programm später importiert. Abschalten lässt sich das nur mit -S, und damit entfällt der gesamte Import von site samt der Ergänzungen am Suchpfad.
Genau hier setzte der Angriff an. cook.pth war laut StepSecurity zwölf Byte groß und enthielt nur import cook. cook.py entpackte einen eingebetteten Blob, den Stealer „privcollect“, und ließ ihn auf einem Daemon-Thread laufen. sitecustomize.py startete einen zweiten Teil, der sich per fork und setsid vom aufrufenden Prozess löste und die Sperrdatei /tmp/remote_code_loader.lock hielt. Dieser Dienst las stündlich einen Hostnamen aus einer Datei domain.txt in einem GitHub-Repository, lud von dort Python-Code unter dem Pfad /codepy und führte ihn ohne jede Prüfung aus. Laut StepSecurity war die Datei beim Release leer, die zweite Stufe also noch nicht scharf. Konto und Repository sind inzwischen gelöscht. Infizierte Hosts fragen aber weiter dieselbe Adresse ab, und StepSecurity weist darauf hin, dass jeder, der den Nutzernamen neu registriert, ihnen Code unterschieben könnte.
Der Stealer selbst arbeitete laut StepSecurity mit 16 Modulen. Er sammelte Browser-Passwörter, SSH-, GPG- und TLS-Schlüssel, Cloud- und Entwickler-Credentials, Instanz-Metadaten, CI-Secrets, Shell-Historien und Konfigurationen von KI-Coding-Werkzeugen. Unter Linux las er zusätzlich die Umgebungsvariablen aller Prozesse über /proc/<pid>/environ. Für LiteLLM-Proxys gab es ein eigenes Modul: War DATABASE_URL gesetzt, fragte es die Tabellen LiteLLM_CredentialsTable und LiteLLM_ProxyModelTable ab, in denen der Proxy die API-Keys der Upstream-Anbieter speichert. Das Ergebnis verschlüsselte er mit AES-256-CBC, umhüllte den Schlüssel mit RSA-4096 und schickte alles per unverschlüsseltem HTTP an www.litellm.dev/healthcheck, eine Domain, die dem Original zum Verwechseln ähnlich sieht. Eine Markierungsdatei unter /tmp schrieb er erst nach einer Antwort mit Status 200. Scheiterte der Versand, lief er bei jedem weiteren Python-Start erneut.
Zwei Details zeigen, wie schwer sich der Weg über die Hooks umgehen lässt. Manche System-Pythons bringen laut StepSecurity ein eigenes sitecustomize.py mit, das früher im Suchpfad steht und das des Angreifers verdeckt. Der Weg über cook.pth läuft trotzdem. Unter Windows fehlen fork und fcntl, der Nachlade-Dienst scheitert dort still, der Stealer läuft dennoch.
Was die manipulierten Commits ändern
Einen Fix-Commit gibt es bei einer Kompromittierung nicht, aufschlussreich sind hier die Commits des Angreifers. Laut Socket fügte 783c43b in der pyproject.toml unter [tool.maturin] die Zeile data = "cook" hinzu. Ein zweiter Commit, 20535d8, ergänzte die Dockerfiles um COPY cook ./cook. Laut StepSecurity landeten dieselben Dateien so unter /app/.venv/lib/python3.13/site-packages/ in den Images. Das Verzeichnis cook/ lag im Build-Baum, fehlte aber in der Quelldistribution. Das sdist auf PyPI enthielt deshalb laut Socket und StepSecurity die manipulierte Zeile, aber keine Schadsoftware. Im Release-Repository BerriAI/project-releaser stellte laut LiteLLM der Commit 9cf97e9 mit der Nachricht „ci: release from a fork source repository“ die Quelle des Builds um.
Am meisten verrät die Quelle selbst. Gebaut wurde laut LiteLLM aus Berri-AI/litellm, einem Fork mit Bindestrich im Namen, und nicht aus BerriAI/litellm. Weil GitHub die Commits eines Forks auch über das Repository ausliefert, von dem er abstammt, erscheint der Commit des Angreifers unter der URL des offiziellen Repositorys. Das offizielle Tag v1.107.0 enthält die Dateien laut LiteLLM und StepSecurity nicht. Die Pipeline lief danach regulär, signierte und veröffentlichte, und die angehängte Provenance des Container-Images verzeichnet laut Socket wahrheitsgemäß die Quelle Berri-AI/litellm. Der Fehler lag also weder im Code noch in der Signatur, sondern in der Frage, welche Quelle die Pipeline überhaupt bauen darf. Wer nur das Repository prüft, sieht nichts. Was auf dem Server läuft, ist das Wheel.
Vom geleakten Schlüssel zum signierten Release
Die Kette begann laut der Ursachenanalyse von LiteLLM Monate vor dem Release. Am 27. Mai 2026 landete eine Konfigurationsdatei im öffentlichen Repository LiteLLM-Labs/lite-harness, die den Master-Key eines internen Alpha-Dienstes für Agenten enthielt. Der Commit wurde per Force-Push überschrieben, der Schlüssel aber nie widerrufen. Am 10. Oktober fragte der Angreifer damit die API dieses Dienstes ab und bekam die Konfiguration aller verwalteten Agenten zurück. Darin stand ein GitHub-CLI-OAuth-Token eines Organisations-Owners, das an kein Dienstkonto gebunden und nicht eingeschränkt war. Laut LiteLLM unterliegen solche Tokens außerdem nicht der erneuten 2FA-Abfrage, die GitHub für besonders heikle Aktionen verlangt. Die erste Mitteilung hatte das Token noch in den Logs eines internen LiteLLM-Deployments vermutet. Die spätere Analyse korrigiert das ausdrücklich.
Mit dem Owner-Token startete der Angreifer Wegwerf-Workflows, die Repository-Secrets an einen Webhook-Dienst schickten. Darunter waren GH_TOKEN und RELEASE_PLEASE_TOKEN, hinter denen Tokens von Maintainern steckten. Die Läufe löschte er nach wenigen Minuten. Am 21. Oktober scheiterte ein Push in das Release-Repository zunächst am Branch-Schutz, worauf das Owner-Token den Schutz aushebelte und die Änderung per Force-Push durchsetzte. Das Freigabe-Gate der Produktion erfüllte das zweite gestohlene Token. Diese Regel war laut LiteLLM in derselben Woche gelockert worden, sodass zwei beliebige Engineers ein Release auslösen und freigeben konnten. Vorher brauchte es ein Konto, das an ein physisches Gerät im Büro gebunden war. Mit dem dritten Token löschte der Angreifer die Protokolle der Läufe.
Betroffen ist, wer im Zeitfenster eine Version aufgelöst hat, die nicht festgenagelt war. Das gilt für pip install litellm ohne Version, für Bereiche wie litellm>=, für Lockfile-Aktualisierungen in diesen Stunden und für Deployments, die latest oder main-stable ziehen. In Kubernetes ist das tückisch, weil ein Container mit dem Tag latest ohne gesetzte imagePullPolicy laut Doku die Policy Always bekommt und bei jedem Start neu zieht. Wiz zählt laut ClickPy rund 49.000 Downloads. Was ich daraus ableite: Auch wer litellm nur als transitive Abhängigkeit eines Agenten-Frameworks nutzt, war betroffen, sobald der Resolver im Fenster 1.107.0 wählte. Und für die Ausführung reicht ein beliebiger Python-Prozess in derselben Umgebung, schon ein Testlauf oder ein pip list.
Nicht betroffen ist dagegen, wer gegen ein Lockfile mit älteren Versionen und Hashes installiert, Images per Digest referenziert oder aus dem offiziellen Tag selbst baut. Laut LiteLLM blieben auch Enterprise-Kunden mit der privaten Registry verschont. Ein LiteLLM-Update in derselben Woche heißt für sich genommen noch nicht, dass du betroffen bist. Maßgeblich ist allein, ob 1.107.0 oder eines der betroffenen Images tatsächlich installiert war.
Der Schaden reicht weit über LiteLLM hinaus. Wiz beobachtete, dass die Angreifer AWS-Instanzrollen von kompromittierten CI-Runnern und Kubernetes-Nodes verwendeten, zuerst mit sts:GetCallerIdentity, dann mit bedrock:ListFoundationModels. Das passt zu LLMjacking, also dem Weiterverkauf oder Missbrauch fremder Modellzugänge. In einer kleinen Zahl von Umgebungen folgte laut Wiz IAM-Aufklärung, etwa mit ListRoles, ListUsers und ListAccessKeys. Auf Proxy-Hosts kommen die Provider-Keys aus der Datenbank hinzu, die das eigene Modul des Stealers ausgelesen hat.
| Vorfall | Betroffen | Fix | Voraussetzung | CVSS |
|---|---|---|---|---|
| LiteLLM-Kompromittierung vom 21.10.2026 (keine CVE) | litellm==1.107.0, alle sieben Wheels, auf PyPI von 11:39:51 bis 13:23 UTC | Pin auf 1.106.2, Umgebung neu bauen | Installation im Zeitfenster über offene Versionsangabe oder Lockfile-Update | nicht bewertet |
| Container-Images derselben Version | Tags v1.107.0, latest, main-latest, main-stable auf Docker Hub (bis 14:20 UTC) und GHCR (litellm, litellm-database, litellm-non_root, spätestens 13:51 UTC nicht mehr abrufbar) | Image v1.106.2 per Digest | Pull im Zeitfenster über schwebende Tags | nicht bewertet |
Ohne CVE gibt es weder einen KEV-Eintrag noch einen EPSS-Wert. Einen Exploit braucht es hier nicht, die Schadsoftware ist der Angriff. Die zweite Stufe war laut StepSecurity und Wiz nie aktiv, die Nutzung erbeuteter Zugangsdaten ist laut Wiz dagegen belegt. Socket meldete die Wheels sieben Minuten nach der Veröffentlichung, Chainguard nach 16 Minuten. Von PyPI entfernt waren sie erst nach knapp zwei Stunden.
Saubere Versionen und der Weg zurück
Einen Patch im klassischen Sinn gibt es nicht. LiteLLM, StepSecurity, Wiz und Chainguard nennen übereinstimmend 1.106.2 als letzte saubere Version. StepSecurity führt zusätzlich die Release-Kandidaten 1.107.0rc1 und rc2 als sauber. Socket merkt allerdings an, dass damit nicht jedes ältere Artefakt geprüft ist. Seit dem 24. und 25. Oktober stehen mit 1.106.3 und 1.106.4 zwei neuere Versionen auf PyPI. Keine der Analysen hat sie untersucht, und die Release Notes zu 1.106.4 nennen keine Sicherheitskorrektur. Du pinnst also litellm==1.106.2 oder das Image v1.106.2 per Digest und baust virtuelle Umgebungen und Images neu, statt sie an Ort und Stelle zu reinigen. Dazu gehören laut StepSecurity auch Caches, interne Spiegel und Pull-Through-Registries, die die Wheels oder Images zwischengespeichert haben.
Die größte Stolperfalle ist die Bereinigung selbst. Jeder Python-Start führt die Hooks erneut aus, und das gilt für pip uninstall genauso wie für ein schnelles pip list. StepSecurity empfiehlt deshalb, zuerst aus der Shell den Prozess zu beenden, der /tmp/remote_code_loader.lock hält, und danach cook.pth, cook.py, sitecustomize.py sowie passende .pyc-Dateien unter __pycache__ aus jedem betroffenen site-packages zu löschen. LiteLLM nennt in seiner Anleitung dieselben Schritte in umgekehrter Reihenfolge. Entscheidend ist bei beiden, dass alles aus der Shell geschieht und vorher kein Python-Prozess startet. Erst danach installierst du neu. Bringt das System-Python ein eigenes sitecustomize.py mit, darfst du daraus nicht schließen, dass alles harmlos war, denn der Weg über die .pth-Datei lief trotzdem.
Auch eine fehlende Markierungsdatei beweist laut StepSecurity nichts. Hatte der Host ausgehenden Internetzugang, solltest du laut LiteLLM davon ausgehen, dass der Versand gelungen ist. Zu rotieren sind dann laut StepSecurity Instanz- und Task-Rollen, Cloud-Keys, SSH- und GPG-Schlüssel, Registry-Tokens für GitHub, npm und PyPI, Kubernetes- und Vault-Tokens, CI-Secrets und die Tokens von KI-Werkzeugen. Auf Proxy-Hosts kommen Master-Key, Salt-Key und virtuelle Keys hinzu, außerdem die Provider-Keys aus der Datenbank und das Passwort aus DATABASE_URL, die der Stealer beide gelesen hat.
Ein Punkt verdient Aufmerksamkeit für die nächsten Tage. Laut LiteLLM liegt in der privaten Enterprise-Registry bereits ein Release für eine Lücke, deren Offenlegung für den 27. Oktober geplant ist. Eine Veröffentlichung dazu habe ich bis zum Stand dieses Beitrags nicht gefunden. Wer heute auf 1.106.2 festnagelt, muss also bald auf eine neue Version wechseln, und die kommt aus einer Pipeline, die eben erst kompromittiert war. Ich würde sie deshalb nicht blind übernehmen, sondern vor dem Rollout Herkunft und Inhalt des Wheels prüfen, wie im nächsten Abschnitt beschrieben.
Härtung, die den Startup-Hook ins Leere laufen lässt
Bevor es um einzelne Maßnahmen geht, hilft eine Einordnung. Die Schadsoftware bricht nicht aus dem Container aus und ruft keinen ungewöhnlichen Syscall auf. Sie tut, was jeder Python-Prozess darf: Dateien und Umgebungsvariablen lesen, auf die er Zugriff hat, und eine ausgehende Verbindung öffnen. Wirksam sind deshalb vier Klassen von Maßnahmen. Die erste lässt das Artefakt gar nicht erst in die Umgebung. Die zweite hält klein, was der Prozess sehen kann. Die dritte sperrt den Rückweg nach draußen. Die vierte macht Startup-Hooks sichtbar, bevor sie Schaden anrichten.
Lockfile mit Hashes statt offener Versionsbereiche
Der Angriff erreichte nur Umgebungen, die im Zeitfenster eine neue Version aufgelöst haben. pip bietet dafür seit Version 8.0 den Hash-Prüfmodus. Sobald eine Anforderung ein --hash trägt oder --require-hashes gesetzt ist, verlangt pip für jedes Paket und jede Abhängigkeit eine exakte Version mit == und einen passenden Hash. Eine neue Version kann dann nicht still nachrücken, und ein ausgetauschtes Artefakt mit gleicher Versionsnummer fällt durch. uv erreicht dasselbe über seine Lock-Datei.
Die Grenze liegt auf der Hand. Wer sein Lockfile genau in diesen zwei Stunden aktualisiert hat, hat den Hash der manipulierten Version eingetragen und sie damit sogar festgeschrieben. Das Lockfile schützt also vor dem unbemerkten Nachrücken, nicht vor der bewussten Aktualisierung. Deshalb gehört es mit der nächsten Maßnahme zusammen. Der Betriebsaufwand ist gering, solange Updates ohnehin über Merge Requests laufen.
Karenzzeit für neue Versionen
Die manipulierten Wheels lagen knapp zwei Stunden auf PyPI, gemeldet wurden sie nach sieben bis 16 Minuten. Eine Regel, die neue Versionen erst nach einigen Tagen annimmt, hätte 1.107.0 deshalb nie ausgewählt. uv kennt dafür exclude-newer, das Kandidaten auf Dateien beschränkt, die vor einem Zeitpunkt hochgeladen wurden, und laut Doku auch relative Angaben wie 1 week oder P7D akzeptiert. Mit exclude-newer-package legst du für einzelne Pakete eine eigene Grenze fest oder nimmst sie aus. pip bringt seit 26.1 die Option --uploaded-prior-to mit, die eine Dauer in Tagen wie P4D erwartet. Renovate setzt mit minimumReleaseAge dasselbe Prinzip auf der Ebene der Merge Requests um, und Sicherheitsupdates umgehen die Wartezeit dort laut Doku.
Zwei Feinheiten aus der Renovate-Doku solltest du kennen. Renovate liest das Alter nur aus Zeitstempeln der Registry. Bei PyPI ist das der früheste Upload einer Version, bei Containern unterstützt Renovate nur Docker Hub, GHCR und andere Registries nicht. Mit dem Standardverhalten timestamp-required, das seit Renovate 42 gilt, bleiben Digest-Updates dort deshalb dauerhaft hängen. Außerdem empfiehlt die Doku, die Karenzzeit zusätzlich im Paketmanager zu setzen, weil transitive Abhängigkeiten sonst beim Aktualisieren des Lockfiles ohne Wartezeit hereinkommen.
Images per Digest statt schwebender Tags
Die Tags latest, main-latest und main-stable zeigten auf Docker Hub rund zweieinhalb Stunden auf das infizierte Image. Wer im Deployment image: …@sha256:… referenziert, bekommt dagegen immer genau das Artefakt, das getestet wurde. Ein neues Image kommt dann nur über einen Merge Request mit neuem Digest ins Cluster, und genau dort greifen Karenzzeit und Pipeline-Tests. Den Aufwand, Digests nachzuziehen, nimmt dir Renovate ab. Zusammen mit einer Karenzzeit funktioniert das laut Doku aber nur für Docker Hub, für GHCR brauchst du eine eigene Regel, sonst bleiben die Updates stehen.
Egress-Sperre für Python-Workloads
Der Stealer schickte seine Beute per HTTP an eine Lookalike-Domain, der Nachlade-Dienst fragte GitHub nach einem Hostnamen. Ein LiteLLM-Proxy braucht ausgehend vor allem die Endpunkte seiner LLM-Anbieter, seine Datenbank und je nach Konfiguration einen Cache oder Logging-Dienste. Eine NetworkPolicy mit policyTypes: [Egress], die genau diese Ziele freigibt, hätte den Versand an litellm.dev verhindert. Weil der Stealer seine Markierung nur nach erfolgreichem Versand schreibt, hätte er es bei jedem Start erneut versucht. Protokolliert dein CNI abgelehnte Verbindungen, wären diese Versuche dort sichtbar geworden.
Die Grenzen sind dieselben wie bei jeder Egress-Regel. Die Standard-NetworkPolicy arbeitet mit IP-Blöcken und Label-Selektoren, für Anbieter-APIs mit wechselnden Adressen brauchst du FQDN-Regeln deines CNI, etwa von Cilium. Sie wirkt nur, wenn dein CNI NetworkPolicies durchsetzt. Und sie schützt nicht, wenn ein Angreifer über ein freigegebenes Ziel exfiltriert. CI-Runner sind schwerer einzugrenzen, weil sie Paketquellen, Registries und Git-Hosts erreichen müssen. Dort lohnt es sich, wenigstens den direkten Weg ins Internet über einen Proxy mit Allowlist zu führen.
Was der Prozess erreichen kann
Der Wert eines Stealers bemisst sich an dem, was er findet. Laut Wiz stammten die missbrauchten AWS-Rollen von CI-Runnern und Kubernetes-Nodes. Die EKS-Best-Practices empfehlen für Pods, die die Node-Rolle nicht brauchen, IMDSv2 zu erzwingen und das Hop-Limit auf 1 zu setzen, etwa mit aws ec2 modify-instance-metadata-options --instance-id <value> --http-tokens required --http-put-response-hop-limit 1 oder in Terraform im Block metadata_options des Launch Templates über http_put_response_hop_limit = 1. Damit erreicht ein gewöhnlicher Pod die Metadaten des Nodes nicht mehr. Die Doku warnt, dass Pods ohne IRSA oder Pod Identity die Node-Rolle dann verlieren, und rät davon ab, IMDS ganz abzuschalten.
Im Pod selbst hilft automountServiceAccountToken: false für Workloads, die die Kubernetes-API nicht brauchen, denn was nicht eingehängt ist, kann nicht gestohlen werden. Secrets aus Umgebungsvariablen in Dateien zu verlegen, ist dagegen kein verlässlicher Schutz, weil der Stealer neben den Umgebungsvariablen gezielt Dateien mit Zugangsdaten einsammelt. Was sich nicht vermeiden lässt: Der Proxy braucht seine Provider-Keys und seine Datenbank. Diese Schlüssel sind bei einer Kompromittierung verloren. Ausgabenlimits und getrennte Keys je Umgebung beim Anbieter halten den Schaden dann klein und machen ihn schnell sichtbar.
Herkunftsnachweise prüfen, aber die richtige Frage stellen
Signierte Images und Provenance sind sinnvoll, nur beantworten sie in diesem Fall die falsche Frage. Die Signatur belegt, dass die echte Pipeline gebaut hat, und das stimmte. Die Provenance des Images nannte als Quelle aber Berri-AI/litellm. Eine Prüfung, die neben der Signatur auch das Quell-Repository im Nachweis mit dem erwarteten Wert vergleicht, hätte das Image abgelehnt. Was ich daraus ableite: Eine Admission-Policy, etwa mit Kyverno, sollte bei Images aus fremden Projekten zusätzlich zur Signatur das Quell-Repository aus der Attestation prüfen. Für die Wheels auf PyPI hätte das nicht geholfen. Keine Quelle berichtet, dass sie eigene Herkunftsnachweise trugen, und laut dem Melder des GitHub-Issues #47826 hat LiteLLM nie welche veröffentlicht.
Am konsequentesten ist es, fremde Wheels gar nicht zu verwenden und aus dem verifizierten Tag selbst zu bauen. Genau so blieb Chainguard nach eigener Darstellung verschont, weil der Build aus der Quelle die Dateien nicht enthielt. Das bedeutet allerdings eigenen Build-Aufwand, und jede neue Version musst du selbst nachziehen.
Startup-Hooks inventarisieren
Ein Prüfschritt in der Pipeline, der im fertigen Image alle .pth-Dateien mit Zeilen ab import sowie jedes sitecustomize.py außerhalb der Distribution auflistet und mit einer Allowlist vergleicht, hätte diese drei Dateien vor dem Rollout gefunden. Das ist meine eigene Ableitung, kein Herstellervorschlag. Langfristig hilft Python selbst: Seit 3.15 gelten solche Zeilen als veraltet, und laut Doku ignoriert Python sie in 3.18 und 3.19 stillschweigend. Als Ersatz gibt es seit 3.15 .start-Dateien mit ausdrücklichem Einstiegspunkt. Bis dahin laufen die alten Zeilen aber weiter.
Was hier nicht hilft
Die naheliegende Empfehlung, mit --only-binary :all: keine Quelldistributionen zu bauen, war in diesem Fall wirkungslos, denn gerade die Wheels waren manipuliert. Auch die Hashes in der RECORD-Datei eines Wheels schützen nicht, weil die Pipeline sie beim Bau der manipulierten Wheels selbst erzeugt hat. python -I nimmt laut Doku nur das Skriptverzeichnis und das Benutzer-site-packages aus dem Suchpfad, die .pth-Dateien im regulären site-packages laufen weiter.
Container-Härtung bleibt richtig, greift hier aber kaum. seccomp mit RuntimeDefault, entzogene Capabilities, runAsNonRoot, allowPrivilegeEscalation: false und der Pod Security Standard „restricted“ verhindern weder das Lesen von Dateien, die der Prozess ohnehin lesen darf, noch eine HTTP-Verbindung. readOnlyRootFilesystem könnte die Sperr- und Markierungsdateien unter /tmp behindern. Ob die Schadsoftware dann scheitert, ist aber nicht dokumentiert, und wer für /tmp ein beschreibbares Volume einhängt, verliert diesen Effekt ohnehin. Ein CVE-Gate in der Pipeline schlägt nie an, weil es keine CVE gibt. Und ein Code-Review des Repositorys findet nichts, weil das Tag sauber ist.
Mehr zu Egress-Regeln, Admission-Policies und Image-Herkunft findest du in der Serie TYPO3 auf Kubernetes unter IT-Grundschutz.
Erkennung und Prüfung
Ob du betroffen bist, zeigen zuerst deine Abhängigkeitsdateien. Durchsuche Lockfiles, requirements-Dateien und pyproject.toml nach litellm mit Version 1.107.0, aber auch nach offenen Bereichen, und prüfe transitive Abhängigkeiten deiner Agenten-Frameworks. Für Container vergleichst du die tatsächlich laufenden Image-IDs der Pods mit den veröffentlichten Digests, etwa sha256:1bbcb1443bc9… für das Haupt-Image. Ein Tag allein sagt nichts, weil es inzwischen wieder auf ein sauberes Image zeigt. Auf Hosts und in Containern suchst du in jedem site-packages nach cook.pth, cook.py und einem sitecustomize.py, das nicht zur Distribution gehört.
Ob die Schadsoftware lief, verraten laut StepSecurity und Wiz mehrere Spuren. Dazu gehören die Dateien /tmp/.pc_once_72129118c59f5636c0bb und /tmp/remote_code_loader.lock, temporäre Verzeichnisse mit den Präfixen .pc_sq_ und .pc_e_ sowie die Umgebungsvariable PRIVCOLLECT_ACTIVE=1. In DNS-, Proxy- und Egress-Logs suchst du nach litellm.dev und www.litellm.dev, nach Anfragen an raw.githubusercontent.com/letters9813/ und nach jedem Pfad, der auf /codepy endet. Ein Treffer auf /codepy bedeutet laut StepSecurity, dass auf diesem Host fremder Code gelaufen ist. Die Hashes der drei Dateien listen StepSecurity und Phoenix Security.
In AWS CloudTrail lohnt der Blick auf GetCallerIdentity-Aufrufe von Instanz- oder Task-Rollen rund um den Installationszeitpunkt und auf jede Nutzung dieser Rollen von außerhalb deiner Infrastruktur. Wiz nennt außerdem bedrock:ListFoundationModels und IAM-Aufrufe wie ListRoles als beobachtete Folgeaktivität.
Bei einem Verdacht isolierst du den Host, sicherst Logs und gehst in der beschriebenen Reihenfolge vor: Prozess mit der Sperrdatei beenden, Dateien löschen, Umgebung neu bauen, Zugangsdaten aus einer sauberen Umgebung rotieren. Blockiere zusätzlich litellm.dev auf DNS- und Egress-Ebene, damit Hosts, die du noch nicht gefunden hast, keine Daten mehr loswerden.
Betreiberempfehlung
Akut handeln solltest du, wenn in deinen Umgebungen litellm ohne festen Pin installiert wird, wenn ein Deployment ein schwebendes Tag des LiteLLM-Images nutzt oder wenn ein Lockfile am 21. Oktober aktualisiert wurde. Prüfe dann sofort die tatsächlich installierten Versionen und Image-Digests, bereinige in der richtigen Reihenfolge und rotiere alles, was die Prozesse erreichen konnten, auf Proxy-Hosts vorneweg die Provider-Keys.
Beobachten genügt, wenn du litellm über ein Lockfile mit Hashes auf einer älteren Version hältst und Images per Digest referenzierst. Sieh dir trotzdem an, wie die nächste Version in deine Umgebung käme. Seit dem Vorfall sind 1.106.3 und 1.106.4 erschienen, die keine der Analysen geprüft hat, und das angekündigte Release zur Lücke mit Offenlegung am 27. Oktober folgt aus derselben Pipeline.
Unabhängig vom Einzelfall gilt, was ich bei jeder Lücke sage: Patches und Updates gehören automatisiert. Wenn Renovate oder Dependabot neue Versionen von Python-Paketen, Images und Helm-Charts selbstständig als Merge Request vorschlagen und die Pipeline sie gegen eine Staging-Umgebung testet, ist ein Sicherheitsupdate eine Sache von Stunden statt Wochen. Dieser Fall zeigt die andere Seite desselben Prozesses. Eine Karenzzeit von einigen Tagen für Routine-Updates hätte 1.107.0 nie in einen Merge Request gelassen, während Sicherheitsupdates davon ausgenommen bleiben und sofort kommen. Bricht ein Update etwas, fällt das im Test auf und nicht in Produktion. Wer dagegen von Hand aktualisiert, greift im Zweifel genau dann zu pip install -U, wenn gerade die falsche Version oben liegt, und hat in der Urlaubszeit wochenlang keinen Patch, gefolgt von einem großen Sammel-Update, bei dem niemand mehr sagen kann, was sich geändert hat.
Häufige Fragen zur LiteLLM-Kompromittierung
Ich importiere litellm gar nicht direkt. Bin ich betroffen?+
Möglicherweise. Die Hooks laufen bei jedem Python-Start in der Umgebung, egal was du importierst. Entscheidend ist, ob 1.107.0 installiert war, auch als transitive Abhängigkeit.
Reicht pip uninstall litellm?+
Nein. pip ist selbst ein Python-Prozess und startet den Stealer erneut. Beende zuerst den Prozess mit der Sperrdatei, lösche die drei Dateien und baue die Umgebung dann neu.
Das Release war signiert. Warum hat das nicht geschützt?+
Die Pipeline hat signiert, was sie gebaut hat, und gebaut hat sie aus einem Fork. Die Signatur belegt den Build-Weg, nicht die Quelle. Prüfe deshalb auch das Quell-Repository im Herkunftsnachweis.
Hilft eine Karenzzeit, wenn ich Sicherheitsupdates sofort brauche?+
Ja, denn Sicherheitsupdates nimmst du davon aus. Für Routine-Updates hätte hier schon ein Tag gereicht: PyPI entfernte die Wheels nach knapp zwei Stunden, und die Scanner meldeten sie nach sieben bis 16 Minuten.
Fazit
Für LiteLLM war das schon der zweite Vorfall dieser Art in diesem Jahr. Nach dem ersten im März hatte das Projekt laut eigener Aussage die Releases in ein privates Repository verlegt, Docker-Tags unveränderlich gemacht und Images signiert. Gereicht haben trotzdem ein nie widerrufener Schlüssel, ein uneingeschränktes Owner-Token und eine gelockerte Freigaberegel. Die Kontrollen waren nur so stark wie die Zugangsdaten, mit denen man sie abschalten konnte. Für dich als Betreiber steckt die übertragbare Lehre an anderer Stelle. Python bietet jedem installierten Paket mit .pth und sitecustomize eine Tür, die vor jedem Import aufgeht, und was durch diese Tür kommt, entscheidet allein das Wheel, nicht das Repository. Vertrauen gehört deshalb an die Stelle, an der ein Artefakt in deine Umgebung kommt: festgenagelt, mit Wartezeit, mit geprüfter Herkunft und mit einer Netzwerkgrenze, die einen Fehlgriff nicht sofort teuer macht.
Quellen
- LiteLLM: Security Incident Oct 21, 2026 (Zeitleiste, Artefakte, Bereinigung)
- LiteLLM: Ursachenanalyse und Angriffskette
- StepSecurity: litellm 1.107.0 Compromised on PyPI
- Socket: LiteLLM PyPI Package and Docker Images Compromised
- Chainguard: LiteLLM attacked again
- Wiz: LiteLLM Compromised, Steals Credentials
- Phoenix Security: LiteLLM 1.107.0 Supply Chain Compromise (No CVE)
- GitHub Issue BerriAI/litellm #47826
- PyPI: Release-Historie von litellm (1.106.3 und 1.106.4)
- Python-Doku: site – Site-specific configuration hook
- Python-Doku: Kommandozeilenoptionen (-I, -S, -s)
- Python Packaging: Binary distribution format (Wheel)
- maturin: Konfiguration (data)
- pip: Secure installs (Hash-Prüfmodus)
- uv: Settings (exclude-newer)
- Simon Willison: pip 26.1 und --uploaded-prior-to
- Renovate: Minimum Release Age
- AWS EKS Best Practices: Identity and Access Management
- Kubernetes-Doku: Network Policies
- Kubernetes-Doku: Image Pull Policy
Ich prüfe deine Python- und Container-Pipelines auf offene Versionsbereiche, schwebende Tags und zu breite Zugangsdaten und richte Karenzzeiten, Digest-Pinning und Egress-Regeln ein.
Lieferketten-Audits für PyPI, Composer und npm, Lockfiles mit Hashes, Renovate mit Karenzzeit, Egress-Policies für Kubernetes und IMDS-Härtung auf AWS.
Plattform-Betrieb statt Beratung auf Papier: Ich prüfe, patche und härte deine Infrastruktur laufend.
