DEV Community

Cover image for LiteLLM 1.107.0 stole credentials from every Python process. I checked PyPI and reproduced the mechanism with a canary
frank chu
frank chu

Posted on

LiteLLM 1.107.0 stole credentials from every Python process. I checked PyPI and reproduced the mechanism with a canary

This morning, SafeDep disclosed that LiteLLM 1.107.0 — published on PyPI today, October 21 — was backdoored. Not "had a vulnerability." Every one of its seven wheels installed a credential stealer and a remote-code-execution backdoor into site-packages, arranged so that both run every time any Python process starts on the host, including processes that never import litellm.

LiteLLM is the library a lot of teams use to route calls to LLM providers — which means its proxy databases are exactly where organizations keep their provider API keys. The stealer knew that: among its targets, per SafeDep's analysis, are SSH keys, cloud credentials (~/.aws, Azure, gcloud, kubeconfig), browser passwords and history, shell histories, CI/CD secrets, AI-tool configs (Claude Code, Cursor, Codex, and more), the environment of other processes via /proc/<pid>/environ, AWS instance metadata — and the LiteLLM_CredentialsTable itself. Collected data was encrypted and POSTed to a lookalike domain. A second payload, planted as sitecustomize.py, forked a detached daemon that re-ran the stealer hourly and downloaded fresh code to exec() from a server whose address it read from a GitHub file.

I did not install 1.107.0, and you shouldn't either — by the time I checked, there was nothing left to install. What I did instead is verify the current state of PyPI myself, check my own machine against the published indicators, and reproduce the mechanism with a harmless canary, because the mechanism is the part that generalizes to the next incident.

What PyPI looks like right now (my queries, this afternoon)

GET pypi.org/pypi/litellm/1.107.0/json   → 404
GET pypi.org/pypi/litellm/1.106.2/json   → 200, 8 files
GET pypi.org/pypi/litellm/1.107.0rc2/json → 200, 8 files
project "latest" version                  → 1.106.2
"1.107.0" in the project's release list   → absent entirely
Enter fullscreen mode Exit fullscreen mode

Details that line up with SafeDep's timeline (all times UTC, today):

1.107.0rc1  uploaded Oct 18 07:54   clean (per SafeDep)
1.107.0rc2  uploaded Oct 20 03:17   clean (per SafeDep)
1.106.2     uploaded Oct 21 10:44   7 wheels + sdist, none yanked
1.107.0     uploaded Oct 21 ~11:39  REMOVED — 404 everywhere
attacker's GitHub account (C2 pointer) deleted ~13:40
Enter fullscreen mode Exit fullscreen mode

So the malicious release lived on PyPI for roughly two hours, and the registry's current state is consistent with a full removal rather than a yank: the version doesn't just have its files pulled, it's gone from the release index, and pip install litellm now resolves to 1.106.2. Two hours is a short window and a long one, depending on how many CI systems pin nothing and install at 12:00 UTC on a Wednesday.

How did it get in? SafeDep's forensic work — worth reading in full — found that the sdist's pyproject.toml gained one line, a maturin data = "cook" setting that copies a cook/ folder into the wheel's purelib root, i.e. straight into site-packages. The folder isn't in the sdist and isn't in any public commit. The wheels carry correct RECORD hashes and platform-consistent build fingerprints, which points at the project's own (private) release pipeline building from attacker-modified source, not at someone uploading hand-edited wheels with a stolen token. PyPI provenance attestations: none, for this or the neighboring clean releases.

The mechanism, reproduced with a canary

The part I could test safely: why "runs in every Python process" is literally true. Python executes lines starting with import in .pth files in site-packages at interpreter startup, and imports sitecustomize at startup if it finds one. I built a throwaway venv with a benign cook.pth/cook.py that just appends its PID to a file:

printf 'import cook\n' > site-packages/cook.pth
# cook.py: append "cook ran in pid <pid>" to /tmp/canary_cook_ran.txt

python -c "pass"
python -c "import sys; print('litellm' in sys.modules)"
Enter fullscreen mode Exit fullscreen mode
litellm imported? False
--- canary marker file:
cook ran in pid 17899
cook ran in pid 17900
Enter fullscreen mode Exit fullscreen mode

Two invocations, neither importing anything, both executed the planted module. That's the whole trick, and it's not exotic — .pth execution is a documented Python feature. The stealer needed no import hook, no sitecustomize race, nothing clever at runtime. The cleverness was all in getting three extra files through a real release pipeline.

One honest wrinkle from my own test: on this Ubuntu VM, my canary sitecustomize.py in the venv did not run, because /usr/lib/python3.12/sitecustomize.py sits earlier on sys.path and shadows it. On systems without a stdlib sitecustomize (stock python Docker images, macOS, Windows), the site-packages copy is the one Python finds. So the backdoor payload's reach varies by platform layout — but the .pth payload, the stealer, has no such dependency. It runs everywhere, unconditionally.

Checking a machine (what I ran on mine)

SafeDep published the indicators. The host-level checks are cheap:

site-packages contains cook.pth / cook.py / sitecustomize.py (planted)?  NO
/tmp/.pc_once_72129118c59f5636c0bb  (stealer already uploaded)           NO
/tmp/remote_code_loader.lock        (backdoor daemon ran)                NO
PRIVCOLLECT_ACTIVE in environment                                        NO
Enter fullscreen mode Exit fullscreen mode

This VM never had LiteLLM installed, so a clean result was expected — the point of publishing the checklist is that it's a two-minute check for any machine that might have installed anything Python-flavored today. If you ran pip install litellm or built a fresh environment between roughly 11:39 and 13:40 UTC today, treat the host as compromised, not as "probably fine": the stealer's target list is specifically the secrets that live on developer and CI machines, and rotating the LLM keys alone doesn't cover SSH keys, cloud credentials, and browser passwords.

The uncomfortable generalization

Strip the names off and the pattern is: one extra line in build config, three files at the root of site-packages, two Python startup mechanisms that have existed forever. Nothing about it required a zero-day. The defenses that would have caught it are equally unglamorous: a release-diff that asks "why does this wheel contain top-level modules that aren't in the repo," provenance attestations on uploads, and install-time auditing of .pth files — a file type whose entire legitimate user base is small and whose malicious potential is total.

LiteLLM will fix its pipeline. The next package's pipeline is the one to worry about. If your CI installs unpinned Python dependencies at build time, today's two-hour window was aimed at you.

Sources: SafeDep, "Compromised LiteLLM 1.107.0 on PyPI Installs a Credential Stealer and Backdoor," October 21, 2026 (safedep.io) — payload analysis, timeline, build forensics, and indicators of compromise are theirs. PyPI state, upload timestamps, host indicator checks, and the .pth/sitecustomize canary reproduction are my own, run this afternoon. I did not download or execute the malicious release.

Top comments (0)