# LiteLLM 1.107.0 supply-chain compromise (PyPI and Docker): detection pack

On 21 October 2026 a malicious `litellm==1.107.0` was published to PyPI, and malicious LiteLLM Docker images were published alongside it. There is no CVE: this is a supply-chain attack, not a bug. LiteLLM says its release pipeline was compromised with stolen developer GitHub credentials and that the artefacts were built from a lookalike repository, `Berri-AI/litellm`. The official `v1.107.0` git tag is clean.

**If 1.107.0 was installed from a wheel, or a container ran from an affected image, treat the host as compromised.** Isolate it, rebuild it, and rotate every credential it could reach from a clean machine. The last clean version is **1.106.2**.

Sources (primary and researcher):
- LiteLLM incident post: https://docs.litellm.ai/blog/security-incident-oct-21-2026
- Independent analysis and IOCs published by security researchers.
- PyPI project: https://pypi.org/project/litellm/ (the latest version is 1.106.2, and `/pypi/litellm/1.107.0/json` returns 404)
- The Hacker News: https://thehackernews.com/2026/10/litellm-compromised-in-supply-chain.html
- The original Docker-image finding came from security researchers, and the later analyses cite it. We did not retrieve that original report itself.

## What is affected

| Artefact | Status |
|---|---|
| PyPI `litellm==1.107.0`, all 7 wheels (`cp310-abi3` macOS x86_64/arm64, manylinux and musllinux x86_64/aarch64, win_amd64) | **Malicious.** Live 11:39:51 to about 13:23 UTC (12:39 to 14:23 BST) per LiteLLM. Since removed. |
| PyPI `litellm-1.107.0.tar.gz` (sdist) | Tampered `pyproject.toml` (`data = "cook"`) but **no payload files** (security researchers) |
| `ghcr.io/berriai/litellm`, `litellm-database`, `litellm-non_root` and `docker.io/litellm/litellm`: tag `v1.107.0`, plus `latest`, `main-latest` and `main-stable` while they pointed at it | **Malicious.** Docker Hub 11:46:56 to 14:20 UTC; GHCR until 13:51 UTC (LiteLLM). The payload sits in `/app/.venv/lib/python3.13/site-packages/`. |
| 1.107.0rc1/rc2, 1.106.x, 1.105.x, 1.104.x, 1.103.x | Clean according to security researchers' inspection |
| LiteLLM private enterprise registry | Not affected (LiteLLM) |

## How it works (security researcher analysis)

Each wheel drops three extra files into the top level of site-packages: `cook.pth`, `cook.py` and `sitecustomize.py`. Python runs them every time **any** interpreter starts in that environment, including `pip`, `pytest` and Jupyter. Nobody has to `import litellm`.

- **`cook.pth`** is 12 bytes, the line `import cook`. It loads `cook.py`, the **privcollect** credential stealer. The stealer runs 16 collection modules covering browser passwords, cookies and history, SSH, GPG and TLS keys, AWS/Azure/GCP/kube/Docker/GitHub CLI credentials, cloud instance metadata, CI and Vault secrets, the environment variables of every readable process, shell histories, the configs of more than 20 AI coding tools (Claude, Cursor, Copilot, Gemini CLI, Codex and others), and, when `DATABASE_URL` is set, the LiteLLM proxy's `LiteLLM_CredentialsTable` and `LiteLLM_ProxyModelTable`. The collected data is encrypted with AES-256 plus RSA-4096 and sent by **HTTP POST over plain HTTP to `http://www.litellm[.]dev/healthcheck`** with the header `X-Token: XWiA8gZNa7NcX2XRoAEb`. The project's real domain is `litellm.ai`. Do not block or flag it.
- **`sitecustomize.py`** is the **remote code loader**. It takes a lock on `/tmp/remote_code_loader.lock`, forks a detached daemon (Linux and macOS only), and every hour reads `https://raw.githubusercontent.com/letters9813/pythonclient-test/main/domain.txt`. If that file contains a domain, the daemon downloads `http://<domain>/codepy` and runs it. It also carries a fallback copy of the stealer.
- The dead-drop `domain.txt` was **empty** when the release went out, so the `/codepy` stage was never armed while the dead drop existed. The `letters9813` account and its repository have since been **deleted** (security researchers; one also saw an empty file at 13:01 UTC). Infected hosts keep polling, though, and anyone who re-registers that username could serve them code. That is why the daemon has to be killed. Any request to `/codepy` means attacker code ran.

