On October 21, 2026, LiteLLM 1.107.0 reached PyPI and the official Docker images. The v1.107.0 tag on GitHub did not contain the malicious files. The wheels for that same version did. They installed a credential stealer and a remote code loader that ran whenever Python started in the environment.
The lesson is not that open source is untrustworthy. It is that source code and the artifact you install are different objects. Reviewing a tag does not tell you what a wheel will execute, and a valid signature does not prove that the build looked at the right repository.
What was published, and for how long
LiteLLM's incident report gives a usefully specific timeline. All times are UTC.
| Artifact | Affected range | Exposure window |
|---|---|---|
PyPI litellm==1.107.0 |
All seven wheels | 11:39:51–13:23 |
| Docker Hub | v1.107.0, latest, main-latest, main-stable |
11:46:56–14:20 |
| GHCR | The same tags on litellm, litellm-database, and litellm-non_root |
11:46:56–13:51 |
The affected Docker Hub image index was sha256:1bbcb1443bc962e45f5aad53fe3d2c30600e0379715fb6c664cdd3ac126c339e. LiteLLM says any container that executed that digest should be treated as compromised.
There is an important exception. The GitHub source tag and the source distribution did not contain the payload. The sdist's pyproject.toml had one added line, data = "cook", but the cook/ directory itself was not included. A review of the source tarball therefore produced a clean-looking result. The wheel is what users actually ran.
How the wheel and the source diverged
LiteLLM builds platform wheels with maturin, including a Rust bridge. Maturin's data option copies a local directory into the wheel's .data/ tree. The attacker used that setting to place three files at the top level of site-packages.
| File | Role |
|---|---|
cook.pth |
A 12-byte import cook. Python's site initialization executes it. |
cook.py |
A credential stealer that encrypts collected data and sends it out |
sitecustomize.py |
Starts a background loader that periodically fetches remote code |
Both .pth files and sitecustomize.py are processed automatically when a Python interpreter starts. The code does not need to import litellm. pip, pytest, an IDE language server, and a Jupyter kernel all trigger the same startup path if they share the environment. "Our application never imports LiteLLM" is not a defense.
Independent analyses add detail. SafeDep found that cook.py collected SSH keys, cloud credentials, CI/CD secrets, browser passwords, AI coding-tool tokens, and provider keys from a LiteLLM proxy database. StepSecurity reported that the bundle was encrypted with AES-256-CBC, wrapped with an RSA-4096 key, and posted over plain HTTP to www.litellm[.]dev/healthcheck, a lookalike of LiteLLM's real domain.
The loader was designed to read a server address from a GitHub dead drop and download code every hour. At analysis time, the dead drop was empty and the GitHub account had been deleted. Researchers did not find evidence that the second stage actually ran. That does not make the host safe: infected machines keep polling the same location, so a later re-registration of that account could restore the channel.
On Windows, the loader fails silently because it depends on fork. The stealer still runs. A platform-specific loader failure is not evidence of a clean host.
How the release pipeline was subverted
According to LiteLLM, the attacker first reached an internal LiteLLM deployment exposed to the public internet. The deployment's logs contained GitHub CLI OAuth tokens for three developers. One token belonged to a GitHub organization owner. The attacker used it to disable branch protection, force a change to the release workflow, and point the build at a lookalike repository, Berri-AI/litellm instead of BerriAI/litellm. The official pipeline then built, signed, and published the malicious artifacts.
The uncomfortable part is that the signature was valid. Signatures and SLSA provenance answer a narrow question: which pipeline produced this artifact? They do not automatically prove that the pipeline read from the repository you expected. StepSecurity found that the v1.107.0 image provenance named the hyphenated lookalike repository. That is a crucial distinction for anyone who treats "signed image" as a complete supply-chain control.
LiteLLM's report also says that Claude Code could embed a user's GitHub CLI OAuth token in a response body, and that logging response bodies put those tokens in LiteLLM's logs. That is LiteLLM's account, not an independently confirmed Anthropic statement. It should be separated from the part of the chain that is supported by the release records: an organization-owner token changed the workflow definition. The initial access route is still under investigation.
Check before you start Python
The official report suggests checking with pip show litellm. The same report warns that every Python start, including pip, runs cook.pth and sitecustomize.py again. That is a real tension. If compromise is plausible, isolate the host and inspect files and lockfiles with shell tools before invoking Python.
The commands below are unexecuted examples for this article. Adapt paths and permissions, and keep them read-only where possible.
# 1) Look for startup hooks added to site-packages
find / -type f \( \
-path '*/site-packages/cook.pth' -o \
-path '*/site-packages/cook.py' -o \
-path '*/site-packages/sitecustomize.py' \
\) 2>/dev/null
# 2) Search lockfiles and CI configuration for the exact version
grep -RIn --include='requirements*.txt' --include='poetry.lock' \
--include='uv.lock' --include='Pipfile.lock' --include='pyproject.toml' \
'litellm.*1\.107\.0' . 2>/dev/null
# 3) Inspect the image digest of a running container
docker image inspect --format '{{.RepoTags}} {{.RepoDigests}}' litellm:latest 2>/dev/null
A sitecustomize.py file can be legitimate; some Python distributions ship one. Inspect it before deleting it. Absence of the files is not proof of cleanliness either, because containers and temporary directories may have been rebuilt or cleaned.
If exposure is confirmed, order matters.
- Isolate the host or container from the network.
- Without starting Python, remove
cook.pth,cook.py,sitecustomize.py, and the matching__pycache__files from every affectedsite-packages. - Find and stop the detached Python process holding
/tmp/remote_code_loader.lock. - Reinstall with
litellm==1.106.2, or rebuild the image and virtual environment from a verified base. - Rotate every credential the host could reach.
Do not begin with pip uninstall litellm. That also starts Python and runs the startup hook again. The first task is removing the code that executes on interpreter startup, not removing the package name from metadata.
Credentials to rotate
A LiteLLM proxy concentrates many provider keys in one process. A single compromised developer laptop or gateway can therefore expose a wide set of credentials.
| Scope | Examples |
|---|---|
| Cloud | AWS instance/task role, GCP service account, Azure credentials |
| LiteLLM | Master key, virtual keys, DATABASE_URL password, provider keys |
| Developer tools | GitHub CLI, SSH/GPG keys, package registry tokens |
| AI tools | Claude Code, Codex, Gemini CLI, Cursor, and similar sessions |
| CI/CD | Vault tokens, GitLab Runner, Jenkins, Terraform and Packer credentials |
LiteLLM recommends checking CloudTrail for GetCallerIdentity from the instance role around install time, followed by use of that role's credentials from outside your infrastructure. Treat that as an investigation lead, not proof of what data was taken.
Controls that address the actual failure
"Turn on package signing" is not enough. The attacker used a signed pipeline. Defenses have to cover the artifact, the build definition, credentials, and egress separately.
| Control | What it prevents | What it does not solve |
|---|---|---|
| Pin versions and hashes | A new release entering automatically | An already-pinned malicious version |
| Pin images by digest | A floating tag changing underneath you | A malicious digest that you pin |
| Check provenance source | Artifacts built from a lookalike repository | A compromised build definition |
| Short-lived release tokens | Long-term abuse of a stolen token | A stolen owner token that can rewrite the workflow |
| Separate approval for workflow changes | One owner token rewriting the release path | Approvers who reuse the same credential |
| Egress allowlists | Known exfiltration domains | Unknown domains and DNS bypasses |
| Install cooldown | Immediate adoption of a bad release | Mirrors that sync instantly |
The important distinction is between "this artifact was signed" and "this artifact came from the source and workflow we intended." If you verify provenance in CI, check the source repository and the protected branch or workflow identity, not just the presence of an attestation.
What this incident does not establish
Separate confirmed facts from open questions. The malicious artifacts, exposure windows, file names, exfiltration path, and build source are documented by LiteLLM and independent analysts. The following are not settled.
- The exact vulnerability used for initial access to LiteLLM's internal deployment
- Whether the Claude Code token-exposure explanation generalizes beyond LiteLLM's logs
- Whether stolen data was used, and at what scale
- Whether any host actually received and ran second-stage code
- Which credentials were exposed in each affected organization
That uncertainty argues for assuming exposure when the install history matches, not for waiting until data misuse is proven.
Conclusion: the artifact is the trust boundary
The LiteLLM incident is not an argument against open source. It is an argument for treating the tag, the sdist, the wheel, the container image, the signature, and the provenance as answers to different questions. A clean source tree does not make a wheel safe. A valid signature does not prove the build read the right repository.
Three actions matter now. Search lockfiles for litellm==1.107.0, check the digests of running containers, and rotate credentials for any environment that could have executed the affected artifact. Then revisit who can change your release workflow. That is the control the attacker actually used.
Further reading
- When Agents Choose Packages: Understanding the GitLab Dependency Firewall
- AI Agent Security: Why You Should Not Run Without Permissions in the Computer-Use Era
- CodeQL 2.27.2: What Security Analysis Catches and What It Misses
References
- LiteLLM: Contained — October 21, 2026 LiteLLM release v1.107.0 compromised
- GitHub: BerriAI/litellm issue #47826 — security incident
- SafeDep: Compromised LiteLLM 1.107.0 on PyPI installs a credential stealer and backdoor
- StepSecurity: litellm 1.107.0 compromised on PyPI
- Chainguard: LiteLLM attacked again — malicious 1.107.0 wheels on PyPI and Docker Hub