LiteLLM 1.107.0 compromise: build path and payload flow

LiteLLM 1.107.0 Supply-Chain Attack

October 22, 2026 · 13 min read · By API User

LiteLLM 1.107.0 was uploaded to PyPI on 21 October 2026 between 11:39 and 11:40 UTC, and every wheel carried a credential stealer and remote code execution backdoor. The package was not built from BerriAI/litellm. The SLSA provenance attached to the release’s container images names the source as https://github.com/Berri-AI/litellm, a hyphenated lookalike of the real organization, at commit 20535d8f. That single character is the incident in miniature: the payloads were a symptom, and a release pipeline pointed at the wrong repository was the cause.

Key Takeaways:

  • All seven 1.107.0 wheels installed cook.pth, cook.py, and sitecustomize.py into the root of site-packages, so the stealer ran on every Python start, including pip itself.
  • PyPI removed the release by 13:23 UTC, but Docker Hub tags kept serving the infected image until 14:20 UTC — 57 minutes longer.
  • BerriAI says the build came from an attacker-controlled lookalike repo reached through stolen GitHub CLI OAuth tokens, one of which belonged to an organization owner.
  • 1.106.2 and the release candidates are clean; pin the replacement by digest, and assume any host that ran 1.107.0 was compromised.

What Shipped in 1.107.0

Each malicious wheel dropped three files under litellm-1.107.0.data/purelib/, which pip installs directly into the site-packages root. The tracking issue in BerriAI/litellm lists them with sizes: cook.pth at 12 bytes, cook.py at 22,252 bytes, and sitecustomize.py at 25,509 bytes.

Why Pinning 1.106.2 Is Only Half the Fix

cook.pth contains one line, import cook. CPython’s site module executes any .pth line beginning with import, so the stealer in cook.py fires on every interpreter start in that environment. PyPI installs, pytest runs, Jupyter kernels, and IDE language servers all count. Nothing needs to import litellm. The decoded cook.py is a zlib and base64 wrapped payload that names itself privcollect and runs across 16 collection modules.

Its targets are chosen for the places LiteLLM runs. SafeDep’s static analysis of the wheels describes browser password and cookie extraction, ~/.ssh and GPG keys, AWS, Azure, gcloud, kube, docker and gh credential files, CI secrets from GitLab Runner, Jenkins and Vault, and the configs of AI coding tools including Claude Code, Cursor, Copilot, Gemini, Codex and Hugging Face. Two modules matter more than the rest: db_litellm reads DATABASE_URL and dumps the LiteLLM_CredentialsTable and LiteLLM_ProxyModelTable tables, which hold every upstream provider key the proxy stores, and cloud_metadata pulls instance role credentials from the metadata service and validates them against STS GetCallerIdentity before packaging.

Exfiltration goes to http://www.litellm.dev/healthcheck. That domain was registered on 13 October 2026 and is not the project’s domain, which is litellm.ai. The bundle is encrypted with AES-256-CBC under a key wrapped in RSA-4096-OAEP, so packet capture does not reveal what left the host.

The second file, sitecustomize.py, is imported automatically after .pth processing. It forks a detached daemon, takes a lock at /tmp/remote_code_loader.lock, and then every hour reads a domain from a GitHub file under the account letters9813, re-runs the stealer, and fetches /codepy from that domain for execution. The source distribution was affected too, though differently: litellm-1.107.0.tar.gz carried no payload files but shipped a tampered pyproject.toml with an injected data = "cook" line in the maturin build section. That one line is what told the build tool to bundle the cook/ directory into every wheel.

The Lookalike Repository and the Stolen OAuth Token

BerriAI published an incident report on 21 October that describes a chain in which several independent controls failed. On the Monday before the release, the company says it temporarily widened release permissions so two internal engineers could publish daily bugfix backports. The attacker held three sets of developer GitHub CLI credentials, and one could run GitHub workflows.

The 10:08 UTC log entries in that report are the sharpest part. At 10:08:29, branch protection rejected an unauthorized edit to the private release workflow because it had not passed the required pull request and status checks. Five seconds later, a credential tied to a privileged developer account overrode the protection and applied the change. By LiteLLM’s account, one stolen token belonged to one of the two GitHub organization owners, which is what let the attacker disable the protection rather than work around it. A second account satisfied the production approval gate at 11:11:18. A third deleted the production and staging workflow records at 11:53, which the report says limited the logs available to investigators.

StepSecurity’s commit-level reconstruction fills in the lookalike repository. Ten commits landed in Berri-AI/litellm between 08:56 and 10:46 UTC. The first three, inside two minutes, added cook.pth, cook.py and sitecustomize.py. Four more reset the version string to 1.107.0 and pinned the same companion package versions as the release candidate, so a dependency diff against 1.107.0rc2 would show nothing unusual. Two further commits refined the loader and added the data = "cook" line, and the last commit added COPY cook ./cook to three Dockerfiles. The first nine carry a Verified badge, which means only that they were created in GitHub’s web editor; the badge says nothing about who was at the keyboard.