## IOCs (verbatim from security researcher analysis; digests also from LiteLLM)

**Files written into site-packages:** `cook.pth`, `cook.py`, `sitecustomize.py`. The wheel `RECORD` lists them as `litellm-1.107.0.data/purelib/...`.

| File | SHA-256 (Linux/macOS) | SHA-256 (Windows, CRLF) |
|---|---|---|
| cook.pth | `7f191632e7320705d154b85204f1ae546b543acbd7e8861229b76f5042b92bdb` | `84dc8c82c0a4a4a62552ef826b49fcc86d1a5c4a5d0f2c06665723f93d129d7d` |
| cook.py | `9dd0a3926f96a37eb86cf1ee2310d9be93139cc4ebef3ef9e2efe9531390be25` | `03ac2a8b752605d1d7710979c17ff18b03dd978203ba23ba97fd8ce3d6f897ea` |
| sitecustomize.py | `34bef04fe88ac9d0aae0475630b1738681d15b94f233ac8aa46eacf5968305fa` | `b6122440d0d9cb1eb1ade9933f49214da8c31288de7710c076c2680dfa528465` |

Check: the `cook.pth` hash is the SHA-256 of the 12-byte text `import cook\n`. We reproduced it with a harmless test file during testing.

| Wheel / sdist (`litellm-1.107.0-cp310-abi3-...`) | SHA-256 |
|---|---|
| macosx_10_12_x86_64.whl | `7598727f209d717041db37f7dca271d3a7d328a279890e0e1d55375729aeb86d` |
| macosx_11_0_arm64.whl | `0aaf7ba346d41ef2f40390436738b001180d5a1c03aa093f121078a9969d9a1a` |
| manylinux_2_28_aarch64.whl | `668169d35b87bf51324080390b1b1c5ab12bd335cfe5f1a99e3689abd5e02ae6` |
| manylinux_2_28_x86_64.whl | `462d0c9f67ca522d9e0e4acd245e90b36ce0d5353604a66bb7c43a1ec4a6fcf7` |
| musllinux_1_2_aarch64.whl | `c2e32d0ce2548aa662c18bf5001911a79fe5b30cf8357132275add02fdcde054` |
| musllinux_1_2_x86_64.whl | `1bbce8caa94f01c0aaa0b7ddae51553a8a32e1ce53d96d44fe583272aff7ad00` |
| win_amd64.whl | `12d4ccc5c6f8d35cf4f2cf233b7f3e418e863e657d81ff5f5fc9e210005b9484` |
| litellm-1.107.0.tar.gz (one researcher only) | `2befe2a79fb4fbbd9a543b5b40b530c8cffa8771bab22b8fb22b2e4ae3b1f819` |

**Docker:**
- `ghcr.io/berriai/litellm` / `docker.io/litellm/litellm` image index: `sha256:1bbcb1443bc962e45f5aad53fe3d2c30600e0379715fb6c664cdd3ac126c339e`
- amd64 manifest: `sha256:bd1aeee419ea36da9aa2b657288da4101887be468f020ac63d22ba6ed84a3ea5`; arm64 manifest: `sha256:56b74573eed6e4aafe4b50603dbe7853e0345f188c8e44a09f1098caf1f4bcdf` (security researchers)
- `litellm-database` index `sha256:c00f9d92c427…` and `litellm-non_root` index `sha256:7b00e91cd2fe…`. Only these truncated prefixes have been published, so the checker matches on the prefix.

**Host and runtime:** `/tmp/.pc_once_72129118c59f5636c0bb` (written after a successful exfiltration), `/tmp/remote_code_loader.lock` (a process holding it is the loader daemon), `.pc_sq_*` and `.pc_e_*` temp directories, and the environment variable `PRIVCOLLECT_ACTIVE=1`.

**Network:** `www.litellm[.]dev` / `litellm[.]dev` (registered 13 Oct 2026), `http://www.litellm[.]dev/healthcheck`, `raw.githubusercontent[.]com/letters9813/pythonclient-test/main/domain.txt`, any `/codepy` path, the header `X-Token: XWiA8gZNa7NcX2XRoAEb`, and the user agent `Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 10.0; WOW64; Trident/7.0)`. Historic dead-drop values were `localhost:18909` and `test.test.com:18909`.

