On October 21, 2026, litellm==1.107.0 landed on PyPI with a credential stealer and a remote-code backdoor baked in. PyPI pulled it about two hours later.
This is the second time LiteLLM has done this. In March, versions 1.82.7 and 1.82.8 shipped malware too. Same package, same class of payload, seven months apart.
If you run LLM gateways, agents, or anything that pip installs into a box holding provider API keys, read on.
TL;DR
- Bad version:
litellm==1.107.0(plus Docker images published the same day). - Clean versions:
1.106.2,1.107.0rc1,1.107.0rc2,litellm-proxy-extras==0.4.110,litellm-enterprise==0.1.78. - The malware runs on every Python process start, not only when you import litellm.
- It steals cloud creds, SSH keys, AI tool tokens, and the LiteLLM proxy DB's provider keys.
- If it ran, rotate everything on that machine. Uninstalling is not enough.
Timeline (UTC, Oct 21)
| Time | Event |
|---|---|
| 10:08 | Attacker bypasses branch protection using stolen GitHub CLI credentials |
| 11:39-11:40 | Seven malicious wheels uploaded to PyPI |
| 11:46 | Compromised Docker images published |
| 13:23 | PyPI removes 1.107.0 |
| 14:20 | Docker Hub / GHCR repointed to clean builds |
Source: SafeDep's analysis. Exposure window on PyPI: roughly 1h45m. For a package with millions of daily downloads, that's plenty.
The part that should bother you: the build was legit
The attacker didn't typosquat. They didn't phish a PyPI token. Per SafeDep, they held GitHub CLI credentials for three LiteLLM developers, reportedly pulled from exposed internal deployment logs. With an org owner's token they:
- Overrode branch protection.
- Pointed the private release workflow at a lookalike repo.
- Used a second developer's credential to clear the approval gate.
Then LiteLLM's own CI built, signed, and published the artifacts. Everything downstream looked official, because it was.
Signatures and "published from the real pipeline" tell you the pipeline ran. They don't tell you the pipeline was running your maintainers' intent.
How the payload runs
One line added to pyproject.toml:
data = "cook"
That made the build drop extra files into site-packages:
-
cook.pth: 12 bytes. Python processes.pthfiles at startup, and lines starting withimportget executed. It importscook.py. -
sitecustomize.py: auto-imported by Python, forks a detached daemon.
Result: the stealer fires in any Python process on that interpreter. Your Jupyter kernel, your pytest run, pip itself. You never have to import litellm.
If this sounds familiar, it should. March's 1.82.8 used a litellm_init.pth the same way. A .pth file is one of the best persistence tricks in Python and almost nobody audits them.
What it steals
SafeDep lists 16 collection modules. Highlights:
-
Cloud: AWS, Azure, GCP configs, Kubernetes service tokens, Docker config, plus IMDS creds from
169.254.169.254 -
Keys:
~/.ssh/*, GPG, TLS private keys,.pem/.key/.pfx -
CI/CD: GitLab runner
config.toml, JenkinsinitialAdminPassword, Terraform creds,.vault-token - AI tooling: Claude Code, Cursor, Copilot, Gemini CLI, Hugging Face, Codex settings
-
LiteLLM proxy: direct DB dump of
LiteLLM_CredentialsTableandLiteLLM_ProxyModelTable, i.e. every upstream provider key you gave the gateway - Everything else: env vars, shell history, browser-saved passwords
Data is encrypted with a random AES-256-CBC key, wrapped with an embedded RSA-4096 key, and POSTed to www.litellm[.]dev/healthcheck. Note that domain: it looks like it belongs to the project. Your egress allowlist might wave it through.
The backdoor is the real problem
The stealer is a smash-and-grab. The backdoor is why rotation matters.
sitecustomize.py daemonizes and, every hour:
- Reads a domain from a file in an attacker-controlled GitHub repo.
- Re-runs the stealer (no once-per-host guard, so it re-exfils).
- Downloads
http://<domain>/codepyandexec()s it.
Because the target domain is read from GitHub, the attacker could repoint the whole fleet without publishing a new package. The GitHub account was deleted at 13:40 UTC, which kills that control channel, but any host that already ran the second stage could have received arbitrary code.
Am I hit? Check in 60 seconds
# Version
pip show litellm | grep -i '^version'
# Persistence files, in every env you have
find / \( -name 'cook.pth' -o -name 'cook.py' \) -path '*site-packages*' 2>/dev/null
find / -name sitecustomize.py -path '*site-packages*' 2>/dev/null
# Marker files
ls -la /tmp/.pc_once_72129118c59f5636c0bb /tmp/remote_code_loader.lock 2>/dev/null
# Daemon: detached, parent PID 1, cwd /
ps -eo pid,ppid,cmd | awk '$2==1 && /python/'
Also grep egress/DNS logs for litellm.dev and the header X-Token: XWiA8gZNa7NcX2XRoAEb. Treat any Python process talking to 169.254.169.254 that shouldn't be as suspect.
Containers: check your image build dates. Anything built on Oct 21 between 11:46 and 14:20 UTC from the official tags needs a rebuild and a hard look.
If you were hit
- Isolate the host. Don't just
pip uninstall. - Rotate: cloud keys, SSH keys, Kubernetes tokens, every LLM provider key (OpenAI, Anthropic, Gemini, etc.), CI secrets, Vault tokens, Hugging Face, and the tokens for your coding agents.
- Audit cloud access logs (CloudTrail, GCP audit, Azure activity) from the install time forward.
- Rebuild from clean, don't clean in place.
What to change before the next one
This will happen again. It already has. Practical hardening, in order of payoff:
1. Pin with hashes. Floating litellm>=1.x is how you pull a bad release within minutes of publish.
uv pip compile requirements.in --generate-hashes -o requirements.txt
uv pip install --require-hashes -r requirements.txt
2. Add a cooldown. Most malicious releases are caught within hours. Don't install anything newer than a day or two.
# pyproject.toml (uv)
[tool.uv]
exclude-newer = "2026-10-20T00:00:00Z"
3. Stop putting every key on one box. A gateway holding all your provider keys is a single jackpot. Scope keys per service, set spend caps, and keep the LLM proxy off machines with SSH keys and cloud admin creds.
4. Lock down egress. Your LLM gateway needs to talk to a handful of provider domains. Allowlist those. litellm.dev/healthcheck shouldn't have gotten out.
5. Audit .pth and sitecustomize.py. Add this to CI and image scans:
python - <<'EOF'
import site, pathlib
for d in site.getsitepackages():
for p in pathlib.Path(d).glob("*.pth"):
t = p.read_text(errors="ignore")
if any(l.startswith(("import ", "import\t")) for l in t.splitlines()):
print("EXEC .pth:", p)
sc = pathlib.Path(d, "sitecustomize.py")
if sc.exists():
print("sitecustomize:", sc)
EOF
Some legit packages ship executable .pth files, so this is a review list, not an alarm.
6. Protect your maintainers' tokens (if you publish packages). Org-owner tokens that can override branch protection are the crown jewels. Use short-lived credentials, require hardware-key 2FA, never log tokens, and make release workflows immutable from a single credential. PyPI Trusted Publishing helps, but as this attack shows, it only moves the trust to your repo's permissions.
My take
The industry keeps treating AI infrastructure like a pile of convenient wrappers. It's not. A LiteLLM install is a credential vault with a Python API on top. Attackers noticed first.
Two incidents in seven months means this is the baseline now, not an anomaly. The fix isn't "trust LiteLLM less." The fix is architecture that assumes any dependency can ship malware on any given Tuesday: pinned hashes, cooldowns, scoped keys, locked egress.
Go check your lockfiles. Then go check your site-packages.
Sources: SafeDep: Compromised LiteLLM 1.107.0 · FutureSearch: minute-by-minute response to the March attack · Sonatype on the March compromise · Hacker News discussion
Top comments (0)