Compromised LiteLLM 1.107.0 on PyPI Installs a Credential Stealer and Backdoor
The LiteLLM 1.107.0 release on PyPI carries a credential stealer and a remote code execution backdoor. All seven wheels (Linux, macOS, Windows) install them as three files in site-packages. Both run when any Python process starts on the host, even one that never imports litellm. The upstream GitHub source does not contain them.
LiteLLM confirmed the compromise in its incident report, which lists remediation steps. PyPI removed the release by 13:23 UTC on 2026-10-21. The Docker images v1.107.0, latest, main-latest, and main-stable on Docker Hub and GHCR also carried the malicious code between 11:46 and 14:20 UTC.
Versions 1.106.2, 1.107.0rc1, and 1.107.0rc2 are clean. The companion packages that 1.107.0 pins, litellm-proxy-extras==0.4.110 and litellm-enterprise==0.1.78, are also clean.
What the attack does
Installing litellm==1.107.0 places three files in site-packages. Python loads two of them each time it starts.
cook.pthimportscook.py, a credential stealer. It collects SSH keys, cloud credentials, CI/CD secrets, AI tool tokens, browser passwords, and the LLM provider keys in a LiteLLM proxy database. It encrypts the data and sends it towww.litellm[.]dev.sitecustomize.pystarts a background daemon. Every hour, the daemon reads a server address from a GitHub file, runs the stealer again, and downloads and runs code from that server.
How the implant got into the release
The pyproject.toml in the 1.107.0 source distribution (sdist) has one extra line in the maturin build section:
[tool.maturin]
...
python-source = "."
data = "cook" # /environ` |
| `db_litellm` | Reads `DATABASE_URL` and dumps the `LiteLLM_CredentialsTable` and `LiteLLM_ProxyModelTable` tables, which hold the LLM provider API keys of a LiteLLM proxy |
| `cloud_metadata` | AWS instance metadata service (IMDSv2, then v1): instance identity and role credentials |
| `gpg_keys`, `tls_privkey` | GPG keys, TLS private keys |
### How it sends the data
1. It encrypts the JSON results with a random AES-256-CBC key, and encrypts that key with RSA-OAEP and the embedded public key.
2. It packs the result into a tar.gz (`hdr.json`, `data.bin`, `key.bin`) and sends it in a POST request to `www.litellm[.]dev/healthcheck` with the header `X-Token: XWiA8gZNa7NcX2XRoAEb`.
3. After a successful upload, it writes `/tmp/.pc_once_72129118c59f5636c0bb` so it collects only once per host. It also sets `PRIVCOLLECT_ACTIVE=1` to avoid a second run in the same process tree.
## Payload 2: the backdoor in sitecustomize.py
Python imports `sitecustomize` at startup, so this file also runs in every Python process.
1. It takes an exclusive lock on `/tmp/remote_code_loader.lock`, so only one daemon runs per host.
2. It calls `fork()` and `setsid()`, sends standard input and output to `/dev/null`, and changes directory to `/` to detach from the parent process.
3. Every hour, it reads `domain.txt` from the attacker’s GitHub repository, runs its embedded copy of the stealer, and downloads and runs code from the attacker's server.
At 13:40 UTC, the GitHub account `letters9813` and its repository returned 404, about two hours after the release. The backdoor now finds no server address and does nothing.