**Source:** lookalike repository `github.com/Berri-AI/litellm`, commit `20535d8fa11f786a6c24df7488a0e449ed5f09ad`, author `Berri-AI <ckdi82fkoos@proton.me>`.

## Files

- `check-litellm-1107.sh`: Linux and macOS checker (bash).
- `Check-LiteLLM-1107.ps1`: Windows checker (PowerShell 5.1 and 7; also runs under pwsh on Linux).
- `litellm-1107-hunting.kql`: queries for DeviceFileEvents, DeviceNetworkEvents, DnsEvents/DeviceEvents and DeviceProcessEvents.
- `sigma/*.yml`: payload files (file_event), proxy (exfiltration, dead drop, `/codepy`, X-Token) and DNS rules.
- `tests/`: container test harnesses that use fake, harmless fixtures only.

## Usage

```bash
sudo bash check-litellm-1107.sh /srv/code /home        # paths to scan for lockfiles
sudo PY_ROOTS="/opt /home" bash check-litellm-1107.sh  # limit the site-packages sweep
```
```powershell
powershell -ExecutionPolicy Bypass -File .\Check-LiteLLM-1107.ps1 -ScanPath C:\src,C:\Users
```

The checkers **never start Python or pip** from the host, because doing so would run the payload again. They read `METADATA`/`RECORD` files directly and only read files and process information. Nothing is written, deleted or killed.

What they check:
1. Every `site-packages` and `dist-packages` directory under the roots, which covers system Python, venvs, conda, pipx, `~/.local` and `/app/.venv`. They look for `litellm-1.107.0` metadata, `RECORD` entries for the payload, `cook.pth`/`cook.py` (hash or the `import cook` line), and `sitecustomize.py` (hash or loader strings). Distros ship their own `sitecustomize.py`, so on its own it is only noted. They also look for leftover `__pycache__/cook.*.pyc` and other `.pth` files that `import cook`.
2. The marker and lock files, `.pc_*` temp directories, and 1.107.0 files in the pip/uv caches.
3. `requirements*.txt`, `constraints*.txt`, `poetry.lock`, `uv.lock`, `pdm.lock`, `Pipfile(.lock)`, `pyproject.toml`, `setup.cfg`, Dockerfiles, compose files and Helm values, checked for 1.107.0, the `v1.107.0` image tag, and the malicious wheel hashes and image digests.
4. Docker images: tag `v1.107.0` or a matching digest is COMPROMISED, and a floating tag built on 21 Oct is UNKNOWN. Running containers (bash only) are checked with `docker exec ls` for payload files.
5. Processes. On Linux these are `PRIVCOLLECT_ACTIVE=1` in `/proc/*/environ`, any holder of `/tmp/remote_code_loader.lock`, and suspicious python command lines. On Windows, python command lines.
6. `/etc/hosts` and the Windows hosts file, the Windows DNS client cache, and on Linux syslog, messages, dnsmasq, named, squid and nginx logs plus journald since 21 Oct.

## Verdicts and exit codes

| Exit | Verdict | Meaning |
|---|---|---|
| 0 | CLEAN | Nothing found. Absence of artefacts is not proof: containers and `/tmp` cleanup remove markers. |
| 1 | COMPROMISED | 1.107.0 installed, payload files, a marker or lock, a loader process, a matching image, or log/DNS traces of litellm.dev |
| 2 | UNKNOWN | Needs review: e.g. `cook.py` with a different hash, a floating LiteLLM image built 21 Oct, or a suspicious cmdline |
| 3 | REFERENCED_ONLY | 1.107.0 is referenced in a lockfile, manifest or cache but not installed on this host. Check every machine or CI job that used those files. |

## Response

