LiteLLM 1.107.0 shipped the payload from the release pipeline
On 21 October 2026 the LiteLLM release pipeline published version 1.107.0 with a credential stealer inside it — to PyPI, Docker Hub and GHCR. The interesting part is not the malware. It is that every trust question answered yes.
Version 1.107.0 of LiteLLM, a Python gateway that fronts many model providers behind one API, was published on 21 October 2026 to PyPI, Docker Hub and GHCR. It carried a .pth site-initialisation file named cook.pth, a second persistence payload named sitecustomize.py, and a credential stealer that never needed LiteLLM to be imported to run.
LiteLLM's own investigation traced the publish to three engineers' GitHub accounts, accessed through stolen GitHub CLI OAuth tokens. The release workflow was invoked against a lookalike repository — Berri-AI/litellm against the project's real BerriAI/litellm — and the latest and main-* tags were repointed. PyPI quarantined the release in about two hours. It was the second supply-chain compromise of the package this year, after the TeamPCP attacks in March used the same cook.pth behaviour.
The mechanism
The publish path is the part worth studying. Everything the registry checks about a release — is this the maintainer, is this the project, is this repository allowed to publish — was satisfied, because the actor was holding the maintainer's own tooling credentials. GitHub CLI OAuth tokens from three separate engineers were enough to invoke the release workflow. What the workflow pointed at was not the project's real source. The difference between the two repository names is a single character.
The other half of the mechanism exploits when Python code gets to run. A .pth file in a site directory is executed at interpreter startup, before any application import. The file in the release contained one line:
import cookThat line launches the collector, so a machine does not have to run LiteLLM to be affected — any Python process in that environment is enough, including the tooling that installed the package. The collection scope is deliberately wide: cloud tokens, SSH and other private keys, browser data, environment variables, and credential JSON files belonging to AI development tools. It also targeted LiteLLM deployments specifically, to read the proxy's saved provider credentials and model configuration — the one place an AI gateway keeps every upstream key at once. Exfiltration was wrapped with AES-256-CBC and RSA-4096 and posted to litellm[.]dev/healthcheck.
Persistence came from sitecustomize.py, which forks a background daemon that polls a GitHub dead drop once an hour and runs whatever Python it is served. A supporting mechanism read a domain.txt file from the letters9813 GitHub repository and, if it held a domain, ran the stealer again and fetched and executed <domain>/codepy. Removing the affected package does not remove any of this. The payload reads a text file over the network and executes the response; that is a live channel, not a loose end.
Then the credentials were used. Stolen AWS instance-role credentials were validated with sts:GetCallerIdentity roughly four and a half hours after publication, and the same keys were then pointed at bedrock:ListFoundationModels. Validating a key proves it works; enumerating model access proves the intent is to spend it. That is the LLMjacking pattern — stolen cloud identity converted into someone else's inference bill — and it is why a compromised package release is a different incident from a compromised library.
What we would have seen
Both halves of this campaign produce signals, and the join between them is where the useful detection sits. Hanu carries 609 behavioural detections across 166 signal types, with 75 correlation rules (34 Tier 1, 41 Tier 2) and no language model in the detection or scoring path.
SUSPICIOUS_PACKAGE_RELEASEfires on the release itself, from registry metadata: a publish attributed to a lookalike repository or actor, a source repository that no longer matches, an install-time hook that is new in this version, a changed publisher, a missing attestation. One rule covers the shape regardless of which package it is. What it does not do is read the artefact —.pthandsitecustomizepayloads and out-of-band tag moves are not detected today, and PyPI has no install hook to catch instead.MALICIOUS_PACKAGEmatches known-bad artefact hashes and quarantines on sight — the path that catches machines which installed before the registry acted.CI-WF-LOOKALIKE-SOURCEcatches the release path pointing atBerri-AI/litellminstead ofBerriAI/litellm, alongside the wider set of CI workflow signatures in Hanu's coverage for GitHub Actions, GitLab, Jenkins and CircleCI.SECRET_LEAKEDrecords what left the environment — cloud keys, registry publish tokens, AI-provider keys — so rotation has a list rather than a guess.LLM_JACKING_ACTIVITYcatches the monetisation step from CloudTrail: a key validating, then Bedrock or SageMaker model enumeration and invocation inside twelve hours, plus the per-key behavioural signals — unfamiliar geography, a young credential's first Bedrock call, a first call to an expensive model. AWS only; a key spent against a non-AWS provider is outside the audit trail this reads.RULE-T1-017treats the release as the incident, andRULE-T2-015joins it to the credential use that follows. The second rule is the one that answers whether the poisoned dependency actually cost anything.
What to do
- 01
Build the list before you rotate anything
Resolve which machines and pipelines actually pulled 1.107.0, in both wheel and container form, from your SBOM and CI inventory. The published guidance to rotate everything in two hours is correct but unusable without scope; a scoped list from inventory is what makes it finite.
- 02
Rotate from a clean environment
Revoke and replace the exposed credentials from a machine that was never in the affected interpreter environment, otherwise the replacement is collected by the same payload. Pin 1.106.2 by digest rather than trusting a version string.
- 03
Hunt the persistence, not just the package
Look for
sitecustomize.pyin the affected environments and for hourly outbound calls to a GitHub dead drop. Uninstalling the release leaves the daemon behind, and the daemon is the part with a remote execution channel. - 04
Check what the credentials did
sts:GetCallerIdentityandbedrock:ListFoundationModelsare the two calls named in the reports. Look for them in CloudTrail around the incident window, and treat any model enumeration from keys that no longer belong to their original owner as active abuse rather than exposure. - 05
Close the release path
Restrict which identities can push workflow files and move tags, require provenance that names an immutable repository rather than a name, and treat a shared OAuth token used by three engineers as three separate rotation events.
The detail worth carrying forward is the shape of the artefact, not the package. A .pth file that runs before your code does, a release that satisfies every identity check because it used real identities, and a four-hour gap between the payload landing and the stolen keys being spent. That shape will appear again in another registry, another ecosystem, another package — which is why Hanu detects the release anomaly rather than the name LiteLLM.
Which versions and artefacts were affected?
If we never import LiteLLM, are we still exposed?
.pth file, and Python executes those at interpreter startup, before any application import happens. Any process in that environment can trigger it — including the tooling that installed the package. The stealer also targeted LiteLLM deployments directly to read the proxy's saved provider credentials and model configuration, so a gateway that holds every upstream key is the highest-value host in the fleet.How is this different from the March compromise of the same package?
cook.pth behaviour, and LiteLLM was hit then too. What changed is the blast radius of the target: an AI gateway's environment holds cloud credentials and AI-provider keys together with the gateway's own saved credentials, so one poisoned release reached both the cloud account and every model provider the company had contracted.The registry removed the release in about two hours. Is that not fast enough?
Run it in your own account
One Docker Compose stack, your datastores, read-only credentials into your clouds. The Community edition is free to run in production.