LiteLLM 1.107.0 PyPI Compromise: How to Check, Remove the Backdoor and Rotate Keys
litellm 1.107.0 on PyPI (Oct 21, 2026) shipped a credential stealer and hourly backdoor via cook.pth and sitecustomize.py. Check if you're affected, remove it, pin 1.106.2, rotate keys.
TL;DR: On October 21, 2026, an attacker hijacked LiteLLM's release pipeline and published
litellm==1.107.0to PyPI. All seven wheels carried a credential stealer and an hourly remote-code loader, and both run every time Python starts, even if nothing importslitellm. The wheels were live from 11:39 to 13:23 UTC (20:39–22:23 KST). The official Docker tagsv1.107.0,latest,main-latestandmain-stablepointed at an infected image from 11:46 to 14:20 UTC (20:46–23:20 KST). If you installed 1.107.0 or pulled one of those tags in that window, treat the host as compromised: delete the implant files before you run Python again, rebuild onlitellm==1.106.2, and rotate every secret the machine could reach. (LiteLLM incident report, SafeDep analysis)
What happened
LiteLLM is the Python SDK and proxy that puts 100+ LLM providers behind one OpenAI-style API, so it often runs on machines that hold OpenAI, Anthropic, Azure, Bedrock and Vertex keys. According to LiteLLM's own report, an attacker who held GitHub CLI credentials belonging to three LiteLLM developers used an org owner's token to override branch protection on the private release workflow. They then pointed the workflow at a lookalike repo, Berri-AI/litellm (the real one is BerriAI/litellm). The official pipeline built, signed and published the poisoned artifacts. The v1.107.0 tag in the real GitHub repo is clean. LiteLLM thinks the GitHub tokens leaked through logs of an internal LiteLLM deployment that was exposed to the internet.
SafeDep found one injected line in the sdist's pyproject.toml, data = "cook" under [tool.maturin]. That line makes every wheel ship three extra files in litellm-1.107.0.data/purelib/, and pip copies them to the root of site-packages:
| File | Size | What it does |
|---|---|---|
cook.pth |
12 bytes | Contains import cook. Python runs it at every interpreter start |
cook.py |
22,252 bytes | zlib+base64-wrapped credential stealer that calls itself privcollect |
sitecustomize.py |
25,509 bytes | Forks a detached daemon that fetches and runs remote code every hour |
The stealer reads browser passwords and history, ~/.ssh, shell history, AWS/GCP/Azure credentials, ~/.kube/config, ~/.docker/config.json, the GitHub CLI hosts.yml, .netrc, Vault/Terraform/GitLab-runner/Jenkins secrets, configs from AI coding tools (Claude Code, Cursor, Windsurf, Copilot, Codex, Gemini CLI and more), GPG and TLS keys, and the environments of other processes via /proc/*/environ. It also queries cloud metadata (169.254.169.254). If DATABASE_URL is set, it dumps the LiteLLM_CredentialsTable and LiteLLM_ProxyModelTable tables, which hold a LiteLLM proxy's upstream provider keys. The loot is encrypted with AES-256 plus an RSA-4096 key and POSTed to http://www.litellm[.]dev/healthcheck with the header X-Token: XWiA8gZNa7NcX2XRoAEb. After a successful upload it writes the marker /tmp/.pc_once_72129118c59f5636c0bb. (SafeDep, OSV MAL-2026-2144)
The backdoor takes a lock on /tmp/remote_code_loader.lock, detaches with fork()/setsid(), and every hour reads a domain from raw.githubusercontent.com/letters9813/pythonclient-test/main/domain.txt. It then re-runs the stealer and executes whatever http://<domain>/codepy returns. SafeDep reports the letters9813 account returned 404 at 13:40 UTC, so new hosts get no server address now. Hosts that ran the daemon before then may already have received second-stage code, and nobody has publicly recovered that domain.
This is LiteLLM's second PyPI compromise this year. In March 2026, versions 1.82.7 and 1.82.8 were malicious, and 1.82.8 shipped a litellm_init.pth that also ran at interpreter start (GitHub issue #24512). Neither version is listed on PyPI anymore.
Timeline
All times are October 21, 2026, from LiteLLM's report unless noted.
| UTC | KST | Event |
|---|---|---|
| 10:08 | 19:08 | Privileged credential overrides branch protection; release workflow modified |
| 10:46 | 19:46 | Staging run builds from lookalike repo Berri-AI/litellm |
| 11:39–11:40 | 20:39–20:40 | Seven malicious 1.107.0 wheels uploaded to PyPI |
| 11:46 | 20:46 | Malicious Docker images published (v1.107.0, latest, main-latest, main-stable) |
| 11:53 | 20:53 | A third developer credential deletes the workflow run records |
| 12:06 | 21:06 | Public report filed: GitHub issue #47826 |
| 13:23 | 22:23 | PyPI has removed 1.107.0 |
| 13:40 | 22:40 | Attacker's GitHub dead-drop account returns 404 (SafeDep) |
| 13:51 | 22:51 | Affected GHCR tags unavailable |
| 14:00 | 23:00 | SafeDep write-up posted to Hacker News (469 points, 158 comments when checked) |
| 14:20 | 23:20 | All Docker Hub tags removed or repointed to clean images |
As of October 22, the PyPI JSON API returns 404 for litellm/1.107.0 and lists 1.106.2 as the latest release.
Step 1: Check whether you are affected
You are exposed if anything resolved an unpinned litellm during the PyPI window, or if a LiteLLM container started from an affected tag during the Docker window. Remember CI runners, dev laptops, notebooks and agent sandboxes, not only production.
Important: pip show and pip freeze start Python, and on an infected environment that runs the payload again. On a suspect host, search the filesystem first. Use pip only on machines you already believe are clean.
Linux / macOS (no Python needed):
# implant files and the bad dist-info, anywhere on disk
sudo find / -xdev \( -name 'cook.pth' -o -name 'cook.py' -o -name 'litellm-1.107.0.dist-info' \) \
-path '*-packages*' 2>/dev/null
# sitecustomize.py placed at a site-packages root (check content before deleting)
sudo find / -xdev -path '*site-packages/sitecustomize.py' 2>/dev/null \
-exec grep -l -E 'remote_code_loader|letters9813|privcollect' {} +
# markers left by the payload
ls -la /tmp/.pc_once_72129118c59f5636c0bb /tmp/remote_code_loader.lock 2>/dev/null
Windows (PowerShell):
Get-ChildItem -Path C:\ -Recurse -Force -ErrorAction SilentlyContinue `
-Include cook.pth,cook.py,sitecustomize.py,'litellm-1.107.0.dist-info' |
Where-Object FullName -match 'site-packages' | Select-Object FullName,Length,LastWriteTime
Lockfiles and requirements (on any machine):
grep -rn --include='*.txt' --include='*.lock' --include='*.toml' -E 'litellm[^a-z_-]*1\.107\.0' .
grep -n -A2 'name = "litellm"' uv.lock poetry.lock 2>/dev/null
If a lockfile says 1.107.0, any environment built from it after 11:39 UTC on October 21 is affected. Also check CI logs for Successfully installed ... litellm-1.107.0 or + litellm==1.107.0. Look in package caches too: find ~/.cache/pip ~/.cache/uv ~/Library/Caches -name 'litellm-1.107.0*' 2>/dev/null. Purge any hits so a later offline install can't reuse them.
Docker and Kubernetes:
docker images --digests | grep -i litellm
docker ps -a --format '{{.ID}} {{.Image}} {{.CreatedAt}}' | grep -i litellm
kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}/{.metadata.name}{"\t"}{.status.containerStatuses[*].imageID}{"\n"}{end}' | grep -i litellm
The compromised image index for berriai/litellm on Docker Hub and GHCR is sha256:1bbcb1443bc962e45f5aad53fe3d2c30600e0379715fb6c664cdd3ac126c339e. LiteLLM also lists litellm-database (sha256:c00f9d92c427…) and litellm-non_root (sha256:7b00e91cd2fe…). Any container that ran one of those is compromised. Because latest and main-stable were repointed, a container that pulled them during the window can still be running the bad image today.
For extra confirmation, the planted files on Linux/macOS hash to cook.pth = 7f191632…b92bdb, cook.py = 9dd0a392…90be25, sitecustomize.py = 34bef04f…8305fa (full hashes in OSV). Windows copies hash differently because of CRLF line endings; see SafeDep's IOC table. Finding a file confirms exposure, but LiteLLM warns that not finding one doesn't prove the host is clean, because container restarts and /tmp cleanup erase evidence.
Step 2: Remove the implant (before running Python)
- Delete the files from the shell in every affected
site-packages:cook.pth,cook.py,sitecustomize.py(only the malicious one, as identified by the grep above), and__pycache__/cook*.pyc/__pycache__/sitecustomize*.pyc. - Kill the daemon. On Linux/macOS:
sudo lsof /tmp/remote_code_loader.lock(orfuser -v) shows the PID. It is a detachedpythonprocess with parent PID 1, its own session, and working directory/. On Windows, look for unexplainedpython.exeprocesses. - Rebuild instead of patching. LiteLLM's guidance is to reinstall pinned to
litellm==1.106.2, but recreating the venv or image from scratch is better. The second-stage loader could have dropped anything.
rm -rf .venv && python -m venv .venv && . .venv/bin/activate
pip install 'litellm==1.106.2'
# uv
uv add 'litellm==1.106.2' && uv lock && uv sync
# poetry
poetry add 'litellm==1.106.2'
For containers, deploy a clean tag by digest (for example v1.106.2) and destroy the old containers, volumes and node caches. Don't restart them.
Safe versions: 1.106.2 and earlier explicit releases are clean, and so are 1.107.0rc1/rc2. LiteLLM says "all previous versions are safe" (the March releases 1.82.7/1.82.8 were removed and were malicious too). The companion packages litellm-proxy-extras==0.4.110 and litellm-enterprise==0.1.78 were checked clean by SafeDep.
Step 3: Rotate credentials
If 1.107.0 ever ran on a host with outbound internet, LiteLLM says to assume exfiltration succeeded. Rotate from a clean machine, starting with what's most valuable:
- LLM provider keys: OpenAI, Anthropic, Azure OpenAI, Gemini/Vertex, Bedrock, Cohere, Mistral, OpenRouter, Hugging Face. Revoke them and issue new ones; don't just add new keys alongside the old ones.
- LiteLLM proxy: every provider key stored in the proxy DB, the master key, all virtual keys, and the
DATABASE_URLpassword. - AWS: deactivate access keys in
~/.aws. For instance and task roles, use IAM's Revoke active sessions and replace the role credentials. - GCP: rotate service-account keys and revoke ADC (
gcloud auth revoke,gcloud auth application-default revoke). - Azure: revoke CLI sessions and refresh tokens; rotate service-principal secrets.
- Kubernetes: rotate service-account tokens and kubeconfig credentials, and any cluster secrets mounted into affected pods.
- GitHub and Git: revoke
ghOAuth tokens and PATs,.git-credentials, deploy keys, and SSH keys. Then audit org audit logs for new tokens and workflow changes. - Registries and CI:
.pypirc,.npmrc, Docker Hub/GHCR tokens, Vault tokens, Terraform Cloud tokens, GitLab runner and Jenkins secrets. .envfiles and process environments: every secret in the environment of any process on the host, not just your app.- People: browser-saved passwords and sessions on affected dev laptops, plus AI coding tool logins (Claude Code, Cursor, Codex and others).
Step 4: Check logs for exfiltration and follow-on access
- DNS, proxy and firewall logs: look for
www.litellm[.]dev(POST to/healthcheck),raw.githubusercontent.com/letters9813/..., and anyGET /codepyto an unknown host. TheX-Tokenheader above is a strong signal if you capture HTTP. - AWS CloudTrail:
GetCallerIdentityfrom the instance role around install time, then any use of that role's credentials from IPs outside your infrastructure (LiteLLM's own recommendation). - Metadata calls: Python processes hitting 169.254.169.254 or 169.254.170.2 when they normally don't.
- Provider dashboards: unusual usage spikes or new keys at OpenAI, Anthropic and others; new IAM users or keys; new GitHub tokens, deploy keys, or workflow edits.
- Host: the
/tmp/.pc_once_…marker, the lock file, the env varPRIVCOLLECT_ACTIVE=1in process environments, and unexplained cron or launch agents (second-stage code isn't documented, so look broadly).
Step 5: Prevent the next one
- Pin exact versions with hashes. Generate hashes with
pip-compile --generate-hashesoruv pip compile --generate-hashes, then install withpip install --require-hashes -r requirements.txt. Commituv.lock/poetry.lockand install withuv sync --locked/poetry sync. Hash pinning won't stop you from choosing a bad new version, but it does stop silent drift to one. - Add a release cooldown. Most malicious releases are caught within hours. uv's
exclude-newersetting ignores packages uploaded after a cutoff, and Dependabot/Renovate can wait several days before proposing upgrades. This one was reported publicly about 30 minutes after upload and pulled within two hours. - Scan continuously.
pip-audit -s osvand OSV-Scanner check your lockfiles against OSV, which lists 1.107.0 under MAL-2026-2144. Run them in CI and on a schedule, not only on PRs. - Pin containers by digest, never
latestormain-stable. Note that the bad images here were signed by the real pipeline, so a valid signature alone wasn't enough. - Trusted Publishing isn't a silver bullet. LiteLLM already used it. The attacker abused the publisher workflow itself, so watch for unexpected releases of packages you depend on.
- Limit what Python can reach. Run proxies with least-privilege roles, IMDSv2 with hop limit 1, egress allow-lists, and no long-lived keys on disk. Keep secrets out of logs: LiteLLM traced the root cause to tokens in request logs.
Quick checklist
- Searched every host, runner and image for
cook.pth,cook.py, a malicioussitecustomize.py, andlitellm-1.107.0.dist-info - Grepped lockfiles, CI logs and pip/uv caches for
1.107.0 - Checked running containers and pods for digest
sha256:1bbcb1443bc9… - Deleted implant files and killed the daemon before running Python
- Rebuilt venvs and images on
litellm==1.106.2, pinned with hashes - Rotated LLM keys, proxy DB/master/virtual keys, cloud roles and keys, K8s, GitHub, SSH, registry and
.envsecrets - Reviewed DNS/egress logs for
www.litellm[.]devand CloudTrail forGetCallerIdentity - Added hash pinning, a release cooldown, and OSV scanning to CI
FAQ
I only use import litellm occasionally. Am I safe if I never imported it?
No. The .pth and sitecustomize.py files run when any Python process starts in that environment, including pip itself.
I'm on 1.106.x or older. Do I need to do anything? Make sure the version is pinned so nothing upgrades you unexpectedly, and confirm no CI job or image resolved 1.107.0 on October 21. No other versions from this incident are reported as affected.
Is the GitHub source for v1.107.0 compromised?
No. LiteLLM, SafeDep and OSV all say the official tag is clean. The artifacts were built from the lookalike Berri-AI/litellm repo.
Was 1.107.0 "yanked"? It was removed. PyPI no longer serves the release at all, so a pin to it will now fail to resolve.
Is the attacker's server still active? The GitHub dead drop was gone by 13:40 UTC, so new infections can't find a command server. The exfil domain and any second stage are still not fully characterized, so assume stolen secrets are in the attacker's hands.
Were enterprise customers affected? LiteLLM says customers pulling from its private enterprise registry weren't affected.
What's still unverified
LiteLLM's report says the investigation is ongoing. As of this writing, how the attacker first got into the internal deployment, the second-stage server address and payload, and the number of affected installs haven't been publicly confirmed.
Sources
- LiteLLM: Contained, Oct 21, 2026 release v1.107.0 compromised
- SafeDep: Compromised LiteLLM 1.107.0 on PyPI installs a credential stealer and backdoor
- GitHub issue BerriAI/litellm#47826
- OSV MAL-2026-2144
- StepSecurity analysis
- Wiz: LiteLLM compromised, steals credentials
- PyPI release history for litellm
- Hacker News discussion
- March 2026 incident: GitHub issue #24512
This guide reflects public reports as of October 22, 2026; follow LiteLLM's incident page for updates.
← Back to all posts