1. **Treat the host as compromised.** Don't start with `pip install`. Any Python start runs the payload again.
2. Preserve evidence if you need it (process list, `/tmp` markers, site-packages listing, proxy and DNS logs). Kill the process holding `/tmp/remote_code_loader.lock`.
3. **Rebuild** the host, venv or container from a known-good image. Don't clean it in place. Redeploy containers from a pinned digest and purge cached `v1.107.0` images and wheels from internal registries, mirrors and pull-through caches.
4. **Rotate credentials from a clean machine**: cloud keys and instance or task roles, SSH and GPG keys, browser-saved passwords and sessions, GitHub/PyPI/npm and other registry tokens, Kubernetes and Vault tokens, CI secrets, AI tool tokens, and on proxy hosts every provider key in the LiteLLM database, the master and virtual keys, the salt key and the `DATABASE_URL` password. Then review CloudTrail for `GetCallerIdentity` from the instance role around install time (LiteLLM's advice) and for use of the credentials from outside your network.
5. **Block** `litellm.dev` at DNS and the proxy, and alert on `raw.githubusercontent.com/letters9813/` and any `/codepy` request.
6. **Pin 1.106.2 by hash.** PyPI publishes these SHA-256 values for 1.106.2:

```
litellm==1.106.2 \
  --hash=sha256:cb1a2a1e55a4a8350ae2167525dc0d06f59b078cd685fb3e0f5419f02da5420e \
  --hash=sha256:34bfdc26c2c075726eda59ec0675e2a8a67b6e59a03980def7ef7651b849141a \
  --hash=sha256:32e567ee53c812e36f79158ec280c8850973f84a9e5fdef50d31ca09306d148c \
  --hash=sha256:ce18971e2bb2aa58ef8efae1cd12da1248e1c2e5ae3cf5903bfb9d563bb34d9d \
  --hash=sha256:bf6f13445bb59d8d02131bbc1fb57161d1ae629bec65dc66f9add4c09fb6994a \
  --hash=sha256:cc778d01ed65a8dd57d5211d581ec237e06df239a09c3d573ace33e0feea76c9 \
  --hash=sha256:836ab19110504ad7ba71d8ce73af6b9d1cf1dc6ffda2b49ebc1a51e348f754bc \
  --hash=sha256:9f0e770cb007c8c6e78917ad7b5c4f059f7bdc4a3606f854831780ea53fd9e2d
```

These cover manylinux x86_64/aarch64, musllinux x86_64/aarch64, macOS x86_64/arm64, win_amd64 and the sdist. Install with `pip install --require-hashes -r requirements.txt`. For Docker, pin `v1.106.2` by digest and verify it with cosign as described in the LiteLLM README. Use exact pins rather than open `litellm>=` ranges.

## Limitations

- The full digests of the `litellm-database` and `litellm-non_root` image indexes haven't been published, so the checker matches on the truncated prefixes. Per-arch manifest digests for those variants aren't known.
- Images that aren't running aren't unpacked, because doing that would mean creating containers. A floating-tag image is flagged UNKNOWN by its build date only.
- Windows: the loader can't fork there (security researchers), and the stealer's `/tmp` marker path doesn't exist, so marker checks rarely fire. Process environment variables aren't readable from PowerShell. The site-packages sweep over all fixed drives can be slow; use `-PyRoot` to narrow it.
- Linux DNS caches (systemd-resolved, nscd) aren't inspected. Use resolver, proxy or EDR logs (KQL Q2/Q3).
- Removed hosts, ephemeral CI runners and rebuilt containers leave no local artefacts. Check CI logs and lockfile history for installs between 11:39 and 13:23 UTC on 21 Oct, and image pulls between 11:46 and 14:20 UTC.

## Testing done

- `tests/run-container-tests.sh` in `python:3.12-slim`: 22/22 passed. A real clean `litellm==1.106.2` install (the official wheel, SHA-256 `cb1a2a1e…420e`, matching PyPI) gave CLEAN. A fake 1.107.0 dist-info plus a harmless `import cook` cook.pth and a placeholder cook.py gave COMPROMISED. requirements, poetry.lock, uv.lock, Pipfile.lock and Dockerfile references gave REFERENCED_ONLY, with no false positive on `1.107.01` or `1.106.2`. Also COMPROMISED: the marker file; a harmless process holding the lock with `PRIVCOLLECT_ACTIVE=1`; a `sitecustomize.py` containing loader strings. A benign `sitecustomize.py` was not flagged.
- Docker check: a dummy image tagged `ghcr.io/berriai/litellm:v1.107.0` gave COMPROMISED, and the same image tagged `v1.106.2` was reported as ok.
- `tests/run-pwsh-tests.ps1` in `mcr.microsoft.com/powershell`: parse OK, and CLEAN, REFERENCED_ONLY and COMPROMISED gave exits 0, 3 and 1.
- `shellcheck` is clean and `sigma check` reports no issues. The real malware was never downloaded.
