21 października około 11:39 UTC na PyPI weszła wersja 1.107.0 paczki litellm, jednej z najczęściej pobieranych bibliotek Pythona do pracy z modelami językowymi i fundamentu własnych proxy LLM. Wszystkie siedem wheeli, na Linuksa, macOS i Windows, niosło złodzieja poświadczeń i backdoor do zdalnego uruchamiania kodu, wgrane oficjalnym kanałem wydawniczym projektu, więc pip nie miał powodu podejrzewać czegoś złego. Ujawniło to niezależnie SafeDep oraz zespół bezpieczeństwa Upwind w zgłoszeniu 47826 na GitHubie BerriAI, a PyPI usunęło wersję około 13:40 UTC, po nieco ponad dwóch godzinach ekspozycji.
Sprawa dotyczy każdego, kto w tym oknie zrobił pip install litellm bez przypiętej wersji, oraz każdego, kto ciągnął obrazy Dockera z pływającymi tagami latest, main-latest, main-stable, bo te też wskazywały na zainfekowany build. Sam prowadziłem u nas poradnik o własnym proxy LLM na LiteLLM i instrukcje z niego przeglądam w tym świetle jeszcze raz. Jeśli 1.107.0 weszła u Ciebie, nie zaczynaj od pipa, tylko od usunięcia zainfekowanych plików i rotacji kluczy, bo złodziej zbierał między innymi klucze API wszystkich dostawców modeli z bazy proxy.
- Najważniejsze informacje
- Co się stało z paczką litellm
- Jak działał atak
- Kto jest zagrożony, a kto nie
- Co zrobić: aktualizacja i rotacja kluczy
- Szerszy obraz: PyPI i ataki supply chain
- Najczęstsze pytania
Najważniejsze informacje
| Co | Szczegóły |
|---|---|
| Zainfekowana wersja | litellm 1.107.0: wszystkie 7 wheeli plus obrazy Docker v1.107.0 oraz tagi latest, main-latest, main-stable |
| Typ ataku | Supply chain: wstrzyknięcie do procesu wydawniczego przez skradzione tokeny OAuth GitHub CLI trzech inżynierów |
| Co kradnie | Klucze API dostawców z bazy proxy, tokeny narzędzi AI, poświadczenia chmurowe, klucze SSH, hasła przeglądarek, zmienne środowiskowe procesów |
| Okno ekspozycji | pip: 11:39 do ok. 13:40 UTC 21 października 2026; Docker: od 11:48 UTC do wycofania tagów wieczorem |
| Wersja bezpieczna | 1.106.2 i wcześniejsze; czyste są też 1.107.0rc1 i rc2; wersji naprawczej 1.107.1 nie ogłoszono w momencie pisania |
| Eksfiltracja | Szyfrowane archiwum tar.gz wysyłane POST-em na domenę www.litellm.dev/healthcheck, podszywającą się pod projekt |
| Skala | Paczkę litellm pobiera dziennie ok. 3,65 mln razy; liczba pobrań samej 1.107.0 nie jest znana |
| Pierwsza pomoc | Usunąć cook.pth, potem cook.py oraz sitecustomize.py z site-packages, zabić daemona, instalować 1.106.2, rotować klucze |
Co się stało z paczką litellm
Oś czasu tego incydentu jest krótka i gęsta, wszystkie godziny poniżej to UTC z 21 października 2026. O 10:44 projekt normalnie wydał wersję 1.106.2. O 11:39:51 do 11:40:13 na PyPI trafiła wersja 1.107.0, zbudowana tym samym maturinem 1.9.4 co legalne wydania, z poprawnymi wpisami w manifeście RECORD. O 11:48 zainfekowany build poszedł do rejestrów Dockera, a o 11:50 bot wydawniczy opublikował release na GitHubie; w tym projekcie to normalna kolejność, więc niczego nie zdradzała. O 12:06 zespół Upwind otworzył zgłoszenie 47826 z pełną analizą, SafeDep opublikował własną, a około 13:40 wersja zniknęła z PyPI, równolegle z kontem GitHub używanym przez backdoor. Projekt BerriAI sklasyfikował incydent jako SEV-0, a wieczorem niezależna sonda potwierdziła, że wszystkie cztery zainfekowane tagi Docker zwracają 404.
Dla porządku: w tym samym zgłoszeniu BerriAI napisało klientom enterprise o prywatnym rejestrze z zaembargowaną poprawką na inną podatność, z datą ujawnienia 27 października; to odrębna sprawa, choć właśnie ona przyciągnęła na Hacker News najwięcej krytyki.
Jak działał atak
Instalacja 1.107.0 wkładała do site-packages trzy pliki: cook.pth o treści import cook, dalej cook.py (22252 bajtów) oraz sitecustomize.py (25509 bajtów). Python wykonuje pliki .pth z liniami import przy każdym starcie interpretera i to samo robi z sitecustomize.py, więc malware odpalał się w każdym procesie Pythona na maszynie, także w takim, który litellm ani razu nie zaimportował, pip włącznie. Na Windowsie backdoor nie umiał forknąć daemona i failował po cichu, ale złodziej i tak działał.
Pierwszy payload, nazywający sam siebie privcollect, miał szesnaście modułów zbierania danych i listę celów, która czyta się z niewygodą:
- hasła zapisane w Chromium odszyfrowane kluczem profilu oraz pliki
logins.jsoni bazękey4.dbFirefoksa, klucze SSH z~/.ssh, historie powłok, poświadczenia AWS, Azure, GCP, kube, Dockera, gita, rclone i.netrc, klucze GPG i TLS, sekrety CI/CD od GitLaba przez Jenkinsa po Vault i Terraform - tokeny narzędzi wytwarzających AI: Claude Code, Claude Desktop, Cursor, Windsurf, Copilot, Gemini CLI, Codex, Aider, opencode, Zed, token Hugging Face; to ta flota, o której ryzykach pisaliśmy przy okazji bezpieczeństwa agentów AI
- zmienne środowiskowe wszystkich czytelnych procesów przez
/proc/, poświadczenia ról z metadanych AWS zwalidowane od razu przez STS/environ GetCallerIdentity, a przy ustawionymDATABASE_URLzrzut tabelLiteLLM_CredentialsTableorazLiteLLM_ProxyModelTable, czyli klucze API wszystkich dostawców modeli trzymane przez proxy
Zebrane dane pakował w tar.gz, klucz AES-256-CBC losowany na sesję owijał RSA-OAEP 4096 bitami i wysyłał POST-em na http://www.litellm.dev/healthcheck z tokenem w nagłówku, więc przechwycenie ruchu nic nie daje. Domena litellm.dev została zarejestrowana 13 października, osiem dni przed atakiem, i nie ma nic wspólnego z oficjalnym litellm.ai. Po udanym uploadie złodziej zostawiał znacznik /tmp/.pc_once_72129118c59f5636c0bb, ale jego brak niczego nie dowodzi, bo kontenery i czyszczenie tmp usuwają takie pliki. Drugi plik, sitecustomize.py, forkował oderwanego daemona i co godzinę czytał adres serwera z pliku domain.txt na GitHubie użytkownika letters9813, wyrzucał złodzieja ponownie i pobierał z tego serwera kod do wykonania przez exec(). Tu jest jedna dobra wiadomość: wg analizy Upwind plik domain.txt był pusty od 20 października, a wg SafeDep konto zniknęło o 13:40, więc brak dowodów, że ktokolwiek dostał drugi etap, choć odbiornik eksfiltracji był wciąż żywy.
Najciekawsza jest droga wejścia: przeglądanie źródeł na GitHubie nie wystarczyło, żeby ją zobaczyć. W sdist zaszyta była jedna dodatkowa linia data = "cook" w sekcji maturin pliku pyproject.toml, każąca buildowi skopiować folder z payloadem do wheeli; samego folderu w sdist nie było, więc diff źródeł wyglądał czysto. Upwind odtworzył łańcuch dziewięciu commitów atakującego, od 08:56 do 10:46, na repozytorium podszywającym się pod projekt, zbudowanych przez oryginalny prywatny pipeline wydawniczy. BerriAI potwierdziło wieczorem, że akcje przeszły przez konta trzech inżynierów, najpewniej przez skradzione tokeny OAuth GitHub CLI: jednym kontem uruchomiono pipeline wydawniczy project-releaser, drugim zatwierdzono. Wszystkie zaangażowane tokeny odwołano i usunięto z organizacji wszystkich użytkowników poza jednym niezaatakowanym kontem, ale nie ustalono, jak tokeny wyciekły.
Kto jest zagrożony, a kto nie
W grupie ryzyka są dokładnie ci, którzy między 11:39 a ok. 13:40 UTC 21 października zainstalowali 1.107.0 przez pip, najczęściej bez pinu w CI albo przez świeże środowisko, oraz ci, którzy między 11:48 a wieczorem pobierali obrazy ghcr.io/berriai/litellm lub litellm/litellm z tagiem v1.107.0 albo pływającymi latest, main-latest, main-stable, w każdym z trzech wariantów obrazu: podstawowym, -database oraz -non_root. Instalacja na maszynie z dostępem do internetu oznacza, wg bezpośredniej rekomendacji Upwind, że trzeba przyjąć sukces eksfiltracji, niezależnie od obecności znacznika w /tmp.
Poza ryzykiem są użytkownicy 1.106.2 i wszystkich wcześniejszych wersji: Upwind przeskanował co do wheela wersje od 1.103.6 wzwyż, a czyste są też kandydaci 1.107.0rc1 i rc2. Czyste okazały się też pakiety towarzyszące litellm-proxy-extras oraz litellm-enterprise. Jeśli Twój Dockerfile trzyma konkretny tag typu v1.106.2, nic się nie stało; warto go dodatkowo przypiąć po digescie.
Co zrobić: aktualizacja i rotacja kluczy
Kolejność ma znaczenie: pierwsza myśl, pip install litellm==1.106.2, jest w zainfekowanym środowisku błędna, bo każdy start Pythona, w tym start pipa, odpala ponownie cook.pth oraz sitecustomize.py. Najpierw z powłoki usuń trzy pliki i ich skompilowane kopie z __pycache__ w każdym dotkniętym site-packages, potem zabij oderwanego daemona Pythona, rozpoznasz go po blokadzie /tmp/remote_code_loader.lock i katalogu roboczym w korzeniu systemu, a dopiero wtedy instaluj 1.106.2; najlepiej zbudować środowisko wirtualne albo obraz od zera.
Rotacja ma objąć wszystko, czego host mógł dotknąć: klucze API dostawców z bazy proxy wraz z kluczem głównym i wirtualnymi, hasło do DATABASE_URL, role IAM instancji, poświadczenia chmurowe z plików oraz sekrety ze zmiennych środowiskowych procesów na tej maszynie. W chmurze sprawdź w CloudTrail wywołania GetCallerIdentity z roli instancji wokół czasu instalacji, a potem użycie jej poświadczeń spoza swojej infrastruktury. Przy nowej instalacji trzymaj pin: pip install litellm==1.106.2, a przy Dockerze tag v1.106.2 przypięty po digescie, bo pływające tagi zostały na ten moment po prostu usunięte i docker pull litellm/litellm:latest zwraca błąd, co jest stanem pożądanym, nie awarią.
Szerszy obraz: PyPI i ataki supply chain
To już drugi udokumentowany przejm LiteLLM w tym roku, o czym przypomniał Hacker News, gdzie wątek zgromadził w jeden dzień ponad 170 punktów. W marcu 2026 pipeline projektu uruchomił niezapiętą, złośliwą wersję 0.69.4 skanera Trivy, przez co wyciekły poświadczenia wydawnicze i na PyPI wylądowały zbackdoorowane wersje 1.82.7 i 1.82.8, zbierające poświadczenia, rozlewające się po węzłach Kubernetesa i instalujące trwałe tylnie drzwi; ślad prowadził do grupy TeamPCP. JFrog, który opisał tamten przypadek, zaznacza, że nowy atak używa innego payloadu i innej infrastruktury, bez dowodów na łącze ze starym, więc to osobne kampanie.
W komentarzach przewija się zmęczenie. Użytkownik stbenjam napisał krótko: „Raz można wybaczyć, dwa razy to już malpractice”, a Cu3PO42 dodał, że dwa razy w rok to fatalny wygląd i szuka alternatywy, choć docenia, że utrzymanie proxy między zmieniającymi się API dostawców to sama praca. Z praktycznych wniosków z wątku: przypinanie wersji, opóźnianie wciągania świeżych wydań oraz audyt procesu wydawniczego, bo litellm nigdy nie publikował attestacji PyPI i nie było technicznego sposobu odróżnić przerobionego builda od legalnego. Wniosek na chłodno jest jeden i stary jak zależności: każde pip install to akt zaufania do całego łańcucha ludzi i maszyn stojących za paczką, a pin i skan hashy to najtańsze, co można z tym zrobić dzisiaj.
Najczęstsze pytania
Co to jest litellm?
Biblioteka Pythona standaryzująca dostęp do modeli językowych różnych dostawców przez jedno API oraz serwer proxy do zarządzania kluczami, budżetami i routingiem modeli. U nas opisuje ją linkowany wyżej poradnik o własnym proxy LLM.
Czy litellm proxy z Dockera jest bezpieczny?
Tak, jeśli obraz był przypięty do czystego taga, na przykład v1.106.2, najlepiej po digescie. Zainfekowane były v1.107.0 oraz pływające latest, main-latest, main-stable w oknie 21 października; te tagi usunięto z rejestrów, więc najbezpieczniej pobrać czysty tag na nowo.
Jak teraz zainstalować litellm proxy?
W czystym środowisku, po usunięciu plików payloadu: pip install litellm==1.106.2, przy kontenerach docker pull ghcr.io/berriai/litellm:v1.106.2 przypięty po digescie. 1.106.2 to najnowsza czysta wersja na PyPI w chwili pisania.
Czym są ataki pypi malware?
To ataki supply chain na ekosystem Pythona: złośliwy kod trafia do paczki na PyPI przez skompromitowane konto wydawcy, przerobiony proces wydawniczy albo typosquatting nazwy, a ofiarą jest każdy, kto paczkę zainstaluje. W tym przypadku wektor stanowiły skradzione tokeny OAuth GitHub CLI trzech inżynierów projektu.
Źródła
- SafeDep, Compromised LiteLLM 1.107.0 on PyPI Installs a Credential Stealer and Backdoor, odczyt 21.10.2026: https://safedep.io/litellm-pypi-compromise-credential-stealer-backdoor/
- Zgłoszenie Upwind i komentarze BerriAI, issue 47826, odczyt 21.10.2026: https://github.com/BerriAI/litellm/issues/47826
- Wątek Hacker News 50181114, odczyt 21.10.2026: https://news.ycombinator.com/item?id=50181114
- JFrog Security Research, LiteLLM Hijacked Again, odczyt 21.10.2026: https://research.jfrog.com/post/litellm-hijacked-again/
- Wątek pierwszego incydentu HN 47501426, odczyt 21.10.2026: https://news.ycombinator.com/item?id=47501426
- API PyPI dla paczki litellm, odczyt 21.10.2026: https://pypi.org/pypi/litellm/json
- Statystyki pobrań pypistats, odczyt 21.10.2026: https://pypistats.org/api/packages/litellm/recent
- Zgłoszenie do bazy OSSF malicious-packages PR 1655, odczyt 21.10.2026: https://github.com/ossf/malicious-packages/pull/1655
