LiteLLM is the proxy a lot of companies put in front of their model providers, so one endpoint fronts several of them. On 21 October its 1.107.0 release went out with a credential stealer and a remote code loader inside it, and PyPI quarantined the version the same day.
The malware is not the interesting part. The interesting part is that the compromised build shipped with signed provenance attached, and the provenance said plainly where it had come from.
One line in a build file
PyPI holds eight files for 1.107.0: seven wheels and a source archive. All seven wheels carry the payload. The archive does not, but its pyproject.toml carries a line that is absent from the tagged source.
Under the maturin build section, somebody added a directive reading data = "cook". That tells the build tool to copy a directory named cook into every wheel it produces. The directory existed only in the build environment.
The wheels were built around 11:21 UTC and uploaded to PyPI eighteen minutes later. Their internal timestamps match the legitimate files, which means nobody repacked the archives by hand afterwards. The build ran correctly. Its input was wrong.
Three files, and the first one is twelve bytes
Each wheel installed three extra files into the site-packages root. cook.pth is twelve bytes and contains the words import cook. cook.py is the stealer at 22,252 bytes, and it calls itself privcollect. sitecustomize.py is the loader at 25,509 bytes.
The twelve-byte one is the whole trick. Python reads .pth files at interpreter startup, before any of your own code, so the payload executes on every python invocation in that environment. That includes pip, which is why the report's remediation section opens with the consequence rather than the instruction: "Don't start with pip install."
What it took
Summarising the target list understates it: SSH, GPG and TLS private keys, decrypted Chromium passwords and Firefox credential stores, AWS, Azure and Google credentials including live ones pulled from instance metadata and validated against the provider, Kubernetes service account tokens, Vault, Jenkins and Terraform secrets, npm and PyPI configs, shell histories, and the environment variables of every process on the host it could read.
Then the part written for this package specifically. If a database URL was configured, the stealer ran the proxy's own database client and dumped two tables: the credentials table and the model table. Those hold every upstream provider key the proxy has been given.
It also went after the credentials of AI coding tools by name, among them Claude, Codex, Gemini, Cursor, Windsurf, Copilot and Hugging Face tokens. A gateway built to hold model keys was turned into a collector of them.
Exfiltration went to a lookalike domain registered on 13 October, eight days before the release. The loader reads a command server address from a GitHub account created the same day, and that file has been empty since 20 October, which makes the second stage dormant rather than removed. One commit re-arms it.
The provenance named the wrong repository
Every v1.107.0 container image carries SLSA build provenance, and anyone can read it. On all six, built between 11:00 and 11:06 UTC, it names the source as Berri-AI/litellm — one hyphen from the project's own BerriAI/litellm. The floating tags most deployments pull, latest and main-latest and main-stable, pointed at those images on both registries.
The supply chain controls did their job. They recorded, signed and published the fact that this build came from somewhere else. Nobody was reading them, because reading them is not a step in anybody's install.
The report makes the same point from the other direction: LiteLLM "has never published attestations, so users currently can't tell a tampered build from a legitimate one". We wrote something close to this when OVHcloud's CLI releases were rewritten under their existing tags. The artefact is the thing to verify, and almost nobody verifies it.
What to do
- Do not run pip in an affected environment. Delete the three files from site-packages from a shell first, then rebuild.
- Rotate everything the host could reach: the instance role, every provider key in the proxy database, the master and virtual keys, the database password.
- Pin 1.106.2 by digest rather than by tag, and rebuild images instead of pulling again.
- Look for a detached python process holding the lock file at /tmp/remote_code_loader.lock.
- The marker at /tmp/.pc_once_72129118c59f5636c0bb confirms a successful exfiltration. Its absence confirms nothing, because containers and temp cleanup remove it.
What is not established
- How the build environment or the publishing credential was compromised. The reporters could not find a workflow that publishes to PyPI and say the project is better placed to answer.
- How many installations pulled it. The version was live for hours and the Docker tags for longer.
- Who did it.
- Whether the dormant command server was armed at any point in the window.
- Which research team found it first. More than one has published and the accounts do not settle the order.
- What the maintainers intend to do. The issue is open, the 1.106.2 release notes do not mention the incident, and no advisory has been published.