# LiteLLM 1.107.0: Why Image Signing Didn't Stop the Backdoor

> Stolen developer tokens rewrote LiteLLM's release workflow on Oct 21. Cosign signed the backdoored 1.107.0 build anyway. Why, and a scanner for your hosts.

- Canonical URL: https://webofmike.com/litellm-pypi-backdoor/
- Author: Mike Moore (https://webofmike.com/about/)
- Published: 2026-10-23
- Last modified: 2026-10-23
- Tags: AI Gateways, Security, Generative AI, Platform Engineering
- Cite as: Mike Moore, "LiteLLM 1.107.0: Why Image Signing Didn't Stop the Backdoor", Web of Mike (webofmike.com), 2026-10-23. https://webofmike.com/litellm-pypi-backdoor/


LiteLLM 1.107.0 landed on PyPI and Docker Hub on October 21, 2026, with a credential stealer built into every wheel, and it was cryptographically signed through the same release pipeline that was hardened after an earlier 2026 compromise. The signing worked exactly as designed. It signed the wrong build, because an attacker had already rewritten what the pipeline would build. I wrote [litellm-1107-ioc-scanner](https://github.com/themsquared/litellm-1107-ioc-scanner) to check hosts for the documented indicators, and this post is the timeline and the argument for why "it's signed" stopped being a useful answer here.

## What happened, in order

Direct answer: a three-hour window on October 21 took an attacker from a lookalike repo to seven backdoored wheels on PyPI, using credentials stolen from LiteLLM's own developers rather than any flaw in the published code.

The timeline, reconstructed from [StepSecurity's technical writeup](https://www.stepsecurity.io/blog/litellm-1-107-0-compromised-pypi-backdoored-wheels-credential-stealer) (all times UTC):

- **Oct 13, 00:45**: `litellm.dev` registered by the attacker.
- **Oct 18, 04:17**: a dead-drop repository, `pythonclient-test`, created for later use.
- **Oct 21, 08:56-10:46**: ten commits pushed to a lookalike repo, `Berri-AI/litellm` (note the capital A, not BerriAI's real `BerriAI/litellm`).
- **10:08:34**: a stolen organization-owner token overrides branch protection on the real release workflow.
- **10:46:39 - 11:08:53**: the modified workflow runs a staging build, then kicks off production.
- **11:11:18**: a second stolen developer credential satisfies the approval gate.
- **11:39:51 - 11:40:13**: seven `1.107.0` wheels upload to PyPI.
- **11:46:56**: infected images publish to GHCR and Docker Hub, with `latest`, `main-latest`, and `main-stable` repointed at them.
- **11:53:49 - 11:54:01**: a third stolen credential deletes the workflow run records.
- **12:06**: the compromise is reported in a BerriAI/litellm GitHub issue.
- **13:23**: PyPI removes `1.107.0`. GHCR and Docker Hub tags follow by 14:20.

Earlier `1.106.x` releases and the `1.107.0` release candidates were reported clean. This was one specific, deliberately timed release.

## This is the second time

JFrog's [write-up of 1.107.0](https://research.jfrog.com/post/litellm-hijacked-again/) says it plainly: this is the second documented hijacking of LiteLLM in 2026. In March, a malicious, unpinned Trivy release running in LiteLLM's CI exposed publishing credentials, and backdoored 1.82.7 and 1.82.8 went out on PyPI. JFrog found no evidence linking the two campaigns, so treat them as separate incidents. That makes it worse, not better. Two different paths into the same release pipeline in seven months.

The supply chain is only part of the record. Since April 2024 the project has published a run of critical and high severity CVEs, and three of them are in CISA's Known Exploited Vulnerabilities catalog. The same classes repeat: template injection to code execution in 2024 and again in 2026, a pre-auth SQL injection in the API key check, and four separate auth bypasses, two of them in MCP paths.

LiteLLM is everywhere because it is easy to adopt. That is exactly why the record matters. Easy to use doesn't forgive gaping security flaws.

| Date (first public record) | Entry | Class | CVSS | KEV | Source |
|---|---|---|---|---|---|
| 2024-04-10 | CVE-2024-2952 | Template injection (SSTI) to RCE, /completions | 9.8 | No | [NVD](https://nvd.nist.gov/vuln/detail/CVE-2024-2952) |
| 2024-06-27 | CVE-2024-5751 | RCE via /config/update | 9.8 | No | [NVD](https://nvd.nist.gov/vuln/detail/CVE-2024-5751) |
| 2025-03-20 | CVE-2024-6825 | RCE via post_call_rules | 8.8 | No | [NVD](https://nvd.nist.gov/vuln/detail/CVE-2024-6825) |
| March 2026 | Supply chain compromise #1: 1.82.7, 1.82.8 backdoored on PyPI after a malicious Trivy release in CI exposed publishing credentials | Supply chain | n/a | n/a | [JFrog](https://research.jfrog.com/post/litellm-hijacked-again/) |
| 2026-04-03 | CVE-2026-35030 | Auth bypass, OIDC cache key (only with enable_jwt_auth, off by default) | 9.4 (CNA) | No | [GHSA](https://github.com/advisories/GHSA-jjhc-v7c2-5hh6) |
| 2026-04-20 | CVE-2026-42208 | Pre-auth SQL injection in API key verification | 9.8 | Added 2026-05-08 | [GHSA](https://github.com/advisories/GHSA-r75f-5x8p-qvmc) |
| 2026-04-26 | CVE-2026-42208 exploitation observed, ~36h after GHSA indexing, targeting key and credential tables | Exploitation | n/a | n/a | [Sysdig](https://www.sysdig.com/blog/cve-2026-42208-targeted-sql-injection-against-litellms-authentication-path-discovered-36-hours-following-vulnerability-disclosure) |
| 2026-05-08 | CVE-2026-42271 | Command injection via MCP stdio test endpoints | 8.8 | Added 2026-06-08 | [NVD](https://nvd.nist.gov/vuln/detail/CVE-2026-42271) |
| 2026-05-21 | CVE-2026-47101 | Privilege escalation: internal_user mints keys for admin routes | 8.8 | No | [NVD](https://nvd.nist.gov/vuln/detail/CVE-2026-47101) |
| 2026-06-16 | CVE-2026-49468 | Auth bypass via Host header, management routes | 9.8 (NVD) | No | [GHSA](https://github.com/advisories/GHSA-4xpc-pv4p-pm3w) |
| 2026-06-21 | CVE-2026-12773 | Auth bypass, MCP proxy | 9.8 NVD / 7.3 CNA | No | [NVD](https://nvd.nist.gov/vuln/detail/CVE-2026-12773) |
| 2026-07-08 | CVE-2026-59822 | Auth bypass, MCP OAuth2 passthrough fallback | 8.2 NVD / 8.8 CNA | Added 2026-09-02 | [NVD](https://nvd.nist.gov/vuln/detail/CVE-2026-59822) |
| 2026-08-27 | CVE-2026-37004 | Unauthenticated SSTI to OS command exec, /prompts/test | 9.8 | No | [NVD](https://nvd.nist.gov/vuln/detail/CVE-2026-37004) |
| 2026-10-21 | Supply chain compromise #2: all seven 1.107.0 wheels and the Docker image carried a credential stealer and hourly remote code loader; sdist did not contain the payload | Supply chain | n/a | n/a | [JFrog](https://research.jfrog.com/post/litellm-hijacked-again/) |

KEV status from the [CISA KEV catalog](https://www.cisa.gov/known-exploited-vulnerabilities-catalog), checked Oct 22, 2026.

I keep the full record, with the questions I'd want a security team to answer before deploying it, at [LiteLLM security record: a due-diligence timeline](/litellm-security-record/).

## Why image signing didn't stop it

Cosign and PyPI's Trusted Publishing answer one question: did this exact CI workflow run produce this artifact. They don't answer a second, different question: were the instructions in that workflow the ones anyone actually approved. BerriAI had already hardened both after an earlier incident this year, and both worked correctly on October 21. They signed the backdoored wheels because the attacker had modified `.github/workflows` itself before the pipeline ran, not because the signing step was broken.

That distinction matters beyond this one project. [agentgateway](https://agentgateway.dev), the AI gateway I work on at Solo.io, signs its own container images with cosign through GitHub OIDC during release, the same mechanism LiteLLM uses. I'm not holding that up as proof agentgateway is immune to this attack class, because it isn't, and no project whose signing step trusts its own repository's workflow files inherently is. If an attacker can get a change into the workflow that builds and signs a release, the signature will cover whatever that modified workflow built, with no indication anything is wrong. What actually defends against this is separate from signing: who can change files under `.github/workflows/**` at all (ideally gated tighter than ordinary code review, with required reviewers that a single compromised token can't override), and whether verification checks provenance against a pinned, external builder identity rather than trusting a project to vouch for its own pipeline. I compare agentgateway and LiteLLM's actual feature sets, with the disclosure that applies to any comparison post like that, in [agentgateway vs LiteLLM vs Portkey](/agentgateway-vs-litellm-vs-portkey/).

The reported initial access is the part that should bother anyone running AI coding tools in a CI context: BerriAI's incident update says GitHub CLI OAuth tokens for three developers likely came from the logs of an internal LiteLLM deployment exposed to the internet, and that it has seen Claude Code embed those tokens in response bodies, which end up in logs wherever response bodies are logged. BerriAI says it has not yet confirmed how the attacker got into that deployment. An agent that logs what it sees is a credential leak waiting for the one response that contains a token. That's a gotcha worth checking in your own pipelines regardless of what you think of this specific incident.

## What the backdoor does

Once installed, the planted files run on every Python interpreter start on the host, not only when something imports `litellm`:

- `cook.pth`, a `.pth` loader (`import cook`) that Python reads automatically.
- `cook.py`, the obfuscated credential stealer itself.
- `sitecustomize.py`, which forks a detached daemon that polls a dead-drop repository hourly for a second-stage payload.

The stealer's documented targets, per [Wiz's and Chainguard's analyses](https://www.wiz.io/blog/litellm-compromised-steals-credentials): browser passwords and cookies, SSH and GPG keys, cloud credentials for AWS, Azure, and GCP, Kubernetes and Docker configs, CI secrets (GitLab Runner, Jenkins, Vault, Terraform), cloud instance metadata, and tokens for more than twenty AI coding tools. On a LiteLLM proxy with `DATABASE_URL` set, it also reaches the provider API keys and model configuration sitting in the proxy's own database. Exfiltration goes out as an AES-256-CBC encrypted, RSA-wrapped archive over plain HTTP to `litellm.dev/healthcheck`.

The second stage, the hourly dead-drop poll, was not armed during the window the infrastructure existed: `domain.txt` in the dead-drop repo was empty, so there was nothing for the daemon to fetch and `exec()`. That's a real mitigating detail, not a reason to relax: the persistence mechanism and the exfiltration path both ran regardless.

## Checking your own hosts

This is what [litellm-1107-ioc-scanner](https://github.com/themsquared/litellm-1107-ioc-scanner) does. It's a single stdlib Python file, no dependencies, that checks for the documented markers without reproducing any of the actual payload:

```bash
git clone https://github.com/themsquared/litellm-1107-ioc-scanner.git
cd litellm-1107-ioc-scanner
./scripts/demo.sh
```

That runs the scanner against a clean fixture, then against a fixture with inert stub files at the real incident's planted paths (`cook.pth`, `cook.py`, `sitecustomize.py`), then against the machine's real site-packages. Against a compromised environment it reports:

```
[CRITICAL] planted file present: .../site-packages/cook.pth
[CRITICAL] planted file present: .../site-packages/cook.py
[WARN] sitecustomize.py present (corroborating, not conclusive): .../site-packages/sitecustomize.py

COMPROMISED: treat this environment as breached. Do not clean in place.
```

It also checks for the `/tmp/remote_code_loader.lock` dead-drop-poll lock file, the `PRIVCOLLECT_ACTIVE` environment variable, and whether the installed `litellm` version is exactly `1.107.0`. Exit code 2 means stop and rebuild, not clean in place: kill whatever holds the lock file before touching pip, rebuild the environment or image from scratch, pin `litellm==1.106.2`, and rotate every credential reachable from that host.

### Gotcha: `sitecustomize.py` alone isn't proof

The scanner flags a bare `sitecustomize.py` as `WARN`, not `CRITICAL`, on purpose. Plenty of legitimate setups use that filename for unrelated interpreter customization, so its presence is corroborating evidence next to `cook.pth`, not conclusive by itself. Don't treat a lone `sitecustomize.py` hit as a confirmed breach; treat the `cook.pth`/`cook.py` pair, the `/tmp` lock file, or the environment variable that way.

### Gotcha: no GHSA number as of this writing

As of October 22, I could not find a GitHub Security Advisory specific to `1.107.0`; the existing LiteLLM malware advisory, GHSA-92x9-889m-jgmw, still only lists the earlier 2026 incident's versions. If you're scripting a dependency check against GHSA IDs instead of exact version pins, that gap means a GHSA-based check will miss this one.

## What I'd actually check this week

If you run LiteLLM anywhere, standalone or as a proxy: run the scanner above against every host and image that could have pulled `1.107.0` between 11:39 UTC on October 21 and roughly 14:20 UTC the same day, pin to `1.106.2` explicitly rather than trusting `latest`, and if you're on the proxy with a `DATABASE_URL`, rotate the master key, the salt key, and every stored provider key regardless of what the scanner finds, since a clean scan only means this host wasn't compromised, not that keys it ever held weren't exposed somewhere upstream.

If you maintain a release pipeline that signs its own artifacts, the broader lesson travels past this one project: ask who can change the workflow files that produce your signed builds, and whether that's actually harder to compromise than changing ordinary application code. If the answer is "the same branch protection," this is what it looks like when that protection is bypassed with a stolen token instead of a code review.

The scanner is at [github.com/themsquared/litellm-1107-ioc-scanner](https://github.com/themsquared/litellm-1107-ioc-scanner). It's stdlib-only on purpose: during an active supply-chain scare is a bad time to ask anyone to trust a new dependency.

