The malicious LiteLLM wheel was built by LiteLLM's own release workflow. Not a typosquat, not a hijacked maintainer account publishing by hand — the project's real, signed, logged pipeline, pointed at a fork called Berri-AI/litellm instead of BerriAI/litellm. That single hyphen is the whole lesson, because build provenance attests the workflow that ran and says nothing about the repository it read. Every team that spent 2026 adopting attestation as its supply-chain answer would have had that answer return "verified".
At a glance
All times UTC on 21 October 2026, per Datadog Security Labs' published timeline. The window from publication to PyPI quarantine was one hour and forty-one minutes; Docker images came down thirty-three minutes after that.
| Time | Event | What it means for you |
|---|---|---|
| 11:39 | Malicious litellm 1.107.0 published to PyPI | Any unpinned install from here on took it |
| ~11:46 | Socket flags the malicious wheels | Detection worked; it does not uninstall anything |
| 13:20 | PyPI quarantines 1.107.0 | New installs stop. Existing ones do not |
| 13:53 | Affected container images removed | Tags v1.107.0, latest, main-latest, main-stable |
LiteLLM is the open-source gateway that sits in front of model providers for a large share of the agent ecosystem — Datadog Security Labs puts it at roughly 23 million weekly downloads. The compromised release carried an infostealer plus a dormant second-stage loader. The last known-good version is 1.106.2. This is the project's second compromise of 2026; versions 1.82.7 and 1.82.8 were affected earlier in the year, and no link between the two has been established.
What was taken was the release path, not the repository
Per Datadog's account, the attackers held GitHub CLI OAuth tokens belonging to three engineers' accounts. They did not need to push a commit to the real repository, open a pull request, or get a review approved. They needed to start a workflow — and a developer CLI token is sufficient authority to do that.
The workflow then did its job correctly. It checked out the source it was told to check out, built it, and published the result to PyPI, GitHub Container Registry and Docker Hub under the project's own identity. Nothing malfunctioned. The one input nobody validated was which repository the word "source" resolved to, and Berri-AI versus BerriAI is a difference designed to survive a human glance at a log line.
This is the specific failure that makes the incident worth your attention rather than just your rotation runbook. A hijacked-maintainer-account attack produces an artefact with the wrong signer. A typosquat produces a package with the wrong name. This produced an artefact with the right signer, the right name, the right version sequence, and a build log showing it came from the official workflow.
The credential class that mattered here was a developer's own CLI token — the thing sitting in ~/.config/gh/hosts.yml on three laptops. If your release workflow can be dispatched by anyone holding a personal token, your release authority is the weakest laptop on the team, whatever your branch protection says.
Which control would have caught it, and which would have blessed it
Sort your supply-chain controls by the question they answer and this incident separates them cleanly. Floating references — latest, main-stable, an unpinned pip install litellm — answer "what is newest?", and for one hour and forty-one minutes the honest answer was the malware. Provenance attestation answers "was this built by the project's pipeline?", and here the truthful answer was yes; a verifier checking it would have passed the artefact and logged a success. A digest pin answers "are these the exact bytes I reviewed?", and the answer was no, so nothing moved.
That ordering is uncomfortable because provenance is the control the industry has been building toward, and it is genuinely valuable against a large class of attacks. It just is not a content check. An attestation is a statement about a process, and a process can be aimed somewhere new without ceasing to be the same process. The site's note on pinning and verification makes the general version of this point: a name that resolves through somebody else's namespace is a preference, and a digest that is the content is a lock. Here the namespace being resolved was a GitHub organisation name, one character wide.
The corollary is the part worth arguing with your platform team about. Adding provenance verification to a pipeline that pulls floating tags does not make the pipeline safer against this attack; it makes the pipeline's logs more confident about an artefact it was going to install anyway. The safety came from refusing unreviewed bytes, and only the digest does that.
The blast radius was the interpreter, not the gateway
The payload dropped three files into site-packages: cook.pth, cook.py and sitecustomize.py. Python executes .pth files and sitecustomize.py at interpreter start, which means the stealer ran whenever anything in that environment ran Python. Not when an agent called the gateway. Not when litellm was imported. Whenever python or pip started.
Two consequences follow, and both of them break the intuitions people use when triaging a dependency incident.
Uninstalling the package does not remove the malware. The three files live beside the package, not inside it, so pip uninstall litellm leaves them in place — and the pip that runs the uninstall triggers them. Datadog's remediation starts with deleting the three files by hand for exactly this reason. A team that reasoned "we removed the bad version, we're clean" is still running it.
The reachable set is the environment's whole credential surface. Datadog's analysis lists cloud credentials including instance metadata for AWS, GCP and Azure; Kubernetes, Docker, Vault and Terraform secrets; SSH, GPG and TLS keys; Git, npm and PyPI tokens; .env files and shell history; browser-saved passwords and cookies; and provider API keys and sessions for Claude, Codex, Gemini, Cursor and Copilot. Exfiltration was encrypted with AES-256 and RSA-4096 and sent to www.litellm[.]dev/healthcheck — a lookalike domain and a path chosen to read as noise in an egress log.
That last group deserves its own sentence. If the LiteLLM proxy had a database configured, the upstream provider credentials stored in its tables were in scope. This is the structural hazard of the pattern, not a bug in this product: the advice to move provider keys out of the model's reach and into a broker at the egress boundary is correct, and it concentrates every provider credential you own into one process with one upgrade path. We wrote that up separately as when the credential broker is compromised, because the design response is a different piece of work from the incident response.
Two hours is longer than an install and shorter than a detection
One hour and forty-one minutes of PyPI availability sounds survivable until you count what fires automatically inside a window that size. Every CI job that resolves dependencies without a lockfile. Every container build that pulls latest. Every autoscaling event that starts a fresh node and installs from scratch. Every developer who ran an upgrade that morning. None of these involve a human deciding to adopt a new version; they are the machinery of a normal Wednesday.
Detection, meanwhile, worked impressively well and did not help the people who had already installed. Socket flagged the wheels roughly seven minutes after publication. That is close to the floor of what automated scanning can achieve, and it was still after the first installs. The asymmetry is the point: scanning shortens the window for the next victim, and does nothing for anyone inside it. Which is the fail-closed question wearing supply-chain clothes — a scanner that has not yet flagged a package is indistinguishable from a scanner that has cleared it.
There is also a rollback trap worth naming. A team pinning main-stable has no record of which immutable artefact it was running last week, so "roll back to the known-good version" requires first reconstructing what the known-good version was. The teams who recovered fastest here were the ones whose lockfile already named 1.106.2 with a hash, which is to say the ones who were never exposed.
What to actually do
In rough order of how much protection each buys per hour of work:
- Pin by digest, for images and for Python. Immutable image digests rather than tags, and hashes in your lockfile (
pip install --require-hashes, or a resolver that writes them). This is the only control in this article that stopped the attack rather than describing it. - Lock release workflows to an allowlist of repository and branch. Datadog's recommendation, and the fix closest to the root cause: a workflow that can only build from one source cannot be aimed at a fork. Audit which identities can dispatch yours.
- Treat developer CLI tokens as release credentials. They were the initial access here. Short lifetimes, no long-lived personal tokens with workflow scope, and a hardware-backed second factor on the accounts that can trigger a publish.
- Write the "we installed a compromised package" runbook before you need it, and make its first step find files the package manager does not own. The
.pthpersistence trick is not novel and it defeats the obvious remediation. - Inventory what your gateway host can reach, not what your gateway is configured to do. The question is which of the four groups above live in that environment, and whether the answer needs to.
- Rotate from a clean machine. Rotating credentials from the compromised host hands the new ones to the stealer. Obvious written down, routinely skipped under pressure.
And keep the scope of the argument honest. LiteLLM responded quickly, published what happened, and the detection ecosystem around it performed close to its theoretical best. Nothing here is a reason to prefer a closed gateway, where the same substitution would simply have been invisible. The reason to care is narrower and more durable: the control that was supposed to answer "can I trust this build?" answered yes, truthfully, about malware — and it will do so again, because that is what it was designed to assert.
FAQ
Which LiteLLM versions were affected?
Only litellm 1.107.0 on PyPI, plus the container images tagged v1.107.0, latest, main-latest and main-stable on GitHub Container Registry and Docker Hub. The last known-good release is 1.106.2.
Is uninstalling LiteLLM enough to clean a host?
No. The payload wrote cook.pth, cook.py and sitecustomize.py into site-packages alongside the package rather than inside it, so uninstalling leaves them — and Python runs them at every interpreter start, including the one that performs the uninstall. Delete the three files explicitly, then rebuild the environment.
Would a provenance attestation have caught this?
No, and that is the central point. The artefact was built by the project's genuine release workflow under the project's own identity; only the source repository was substituted. An attestation of build provenance would have verified successfully.
What credentials should be rotated?
Everything reachable from the affected environment, rotated from a machine that was never exposed: cloud and orchestration credentials, SSH, GPG and TLS keys, Git, npm and PyPI tokens, model provider API keys, and any upstream provider credentials stored in the LiteLLM proxy's own database. Then audit logs for use of those credentials during and after the window.
Was a second-stage payload ever delivered?
Not as far as has been reported. sitecustomize.py polled a file in a GitHub repository hourly for a domain to download and execute, but the file held only test values and the repository has since been removed. The loader was present and functional; the payload was not observed in the wild.
Does this mean self-hosting a gateway is the wrong call?
No. It means a self-hosted gateway is a credential concentrator whose upgrade path you own, so digest pinning and host-level credential hygiene are load-bearing rather than optional. A hosted gateway moves the same risk to a vendor whose release pipeline you cannot pin at all.
Further reading
On this wiki:
- Pinning and verification — why a name is a preference and a digest is a lock.
- When the credential broker is compromised — the design response to a concentrated key store.
- Agent supply-chain security — the broader class this belongs to.
- Secrets management for agents — the broker pattern this incident stress-tests.
- AI gateways — what the component does and why it holds what it holds.
- Unpinned vendor defaults — the adjacent habit of inheriting choices without a version number.
- Detecting agent compromise — what the egress log should have shown.
Sources:
- Datadog Security Labs — LiteLLM supply chain compromise: what you need to know (timeline, IOCs, remediation)
- The Register — LiteLLM supply chain attack hoovers up passwords and secrets (21 October 2026)
- Help Net Security — LiteLLM hit by second supply chain attack this year (22 October 2026)
- BleepingComputer — LiteLLM release pipeline hacked to distribute info-stealer malware