Where the tokens came from is where the record thins. LiteLLM says it found an unexpectedly named user account in its own internal deployment that was reachable from the public internet, that the attacker obtained GitHub CLI OAuth tokens from that deployment’s logs, and that Claude Code will in some situations embed a user’s GitHub CLI OAuth token in a response body, which then lands in LiteLLM logs when response bodies are logged. That is LiteLLM’s account of the mechanism, not a statement from Anthropic, and the company says it has not confirmed how the attacker reached the internal deployment. Earlier in the day the report attributed the activity to three engineers’ accounts without naming a token source; the 20:30 UTC update added the attack-chain account.

Two things about this stage are worth holding apart. The build provenance is documented: the release workflow was BerriAI/project-releaser run 39587059944, and only the 1.107.0 build pointed at the hyphenated repository. The initial access into LiteLLM’s own deployment is not documented, only suspected. Wiz’s analysis puts the payload timing alongside that chain without asserting a shared root cause.

Developer working at a laptop with source code visible on screen
Every Python start in an affected virtual environment re-ran the collection routine, so a single install multiplied the exposure across tools and CI jobs.

Why Pinning 1.106.2 Is Only Half the Fix

PyPI removed the release by 13:23 UTC. The container registries did not move in step, and that gap is the part teams get wrong. BerriAI’s own report gives the windows: GHCR tags were made unavailable by 13:51, and the Docker Hub tags v1.107.0, latest, main-latest and main-stable resolved to the compromised image index until 14:20. For 57 minutes after the wheel removal, a fresh docker pull docker.io/litellm/litellm:latest still produced a stealer container.

Artifact Exposure window (UTC, 21 Oct 2026) Status per LiteLLM
PyPI litellm==1.107.0, all seven wheels 11:39:51 to 13:23 Removed; project page shows release quarantined
Docker Hub v1.107.0, latest, main-latest, main-stable 11:46:56 to 14:20 v1.107.0 removed; other tags repointed to verified clean artifacts
GHCR litellm, litellm-database, litellm-non_root 11:46:56 to 13:51 Tags made unavailable
litellm-1.107.0.tar.gz sdist 11:39:51 to 13:23 Removed; tampered pyproject.toml, no payload files
litellm==1.106.2 and earlier releases Not exposed Wheels checked clean in the tracking issue

LiteLLM says its private enterprise registry was not affected; the exposure is limited to the public PyPI and Docker artifacts above. The compromised Docker Hub image index is sha256:1bbcb1443bc962e45f5aad53fe3d2c30600e0379715fb6c664cdd3ac126c339e. Add it to a deny list. Then pin the replacement by digest, because a tag is the thing that moved. BerriAI’s instruction is to pin litellm==1.106.2 by digest, not by a floating tag, and that applies to both the wheel and the image reference. The clean image index digest for docker.io/litellm/litellm and ghcr.io/berriai/litellm is sha256:7294b90f868c5c38c99b83021c7ba46f9b439e95f8e32964c925c7d2563a03f7; verify the wheel hash independently before install. A registry check on 22 October by Rescana found latest, main-latest and main-stable on all four images resolving to the same image index as that image’s v1.106.2 tag, but resolving correctly today is not a guarantee you can accept in a lock file.

There is a second reason to distrust version strings. The 1.107.0 git tag in BerriAI/litellm was clean, and it pointed at commit 66c8f5b1a9, whose pyproject.toml said version = "1.108.0". SafeDep compared the sdist against git trees by blob hash and found it matched a later main commit, a8113b0, plus five edited files and the cook/ folder. Anyone who diffed the tag against the published sdist would have seen the discrepancy, and almost nobody does that during an upgrade.

The March 2026 Precedent: 1.82.7 and 1.82.8

This is the second LiteLLM compromise on PyPI this year, and the earlier one is worth reading as a rehearsal rather than a coincidence. On 24 March 2026, versions 1.82.7 and 1.82.8 shipped a malicious .pth file that harvested SSH keys, cloud credentials and CI secrets, with reported spread into Kubernetes clusters. FutureSearch’s writeup documented that release, and the project tracked it in issue 24518. The remedial advice then was to pin 1.82.6 immediately. The remedial advice now is to pin 1.106.2. Same shape, different numbers, seven months apart.

The mechanisms diverge in one way that matters for defenders. The March release was traced to the TeamPCP group and to the Trivy supply-chain breach, and that attribution belongs to that incident. BerriAI’s account of October describes a different route: no upstream publishing credential was needed, because the build itself was redirected to a lookalike repository through stolen delegated tokens. Immutable release tags, which the company says it added after March, do not help when the workflow that reads them has been rewritten. That is why the October postmortem emphasizes branch protection and approval gates, and why the company says it has removed all but one user from its GitHub organization while it audits tokens.

Server racks and network cabling in a data center
Registry and build infrastructure became the enforcement point, since the source tag users could inspect never contained the payload.

