LiteLLM 1.107.0: Clean Source, Poisoned Installer
On October 21 at noon, LiteLLM's 1.107.0 version was uploaded to PyPI—and pulled by the project within two hours. This wheel was stolen goods: it contained a credential stealer and a remote-control backdoor. The Docker-side latest and main-stable images survived a few hours longer before being removed. Note the "again" implied by this story: in the TeamPCP supply chain attack this past March, LiteLLM was already hit once—the malicious file didn't even change its name. It's still called cook.pth.
The source was clean; the build was not
The most remarkable part of this incident: the source repository was clean. Post-incident inventory showed the three malicious files were not on GitHub and not in the source archive—they were introduced during the build stage.
LiteLLM builds its Rust bridging layer with maturin, and the build config contained an extra line: data = "cook", packing a directory named cook from the build machine into the wheel's data section. The build tree was tampered with between checkout and build. The malicious files carried timestamps and metadata identical to legitimate files—this was not a manual repackaging after the fact.
When pip installs the wheel, the data section's contents are dumped into the root of site-packages—a carefully chosen location, because Python automatically executes special files there at every interpreter startup.
Three files, three jobs
cook.pth— 12 bytes total, containing onlyimport cook..pthfiles are Python startup hooks: it runs when any Python process on the machine starts. You don't need to import LiteLLM, or even run it.cook.py— the stealer itself: one heavily obfuscated line of code that unpacks and executes a 61KB payload.sitecustomize.py— the remote control: also auto-executed at interpreter startup, it launches a background daemon that polls the attacker's infrastructure once per hour, executing whatever Python code the attacker sends.
What was stolen
The loot list reads like "the company's entire digital asset inventory": browser-saved passwords and browsing history, private keys in ~/.ssh, shell history, AWS/Azure/GCP cloud credentials (including live queries to cloud instance metadata endpoints), Docker configs and Swarm secrets, Kubernetes service account tokens, CI/CD secrets, environment variables, and credential files for various AI tools. Plus LiteLLM's own proxy config table—which stores the API keys for every upstream model provider the company uses: OpenAI's, Anthropic's, every cloud's, none spared. The loot was zipped and POSTed to litellm.dev/healthcheck—a lookalike domain sharing the project's name, disguising exfiltration as health-check reporting.
Why target LiteLLM twice? Because it's the keyring that holds every AI provider key for an entire company.
Where accountability breaks down
The release was built from an impersonated repository—the real one is BerriAI/litellm, the attacker's was Berri-AI/litellm, differing by one hyphen (now 404). The telling detail: every Docker image's SLSA provenance metadata faithfully recorded this malicious origin and its corresponding commit. The provenance system honestly documented the loot's birthplace, but no link in the chain required "the source must be the official repository," so nobody stopped it.
Officials have still not located the GitHub Actions workflow used for the release, and remain unsure how the build environment or release credentials were lost. The preliminary suspicion is a freshly patched vulnerability on an internal deployment.
Remediation has a counterintuitive trap
Don't start with pip install. pip is itself a Python process—on an infected machine, launching it runs cook.pth all over again. The correct order:
1. Manually delete the three files and their caches from the shell.
2. Kill the daemon holding the /tmp/remote_code_loader.lock lock file.
3. Rotate all credentials before reinstalling.
4. Reinstall 1.106.2, pinned by digest.
The official statement is blunter: if 1.107.0 was ever installed on a machine with network access, assume exfiltration succeeded—the stealer starts working at the very first Python startup.
Timeline
Posted at 11:40, removed shortly after 13:00—a window of less than two hours. Seven months passed between March's campaign and October's recurrence: same package, same technique, same filename. For a proxy project that holds every model provider key on its users' behalf, attacker persistence is never random.