← all posts
// security · security

LiteLLM 1.107.0 was on PyPI for under two hours, and the gateway held every key

On 21 October 2026, between 11:39 and 11:40 UTC, someone uploaded seven wheels for litellm==1.107.0 to PyPI. All seven carried a credential stealer and a remote-code loader. At 11:46 UTC the same payload went out in the official Docker images, and latest, main-latest and main-stable were moved to point at them. It is LiteLLM's second compromise this year, after 1.82.7/1.82.8 in March.

LiteLLM is usually deployed as an LLM gateway, so it is often the single process that holds every provider API key a company has. The stealer knew that. One of its modules reads DATABASE_URL and queries the LiteLLM proxy database for stored upstream credentials. If you run LiteLLM as a proxy and pulled 1.107.0, assume every provider key in that database has been stolen.

What actually shipped

Sources: BerriAI's post-mortem (marked provisional), the reporter's GitHub issue, StepSecurity and Datadog.

Artifacts: all seven 1.107.0 wheels (macOS, Linux glibc/musl, Windows) and the v1.107.0 images on GHCR (litellm, litellm-database, litellm-non_root) and Docker Hub. The sdist has a tampered pyproject.toml (data = "cook" under [tool.maturin]) but, per StepSecurity, no payload. The git tag v1.107.0 is clean.

Mechanism: three files are dropped into site-packages: cook.pth, cook.py (the stealer) and sitecustomize.py (the loader). A .pth file runs at every interpreter start, so you never have to import litellm for it to fire. Deleting the package directory does not remove these files.

The stealer has 16 modules covering browser data, SSH/GPG keys, cloud and CI secrets, instance metadata, every process environment, 20+ AI coding tools' credentials and the LiteLLM DB. It encrypts the bundle (AES-256 + RSA-4096) and POSTs it over plain HTTP to http://www.litellm.dev/healthcheck. A marker file is written only after an HTTP 200, so the stealer runs again on every Python start until exfiltration succeeds.

The loader, on Linux/macOS, forks a daemon that reads a hostname from a GitHub dead-drop file once an hour, then fetches and exec()s http://{host}/codepy, where {host} is that hostname. The dead drop was empty at release, so StepSecurity and Datadog both say no second stage was observed.

Root cause, per BerriAI: an internet-exposed internal LiteLLM deployment had logged GitHub CLI OAuth tokens belonging to three developers, one of them an org owner. With them, the attacker overrode branch protection, pointed the release workflow at the lookalike repo Berri-AI/litellm (one hyphen off), passed the production approval gate and deleted the run records. The pipeline published with valid provenance. BerriAI also says it has seen Claude Code embed a user's gh token in a response body, which then ended up in logs. How the attacker got into the internal deployment is still unconfirmed.

The exposure window

ChannelLive (UTC, 21 Oct)Duration
PyPI wheels11:39:51 → ~13:20 to 13:23 (quarantined/removed)~1h41m to 1h43m
GHCR images11:46:56 → 13:51~2h04m
Docker Hub tags11:46:56 → 14:20~2h33m

Arithmetic: 13:23 − 11:39:51 ≈ 1h43m (Datadog says quarantine at 13:20); 14:20 − 11:46:56 ≈ 2h33m. Short, but floating tags and unpinned CI catch a lot in two hours, and any mirror that cached the wheel keeps serving it after PyPI removes it.

If you run LiteLLM as a proxy and pulled 1.107.0, assume every provider key in that database has been stolen.

Incident response checklist

Do not start Python in a suspect environment to check it. pip list, pip uninstall and python -c all start an interpreter, and that runs cook.pth. Check from the filesystem:

# any interpreter, any venv, any image layer you can mount
find / -path '*site-packages*' -name 'cook.pth' 2>/dev/null
find / -path '*site-packages*' -name 'cook.py' 2>/dev/null
find / -path '*site-packages*' -name 'litellm-1.107.0.dist-info' 2>/dev/null
ls -la /tmp/.pc_once_72129118c59f5636c0bb /tmp/remote_code_loader.lock 2>/dev/null
grep -rn -A1 -F 'name = "litellm"' --include='*.lock' . | grep -F '1.107.0'
grep -rn -F 'litellm==1.107.0' --include='requirements*.txt' .

The host IOCs come from StepSecurity and Datadog: the three dropped files, marker /tmp/.pc_once_72129118c59f5636c0bb, lock /tmp/remote_code_loader.lock, env var PRIVCOLLECT_ACTIVE=1 and temp dirs .pc_sq_ / .pc_e_. The network IOCs are litellm.dev / www.litellm.dev, header X-Token: XWiA8gZNa7NcX2XRoAEb, any request to /codepy, and the dead drop raw.githubusercontent.com/letters9813/pythonclient-test/main/domain.txt. The malicious GHCR litellm:v1.107.0 image index is sha256:1bbcb1443bc962e45f5aad53fe3d2c30600e0379715fb6c664cdd3ac126c339e. Per-platform wheel hashes are in the StepSecurity and Datadog posts. Copy them from there.

If you find a hit, work through this in order:

StepAction
Stop the loaderKill the process holding /tmp/remote_code_loader.lock (fuser/lsof) before touching pip.
Remove the payloadDelete cook.pth, cook.py, sitecustomize.py and their __pycache__ entries. Better still, throw away the venv or image and rebuild it. BerriAI says to pin 1.106.2, and to pin images by digest.
Assume theftAssume exfiltration succeeded if the host had outbound access. BerriAI is explicit that a missing marker file "proves nothing".
RotateIn this order on gateway hosts: every provider key in the proxy DB and in litellm_params, the master key, salt key, virtual keys, and the DATABASE_URL password. After that: instance/task IAM roles, CI secrets, registry tokens (GitHub, npm, PyPI), Kubernetes/Vault tokens, SSH keys, and AI tool tokens.
Hunt in CloudTrailLook for GetCallerIdentity from instance roles around install time, and for those credentials being used from outside your network.
Check for stage twoAny /codepy request in your logs means attacker code ran. That is a full incident, not a rotation.
Clean mirrorsPurge 1.107.0 from internal mirrors and v1.107.0 from internal registries. Audit any litellm>= constraints and floating tags.

The structural fixes

My read: two of the usual controls would not have saved you this time, and two would have.

Two would not have helped on their own. Provenance attestations were valid, because the real pipeline built the malicious code. Attestations say which workflow built an artifact, not whether you trust the source repo recorded in them. Check the source repository identity (BerriAI/litellm), not just that a signature exists. Hash-pinned lockfiles (uv.lock, pip install --require-hashes) faithfully pin whatever you resolved. If you resolved during the window, you pinned the backdoor. They are still essential, because they stop silent drift on every later rebuild.

Two would have helped. The first is a quarantine delay on new versions. Use a private mirror that only serves releases older than N days, or uv's exclude-newer set to a timestamp you move forward deliberately. PyPI exposure lasted under two hours, so a 72-hour delay gives you zero exposure. Security fixes become an explicit, reviewed exception. The second is egress allowlists on the gateway. A LiteLLM proxy needs outbound access to your model providers and your database, and nothing else. Exfiltration went over plain HTTP to a domain registered eight days earlier (13 October), and an allowlist would have dropped it. Treat the gateway like a payment service.

Two more lessons generalize: developer CLI tokens are production credentials, and logging full LLM response bodies logs whatever secrets the model echoes. Redact before you persist.

What I couldn't verify

BerriAI's claim that Claude Code embeds gh tokens in response bodies comes from BerriAI alone; I found no Anthropic statement on it. The initial entry point is unconfirmed. BerriAI mentions "a recently patched vulnerability" and an embargoed enterprise fix due for disclosure on 27 October, and no CVE has been published. I don't have a download or pull count for the window. The 1.107.0.dev1 to dev3 pre-releases were not inspected, according to StepSecurity. PyPI removal time differs between sources (13:20 vs "by 13:23").

Related: the npm ChainDrop worm also beat signatures, and API keys are the new loot. I'll revisit when BerriAI publishes its final root-cause report after 27 October.

Sources

Primary: LiteLLM: Security incident, Oct 21 2026 (official post-mortem); GitHub issue BerriAI/litellm#47826. Analysis: StepSecurity: LiteLLM 1.107.0 compromised, backdoored wheels with credential stealer; Datadog Security Labs: LiteLLM compromised on PyPI and Docker.

#security#supply-chain#llm-gateway