LiteLLM Was Poisoned Again, Through a Different Door. The Bad Release Was Gone Eight Hours Before the Advisory Named It.
If you run LiteLLM, check today whether version 1.107.0 ever touched your machines. On October 21, someone used stolen developer tokens to run BerriAI's own release workflow and published a backdoored litellm 1.107.0 to PyPI and to Docker Hub and GitHub's container registry. The PyPI release was live from 11:39 to about 13:23 UTC. The Docker tags latest, main-latest and main-stable pointed at the infected build from 11:46 to 14:20 UTC. If you installed or pulled in those windows, assume your credentials are gone, and rotate them from a clean machine. The last clean release is 1.106.2.
This is the second time this year. In March it was versions 1.82.7 and 1.82.8. Same package, a different way in.
Who found it
Credit goes to two teams that did the primary research and published it the same day: StepSecurity and Datadog Security Labs. Everything below about how the attack worked comes from their write-ups. We did not discover any of it.
What the backdoor does
The release drops three files into Python's site-packages folder: cook.pth, cook.py and sitecustomize.py. A .pth file runs every time Python starts, so the malware runs even if your code never imports LiteLLM. Running pip or python again re-triggers it.
cook.py is a credential stealer. Both write-ups list the same kinds of targets: cloud credentials, SSH and Git keys, npm and PyPI tokens, browser passwords and cookies, AI coding tool sessions, and, if the LiteLLM proxy has a database configured, the upstream provider keys it manages. That last one matters for a gateway. LiteLLM exists to hold your API keys for many model providers in one place. The stolen bundle is encrypted and sent to www.litellm[.]dev/healthcheck.
sitecustomize.py is a backdoor that checks a GitHub-hosted file every hour for an address and runs code fetched from it. Datadog says the values it observed were test data, so no second stage was seen.
Uninstalling the package does not remove these files. Delete all three by hand.
How they got in
According to StepSecurity, citing BerriAI's post mortem, it started with an unexpected user account on an internally exposed LiteLLM deployment at BerriAI. The cause is not confirmed; a recently patched vulnerability is suspected. GitHub CLI login tokens for three developers ended up in that deployment's logs. Then, on October 21:
10:08 UTC: branch protection rejected an unauthorized change to the private release workflow, and five seconds later an organization owner's token overrode it and pointed the release at a lookalike repository, Berri-AI/litellm, one hyphen away from the real BerriAI/litellm.
11:11 UTC: a second developer's credential passed the production approval gate.
11:39 to 11:40 UTC: seven wheels reached PyPI, alongside a tampered source archive.
11:46 UTC: the infected Docker images went up.
11:54 UTC: a third developer's credential deleted the staging and production run records.
Datadog's account agrees on the stolen tokens and the lookalike fork, and says it was not yet clear how the credentials were first compromised. Neither team names an actor. Datadog writes that no connection to the March compromise has been established.
The detail worth sitting with: after March, BerriAI moved releases to a private repository, adopted PyPI Trusted Publishing and signed its images, per StepSecurity. Those were the right moves. The attacker did not break them. They used the release workflow itself, with real tokens, so the result was a properly published, properly signed bad release.
The first time, in March
On March 24, versions 1.82.7 and 1.82.8 were pushed to PyPI. The chain ran through Trivy: LiteLLM's CI pipeline installed a poisoned version of the scanner, and that is how TeamPCP got publishing access. We mapped that chain on April 1 in One Actor, Three Supply Chains, and in August we wrote up the one word we got wrong when the "Trivy, not LiteLLM" story ran. By September the LiteLLM proxy had collected three entries in CISA's Known Exploited Vulnerabilities catalog, the third an MCP authentication bypass. If the suspected entry point in October does turn out to be a patched LiteLLM bug, the gateway was used to poison the gateway. That is not confirmed, and we are not claiming it.
So, twice in seven months: once through a build tool in CI, once through developer tokens and the release workflow. Different doors, same house.
What to do now
Search for 1.107.0. Check lockfiles, requirements files, pip and uv caches, CI logs, virtual environments and container images for litellm 1.107.0. Wheel and archive hashes are listed below.
Check what you pulled by tag. If anything pulled litellm/litellm or the berriai images on GHCR (litellm, litellm-database, litellm-non_root) by the latest, main-latest or main-stable tag between 11:46 and 14:20 UTC on October 21, treat it as infected, even though the tags now point at clean builds. A tag is a pointer; what you got is whatever it pointed at when you pulled.
Clean up by hand. Delete cook.pth, cook.py and sitecustomize.py from site-packages, then rebuild the environment or image.
Rotate from a clean machine. Cloud keys, SSH keys, Git and package-registry tokens, AI tool sessions, and every provider key your LiteLLM proxy held. Then check your providers' logs for use of those keys since October 21.
Pin what you can't afford to re-check. Pin the Python package by hash and the image by digest, not by tag. Consider a waiting period before adopting new releases; some installers, such as uv, can ignore anything published after a date you choose. A two-hour window does not survive a one-day wait.
Indicators
From StepSecurity and Datadog. All 17 are now in our indicator index under the source manual-batch-litellm-1107-2026-10, credited to them, at confidence 90 (domains, URL, wheels) or 85 (source archive and payload files).
Network: litellm[.]dev, www.litellm[.]dev, and the exfiltration URL hxxp://www.litellm[.]dev/healthcheck. StepSecurity reports the domain was registered on October 13 at 00:45 UTC, eight days before the release.
Wheel SHA-256: macOS x86_64 7598727f209d717041db37f7dca271d3a7d328a279890e0e1d55375729aeb86d; macOS arm64 0aaf7ba346d41ef2f40390436738b001180d5a1c03aa093f121078a9969d9a1a; manylinux aarch64 668169d35b87bf51324080390b1b1c5ab12bd335cfe5f1a99e3689abd5e02ae6; manylinux x86_64 462d0c9f67ca522d9e0e4acd245e90b36ce0d5353604a66bb7c43a1ec4a6fcf7; musllinux aarch64 c2e32d0ce2548aa662c18bf5001911a79fe5b30cf8357132275add02fdcde054; musllinux x86_64 1bbce8caa94f01c0aaa0b7ddae51553a8a32e1ce53d96d44fe583272aff7ad00; Windows amd64 12d4ccc5c6f8d35cf4f2cf233b7f3e418e863e657d81ff5f5fc9e210005b9484.
Source archive litellm-1.107.0.tar.gz: 2befe2a79fb4fbbd9a543b5b40b530c8cffa8771bab22b8fb22b2e4ae3b1f819.
Payload files, Linux and macOS: cook.pth 7f191632e7320705d154b85204f1ae546b543acbd7e8861229b76f5042b92bdb; cook.py 9dd0a3926f96a37eb86cf1ee2310d9be93139cc4ebef3ef9e2efe9531390be25; sitecustomize.py 34bef04fe88ac9d0aae0475630b1738681d15b94f233ac8aa46eacf5968305fa. Windows: cook.pth 84dc8c82c0a4a4a62552ef826b49fcc86d1a5c4a5d0f2c06665723f93d129d7d; cook.py 03ac2a8b752605d1d7710979c17ff18b03dd978203ba23ba97fd8ce3d6f897ea; sitecustomize.py b6122440d0d9cb1eb1ade9933f49214da8c31288de7710c076c2680dfa528465.
What our tools would and would not have caught
This is the part we would rather not write.
The feed was late. Before today we held none of these indicators. They went in about a day after the two write-ups. That is distribution, not detection, and the credit belongs to the people who found them.
The package check could not have helped during the window. Our pre-install check is version-aware and reads OSV's malicious-package records. The record for LiteLLM, MAL-2026-2144, was created in March for 1.82.7 and 1.82.8. Its source repository shows 1.107.0 was added at 21:46 UTC on October 21, eight hours and 23 minutes after PyPI had already removed the release. So for the whole time the bad release was installable, a check that relies on that record had nothing to block it with. It names 1.107.0 now. That is useful for the next person cleaning up a cache or a lockfile. It is not prevention.
Nothing we run judges a container image. Our package check judges package names and versions. Our MCP server check looks at a server's dependencies. Neither takes an image tag or a digest. If your gateway runs from litellm:main-stable and you pulled it between 11:46 and 14:20 UTC, nothing we sell would have said a word. That is the next lane we need to build. We are not putting a date on it.
What would have worked here are the dull controls that need a person to decide once: pin by hash, pin images by digest, wait a day before taking a new release, and keep developer tokens out of logs. Automation can check everything else, and that is the split we keep coming back to: a person decides the irreversible things, and automation does the rest. A feed that arrives a day later and a check that waits on an advisory are both after the fact when the bad release lasts less than two hours.
Get the indicators
The 17 indicators above are in our free feed. A free key takes about 30 seconds and covers the feed and the package check.
We checked this post before publishing: about 45 checkable claims, 7 corrected or cut, mostly where our first draft said our tools would have caught more than they could. At least 5% of what remains is still wrong, and we don't know which.
If this was useful, or if it wasn't, rate it at the bottom of the page. It is the only direct signal we get.
Every indicator in this post is in the feed. Free.
1.58M+ IOCs, STIX 2.1 / TAXII, 88% novel vs ThreatFox, exploited-CVE leads ahead of CISA. No credit card — a free API key in 30 seconds, and you can audit every claim above against the live endpoints.
Was this useful? Thirty seconds, no cookies, no tracking, no third parties, your address hashed and never stored. If the box below does not load, the same question lives at https://analytics.dugganusa.com/nps.html?post=litellm-poisoned-again-through-a-different-door



Comments