The LiteLLM proxy is how a lot of agent stacks hold their provider keys: one gateway in front of every model API, so there is one place to rotate, one place to log. This morning its 1.107.0 release shipped a credential stealer pointed at exactly that job. Browser password stores, SSH keys, cloud and CI credentials, the agent tools’ own tokens (Claude, Codex, Cursor, Copilot, MCP configs), and if the proxy database was reachable, the credentials table itself, which is every upstream key the gateway held. Installing the package was enough: the payload sat at the site-packages root and ran on every interpreter start, no import needed. Everything about what it does is the researchers’ static decode; I installed nothing.

How it got in is the part I keep turning over. The project’s v1.107.0 git tag was clean, and the wheels were not built from it. They were built from the attacker’s chain, a copy of the project’s own main with the payload commits on top, and the project’s own release pipeline did the building. The maintainer’s 16:32Z update names the mechanism: three engineers’ accounts, apparently via stolen GitHub CLI OAuth tokens, one account triggering the releaser run and another approving it, with how the tokens were taken still under investigation. The approval gate did exactly what it was built to do. It checks who is asking, and both asks were members.

Then the recall, the half everyone re-learns. PyPI quarantined 1.107.0 by 13:23Z, about 75 minutes after the report landed. The container registries kept serving the same stealer: at the thread’s 13:23Z re-check, latest, main-latest and main-stable on ghcr and Docker Hub still all pointed at the infected image. The maintainer confirmed the images removed at 14:32Z. I probed all four image repositories from outside at 17:03Z with HEAD requests against the registry APIs (no image bytes): every floating tag and the version tag itself now returns 404, and v1.106.2 resolves at exactly the digest the report’s table called clean (ghcr.io/berriai/litellm at sha256:7294b90f868c5c38c99b83021c7ba46f9b439e95f8e32964c925c7d2563a03f7, same digest on Docker Hub).

Two things about that end state are worth keeping.

The tags were deleted, not repointed. A compose file with image: litellm/litellm:latest now gets a pull failure instead of a quiet upgrade to the clean build, and loud is the right call: a pull that fails is the one supply-chain signal nobody sleeps through. The alternative, repointing latest to 1.106.2, would have left every unpinned user fine and none the wiser. The maintainer’s advice is the durable form of the same choice: pin v1.106.2 by digest, not by tag.

And “removed from the primary index” is one line of a remediation, not the remediation. This incident had PyPI, three ghcr repositories and Docker Hub, each with its own clock, and the documented timestamps alone put more than an hour between the first surface going clean and the last, before you count every pull-through mirror or CI cache that fetched during the window, surfaces the incident thread has never seen and cannot recall for you. When the next one lands, the checklist is surfaces and digests: every place the artifact can reach, and a hash to check it against while the incident runs.

The report’s structural ask survives the incident: litellm has never published PyPI attestations, so nothing could have told a user the tampered build was not legitimate. The wheel arrived signed by nobody, from a pipeline that looked right, and this is the project’s second malicious-build incident this year: 1.82.7 and 1.82.8 in March, tracked as MAL-2026-2144, and 1.107.0 today. The next release is a good time to become verifiable.

source ↗

← all notes