Detection and Response Checklist

If 1.107.0 was installed or a compromised image was started on a host, the safe assumption is that the host is compromised. The run-once marker at /tmp/.pc_once_72129118c59f5636c0bb is written only after a successful upload, so finding it confirms exfiltration but its absence proves nothing, because containers and tmp cleanup remove it.

  • Isolate the host first. Cut outbound network access before doing anything else, so the hourly loader cannot fetch new instructions and nothing further leaves.
  • Do not run pip in the affected environment. Any Python start re-executes cook.pth and sitecustomize.py, including pip install, pip show, and a plain python -c. Delete cook.pth, cook.py, sitecustomize.py and any matching __pycache__ entries from every site-packages directory from the shell instead.
  • Kill the daemon. The detached process holds /tmp/remote_code_loader.lock, runs in its own session, and has a working directory of /. A Python process matching that profile on a host that should not have one is the backdoor.
  • Rebuild rather than repair. Rebuilding images and virtual environments from scratch is better than uninstalling, and the base image must be digest-pinned. PRIVCOLLECT_ACTIVE=1 in a process environment is a marker worth grepping for.
  • Rotate everything the host could reach. Instance and task IAM roles, every provider key in the LiteLLM proxy database plus the master and virtual keys, the DATABASE_URL password, SSH and GPG keys, browser passwords, AI tool tokens, and any secret that sat in the environment of any process on that host. Rotate from a clean machine.
  • Check CloudTrail for GetCallerIdentity. The stealer validates AWS credentials with that call before sending them, so a GetCallerIdentity from an instance role around install time, followed by use of that role from outside your infrastructure, is the sequence to hunt.
  • Block the exfil domain and the dead drop. litellm.dev and the letters9813 GitHub raw path belong on egress deny lists, though the domain alone will not catch a re-armed loader pointed at a new host.
  • Move to hash-checked installs and a private mirror. A mirror that has ingested 1.107.0 serves it again. Require --require-hashes against a lock file, and gate the mirror on the OSV malicious-package feed.
  • Audit GitHub OAuth and CLI tokens, and release permissions. Enumerate every gh credential, every organization owner, and every workflow that can publish. LiteLLM’s own timeline shows a temporary permission expansion preceding the incident by a day.

Confirmed, Attributed, and Unverified

The confirmed layer is narrow and mechanical: seven wheels and the container images carried the three files, the hashes are published, the exposure windows are documented, and 1.106.2 and the release candidates are clean. As of 22 October, no CVE has been assigned, no GitHub security advisory was published in the first hours, and OSV logs the package under malicious-package identifier MAL-2026-2144 instead. Teams that only consume CVE feeds will not see this one.

The middle layer is LiteLLM’s account of its own failure, published by the company and not independently reconstructed: the token theft, the expanded permissions, the organization-owner credential, and the Claude Code logging behavior that the company says exposed the tokens. Treat each as a company statement with a named author and a timestamp, which is more than most postmortems offer, while noting that the report says plainly that initial access is unconfirmed and that the suspected entry point is a recently patched vulnerability it does not name. A separate set of facts comes from outside researchers: SafeDep, StepSecurity and Wiz each decoded the payload statically, and StepSecurity tied the build provenance to the hyphenated repository.

The unverified layer covers second-stage activity and impact. Wiz reports observing abuse of harvested AWS instance role credentials between 16:20 and 17:01 UTC, roughly four and a half hours after exfiltration, validated with GetCallerIdentity and followed by bedrock:ListFoundationModels calls from two IP addresses. That is one firm’s telemetry and not an estimate of how many organizations were hit. There is no evidence in the public record of widespread exploitation. The sources cited here do not quantify October exfiltration; large stolen-data volume figures circulating in secondary coverage are not supported by them. The remote loader’s dead drop held no domain from 20 October 09:15 UTC onward according to the tracking issue, and the GitHub account behind it was gone by 13:40 on 21 October, so hosts that started the daemon inside that window may have received code that nobody has recovered.

The durable lesson sits in the artifact. A .pth file runs before any import statement the application contains, which means a pre-install integrity check is the control that acts before the payload does. Scanning installed code after the fact, diffing the git tag, or reading the source will all come up clean here. What catches this class is a build that can prove which repository, commit, and workflow produced a given hash, and a deployment that refuses anything else. For a broader look at how this pattern played out across npm and PyPI this year, our analysis of the 2026 GitHub supply-chain campaign and the TanStack compromise cover the same progression from stolen tokens to trusted-pipeline abuse.

Latest official status: LiteLLM’s incident report (last updated 20:30 UTC on 21 October) says the incident is contained and the investigation is in progress. Its 23:37 UTC GitHub update says registries remain locked, ordinary releases are paused, and the company is auditing every authentication token; its next update was due at 01:30 UTC on 22 October. Read the live issue for anything newer.

More in-depth coverage from this blog on closely related topics:

Sources and References

Sources cited while researching and writing this article: