Skip to content
chornous.dev

Type two or more letters. Esc closes.

Index of sheets
Theme

Sheet 03 · Writing

All notes

LiteLLM 1.107.0 shipped a credential stealer to PyPI and Docker for two hours: what to delete and what to rotate

Attackers published a malicious litellm 1.107.0 on October 21, 2026, through LiteLLM's own release workflow pointed at a lookalike fork. The wheels drop cook.pth, cook.py and sitecustomize.py into site-packages, which run on every Python start. The last clean version is 1.106.2.

Pl. 70 · section drawing generated from the slug “litellm-1-107-0-supply-chain-compromise”

A malicious litellm 1.107.0 went up on PyPI at 11:39 UTC on October 21, 2026. PyPI quarantined it at 13:20 UTC, and LiteLLM pulled the matching Docker images at 13:53 UTC. Datadog Security Labs published the analysis this post draws on. The maintainers track the incident in BerriAI/litellm issue #47826. The last clean release is 1.106.2.

If you install LiteLLM unpinned, run its proxy from latest or main-stable, or rebuilt a CI image that afternoon, you may have pulled it.

How it got published

The attackers held GitHub CLI OAuth tokens from three LiteLLM engineers. Datadog says it isn't clear yet how they got them. They used those tokens to start LiteLLM's private release workflow against Berri-AI/litellm, a fork whose name differs from the real BerriAI/litellm by one hyphen. They approved the run and deleted the evidence.

The wheels came out of LiteLLM's real pipeline, so they carry build provenance. That provenance names the malicious fork as the source. A provenance check that asks "did LiteLLM's workflow build this?" says yes. A check that pins the source repository to BerriAI/litellm says no.

Artifacts affected by the litellm 1.107.0 compromise
WhereWhat
PyPIlitellm==1.107.0, wheels for macOS, manylinux, musllinux and Windows
GHCR and Docker Hubtags v1.107.0, latest, main-latest, main-stable
Image index digestsha256:1bbcb1443bc962e45f5aad53fe3d2c30600e0379715fb6c664cdd3ac126c339e

Uninstalling won't clean it

The wheel adds three files to site-packages:

  • cook.pth
  • cook.py
  • sitecustomize.py

Python executes .pth files and sitecustomize.py each time the interpreter starts. The malware runs in that environment even if your code never imports litellm. pip uninstall litellm removes the package directory and leaves these three files in place, and the pip you ran to uninstall starts Python, which runs the malware again.

cook.py collects secrets, encrypts them with AES-256 and RSA-4096, and posts them to www.litellm[.]dev/healthcheck with the header X-Token: XWiA8gZNa7NcX2XRoAEb. The real project lives at litellm.ai. The list of targets covers most of a developer laptop or CI runner:

  • cloud credentials for AWS, GCP and Azure, instance metadata included, plus Kubernetes, Docker, Vault and Terraform secrets
  • SSH, GPG and TLS keys, Git, npm and PyPI tokens, .env files and shell history
  • API keys and sessions for Claude, Codex, Gemini, Cursor, Copilot and other AI tools
  • saved browser passwords and cookies
  • on a LiteLLM proxy with a database, the tables that hold upstream provider keys

sitecustomize.py is a backdoor. Each hour it reads a domain name from a file in a GitHub repository, letters9813/pythonclient-test, then downloads and runs Python from that domain. The repository is gone, and Datadog saw nothing but test values, with no second stage in the wild.

Check a machine or image

# Which version is installed?
python -m pip show litellm 2>/dev/null | grep -i '^version'

# Look for the dropped files without starting the suspect interpreter
find / -path '*site-packages/*' \
  \( -name 'cook.pth' -o -name 'cook.py' -o -name 'sitecustomize.py' \) \
  2>/dev/null

# Host markers the malware leaves
ls -la /tmp/.pc_once_72129118c59f5636c0bb /tmp/remote_code_loader.lock 2>/dev/null

The first command starts Python, so run it from a clean interpreter or skip it and read site-packages/litellm-*.dist-info with ls. A sitecustomize.py can belong to your distribution, so read any hit before you delete it. For containers, search your registry and deploy history for the digest above and for any pull of latest or main-stable on October 21.

If you find it

Datadog lists five steps:

  1. Delete cook.pth, cook.py and sitecustomize.py from site-packages.
  2. Remove the 1.107.0 install and anything built from it. Rebuilding the image beats patching it.
  3. Pin litellm==1.106.2.
  4. From a different, clean machine, rotate every credential the host could read.
  5. Audit cloud, Git and provider logs for use of those credentials since 11:39 UTC on October 21.

Step 4 is the expensive one. On a CI runner, "every credential" means the deploy keys, registry tokens and cloud roles that runner holds, along with your model provider keys.

This week

  1. Search every lockfile, image and CI log for litellm 1.107.0 and for the image digest above.
  2. Pin LiteLLM by version and hash, and pull its images by digest, not by latest or main-stable.
  3. If you maintain a package, restrict your release workflow to your own repository and protected branches, and treat gh OAuth tokens on engineers' laptops as production secrets.
  4. If you verify provenance for dependencies, check the source repository field, not the signer alone.

Volodymyr Chornous