← Tech
💻Tech

LiteLLM 1.107.0 PyPI Compromise: How to Check, Remove the Backdoor and Rotate Keys

litellm 1.107.0 on PyPI (Oct 21, 2026) shipped a credential stealer and hourly backdoor via cook.pth and sitecustomize.py. Check if you're affected, remove it, pin 1.106.2, rotate keys.

TL;DR: On October 21, 2026, an attacker hijacked LiteLLM's release pipeline and published litellm==1.107.0 to PyPI. All seven wheels carried a credential stealer and an hourly remote-code loader, and both run every time Python starts, even if nothing imports litellm. The wheels were live from 11:39 to 13:23 UTC (20:39–22:23 KST). The official Docker tags v1.107.0, latest, main-latest and main-stable pointed at an infected image from 11:46 to 14:20 UTC (20:46–23:20 KST). If you installed 1.107.0 or pulled one of those tags in that window, treat the host as compromised: delete the implant files before you run Python again, rebuild on litellm==1.106.2, and rotate every secret the machine could reach. (LiteLLM incident report, SafeDep analysis)

What happened

LiteLLM is the Python SDK and proxy that puts 100+ LLM providers behind one OpenAI-style API, so it often runs on machines that hold OpenAI, Anthropic, Azure, Bedrock and Vertex keys. According to LiteLLM's own report, an attacker who held GitHub CLI credentials belonging to three LiteLLM developers used an org owner's token to override branch protection on the private release workflow. They then pointed the workflow at a lookalike repo, Berri-AI/litellm (the real one is BerriAI/litellm). The official pipeline built, signed and published the poisoned artifacts. The v1.107.0 tag in the real GitHub repo is clean. LiteLLM thinks the GitHub tokens leaked through logs of an internal LiteLLM deployment that was exposed to the internet.

SafeDep found one injected line in the sdist's pyproject.toml, data = "cook" under [tool.maturin]. That line makes every wheel ship three extra files in litellm-1.107.0.data/purelib/, and pip copies them to the root of site-packages:

File Size What it does
cook.pth 12 bytes Contains import cook. Python runs it at every interpreter start
cook.py 22,252 bytes zlib+base64-wrapped credential stealer that calls itself privcollect
sitecustomize.py 25,509 bytes Forks a detached daemon that fetches and runs remote code every hour

The stealer reads browser passwords and history, ~/.ssh, shell history, AWS/GCP/Azure credentials, ~/.kube/config, ~/.docker/config.json, the GitHub CLI hosts.yml, .netrc, Vault/Terraform/GitLab-runner/Jenkins secrets, configs from AI coding tools (Claude Code, Cursor, Windsurf, Copilot, Codex, Gemini CLI and more), GPG and TLS keys, and the environments of other processes via /proc/*/environ. It also queries cloud metadata (169.254.169.254). If DATABASE_URL is set, it dumps the LiteLLM_CredentialsTable and LiteLLM_ProxyModelTable tables, which hold a LiteLLM proxy's upstream provider keys. The loot is encrypted with AES-256 plus an RSA-4096 key and POSTed to http://www.litellm[.]dev/healthcheck with the header X-Token: XWiA8gZNa7NcX2XRoAEb. After a successful upload it writes the marker /tmp/.pc_once_72129118c59f5636c0bb. (SafeDep, OSV MAL-2026-2144)

The backdoor takes a lock on /tmp/remote_code_loader.lock, detaches with fork()/setsid(), and every hour reads a domain from raw.githubusercontent.com/letters9813/pythonclient-test/main/domain.txt. It then re-runs the stealer and executes whatever http://<domain>/codepy returns. SafeDep reports the letters9813 account returned 404 at 13:40 UTC, so new hosts get no server address now. Hosts that ran the daemon before then may already have received second-stage code, and nobody has publicly recovered that domain.

This is LiteLLM's second PyPI compromise this year. In March 2026, versions 1.82.7 and 1.82.8 were malicious, and 1.82.8 shipped a litellm_init.pth that also ran at interpreter start (GitHub issue #24512). Neither version is listed on PyPI anymore.

Timeline

All times are October 21, 2026, from LiteLLM's report unless noted.

UTC KST Event
10:08 19:08 Privileged credential overrides branch protection; release workflow modified
10:46 19:46 Staging run builds from lookalike repo Berri-AI/litellm
11:39–11:40 20:39–20:40 Seven malicious 1.107.0 wheels uploaded to PyPI
11:46 20:46 Malicious Docker images published (v1.107.0, latest, main-latest, main-stable)
11:53 20:53 A third developer credential deletes the workflow run records
12:06 21:06 Public report filed: GitHub issue #47826
13:23 22:23 PyPI has removed 1.107.0
13:40 22:40 Attacker's GitHub dead-drop account returns 404 (SafeDep)
13:51 22:51 Affected GHCR tags unavailable
14:00 23:00 SafeDep write-up posted to Hacker News (469 points, 158 comments when checked)
14:20 23:20 All Docker Hub tags removed or repointed to clean images
Timeline chart of the LiteLLM 1.107.0 compromise on October 21, 2026, in UTC and KST

As of October 22, the PyPI JSON API returns 404 for litellm/1.107.0 and lists 1.106.2 as the latest release.

Response flowchart: decide if you are affected, then remove implant, kill daemon, rebuild on 1.106.2, rotate secrets, hunt in logs

Step 1: Check whether you are affected

You are exposed if anything resolved an unpinned litellm during the PyPI window, or if a LiteLLM container started from an affected tag during the Docker window. Remember CI runners, dev laptops, notebooks and agent sandboxes, not only production.

Important: pip show and pip freeze start Python, and on an infected environment that runs the payload again. On a suspect host, search the filesystem first. Use pip only on machines you already believe are clean.

Linux / macOS (no Python needed):

# implant files and the bad dist-info, anywhere on disk
sudo find / -xdev \( -name 'cook.pth' -o -name 'cook.py' -o -name 'litellm-1.107.0.dist-info' \) \
  -path '*-packages*' 2>/dev/null
# sitecustomize.py placed at a site-packages root (check content before deleting)
sudo find / -xdev -path '*site-packages/sitecustomize.py' 2>/dev/null \
  -exec grep -l -E 'remote_code_loader|letters9813|privcollect' {} +
# markers left by the payload
ls -la /tmp/.pc_once_72129118c59f5636c0bb /tmp/remote_code_loader.lock 2>/dev/null

Windows (PowerShell):

Get-ChildItem -Path C:\ -Recurse -Force -ErrorAction SilentlyContinue `
  -Include cook.pth,cook.py,sitecustomize.py,'litellm-1.107.0.dist-info' |
  Where-Object FullName -match 'site-packages' | Select-Object FullName,Length,LastWriteTime

Lockfiles and requirements (on any machine):

grep -rn --include='*.txt' --include='*.lock' --include='*.toml' -E 'litellm[^a-z_-]*1\.107\.0' .
grep -n -A2 'name = "litellm"' uv.lock poetry.lock 2>/dev/null

If a lockfile says 1.107.0, any environment built from it after 11:39 UTC on October 21 is affected. Also check CI logs for Successfully installed ... litellm-1.107.0 or + litellm==1.107.0. Look in package caches too: find ~/.cache/pip ~/.cache/uv ~/Library/Caches -name 'litellm-1.107.0*' 2>/dev/null. Purge any hits so a later offline install can't reuse them.

Docker and Kubernetes:

docker images --digests | grep -i litellm
docker ps -a --format '{{.ID}} {{.Image}} {{.CreatedAt}}' | grep -i litellm
kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}/{.metadata.name}{"\t"}{.status.containerStatuses[*].imageID}{"\n"}{end}' | grep -i litellm

The compromised image index for berriai/litellm on Docker Hub and GHCR is sha256:1bbcb1443bc962e45f5aad53fe3d2c30600e0379715fb6c664cdd3ac126c339e. LiteLLM also lists litellm-database (sha256:c00f9d92c427…) and litellm-non_root (sha256:7b00e91cd2fe…). Any container that ran one of those is compromised. Because latest and main-stable were repointed, a container that pulled them during the window can still be running the bad image today.

For extra confirmation, the planted files on Linux/macOS hash to cook.pth = 7f191632…b92bdb, cook.py = 9dd0a392…90be25, sitecustomize.py = 34bef04f…8305fa (full hashes in OSV). Windows copies hash differently because of CRLF line endings; see SafeDep's IOC table. Finding a file confirms exposure, but LiteLLM warns that not finding one doesn't prove the host is clean, because container restarts and /tmp cleanup erase evidence.

Step 2: Remove the implant (before running Python)

  1. Delete the files from the shell in every affected site-packages: cook.pth, cook.py, sitecustomize.py (only the malicious one, as identified by the grep above), and __pycache__/cook*.pyc / __pycache__/sitecustomize*.pyc.
  2. Kill the daemon. On Linux/macOS: sudo lsof /tmp/remote_code_loader.lock (or fuser -v) shows the PID. It is a detached python process with parent PID 1, its own session, and working directory /. On Windows, look for unexplained python.exe processes.
  3. Rebuild instead of patching. LiteLLM's guidance is to reinstall pinned to litellm==1.106.2, but recreating the venv or image from scratch is better. The second-stage loader could have dropped anything.
rm -rf .venv && python -m venv .venv && . .venv/bin/activate
pip install 'litellm==1.106.2'
# uv
uv add 'litellm==1.106.2' && uv lock && uv sync
# poetry
poetry add 'litellm==1.106.2'

For containers, deploy a clean tag by digest (for example v1.106.2) and destroy the old containers, volumes and node caches. Don't restart them.

Safe versions: 1.106.2 and earlier explicit releases are clean, and so are 1.107.0rc1/rc2. LiteLLM says "all previous versions are safe" (the March releases 1.82.7/1.82.8 were removed and were malicious too). The companion packages litellm-proxy-extras==0.4.110 and litellm-enterprise==0.1.78 were checked clean by SafeDep.

Step 3: Rotate credentials

If 1.107.0 ever ran on a host with outbound internet, LiteLLM says to assume exfiltration succeeded. Rotate from a clean machine, starting with what's most valuable:

  • LLM provider keys: OpenAI, Anthropic, Azure OpenAI, Gemini/Vertex, Bedrock, Cohere, Mistral, OpenRouter, Hugging Face. Revoke them and issue new ones; don't just add new keys alongside the old ones.
  • LiteLLM proxy: every provider key stored in the proxy DB, the master key, all virtual keys, and the DATABASE_URL password.
  • AWS: deactivate access keys in ~/.aws. For instance and task roles, use IAM's Revoke active sessions and replace the role credentials.
  • GCP: rotate service-account keys and revoke ADC (gcloud auth revoke, gcloud auth application-default revoke).
  • Azure: revoke CLI sessions and refresh tokens; rotate service-principal secrets.
  • Kubernetes: rotate service-account tokens and kubeconfig credentials, and any cluster secrets mounted into affected pods.
  • GitHub and Git: revoke gh OAuth tokens and PATs, .git-credentials, deploy keys, and SSH keys. Then audit org audit logs for new tokens and workflow changes.
  • Registries and CI: .pypirc, .npmrc, Docker Hub/GHCR tokens, Vault tokens, Terraform Cloud tokens, GitLab runner and Jenkins secrets.
  • .env files and process environments: every secret in the environment of any process on the host, not just your app.
  • People: browser-saved passwords and sessions on affected dev laptops, plus AI coding tool logins (Claude Code, Cursor, Codex and others).

Step 4: Check logs for exfiltration and follow-on access

  • DNS, proxy and firewall logs: look for www.litellm[.]dev (POST to /healthcheck), raw.githubusercontent.com/letters9813/..., and any GET /codepy to an unknown host. The X-Token header above is a strong signal if you capture HTTP.
  • AWS CloudTrail: GetCallerIdentity from the instance role around install time, then any use of that role's credentials from IPs outside your infrastructure (LiteLLM's own recommendation).
  • Metadata calls: Python processes hitting 169.254.169.254 or 169.254.170.2 when they normally don't.
  • Provider dashboards: unusual usage spikes or new keys at OpenAI, Anthropic and others; new IAM users or keys; new GitHub tokens, deploy keys, or workflow edits.
  • Host: the /tmp/.pc_once_… marker, the lock file, the env var PRIVCOLLECT_ACTIVE=1 in process environments, and unexplained cron or launch agents (second-stage code isn't documented, so look broadly).

Step 5: Prevent the next one

  • Pin exact versions with hashes. Generate hashes with pip-compile --generate-hashes or uv pip compile --generate-hashes, then install with pip install --require-hashes -r requirements.txt. Commit uv.lock/poetry.lock and install with uv sync --locked / poetry sync. Hash pinning won't stop you from choosing a bad new version, but it does stop silent drift to one.
  • Add a release cooldown. Most malicious releases are caught within hours. uv's exclude-newer setting ignores packages uploaded after a cutoff, and Dependabot/Renovate can wait several days before proposing upgrades. This one was reported publicly about 30 minutes after upload and pulled within two hours.
  • Scan continuously. pip-audit -s osv and OSV-Scanner check your lockfiles against OSV, which lists 1.107.0 under MAL-2026-2144. Run them in CI and on a schedule, not only on PRs.
  • Pin containers by digest, never latest or main-stable. Note that the bad images here were signed by the real pipeline, so a valid signature alone wasn't enough.
  • Trusted Publishing isn't a silver bullet. LiteLLM already used it. The attacker abused the publisher workflow itself, so watch for unexpected releases of packages you depend on.
  • Limit what Python can reach. Run proxies with least-privilege roles, IMDSv2 with hop limit 1, egress allow-lists, and no long-lived keys on disk. Keep secrets out of logs: LiteLLM traced the root cause to tokens in request logs.

Quick checklist

  • Searched every host, runner and image for cook.pth, cook.py, a malicious sitecustomize.py, and litellm-1.107.0.dist-info
  • Grepped lockfiles, CI logs and pip/uv caches for 1.107.0
  • Checked running containers and pods for digest sha256:1bbcb1443bc9…
  • Deleted implant files and killed the daemon before running Python
  • Rebuilt venvs and images on litellm==1.106.2, pinned with hashes
  • Rotated LLM keys, proxy DB/master/virtual keys, cloud roles and keys, K8s, GitHub, SSH, registry and .env secrets
  • Reviewed DNS/egress logs for www.litellm[.]dev and CloudTrail for GetCallerIdentity
  • Added hash pinning, a release cooldown, and OSV scanning to CI

FAQ

I only use import litellm occasionally. Am I safe if I never imported it? No. The .pth and sitecustomize.py files run when any Python process starts in that environment, including pip itself.

I'm on 1.106.x or older. Do I need to do anything? Make sure the version is pinned so nothing upgrades you unexpectedly, and confirm no CI job or image resolved 1.107.0 on October 21. No other versions from this incident are reported as affected.

Is the GitHub source for v1.107.0 compromised? No. LiteLLM, SafeDep and OSV all say the official tag is clean. The artifacts were built from the lookalike Berri-AI/litellm repo.

Was 1.107.0 "yanked"? It was removed. PyPI no longer serves the release at all, so a pin to it will now fail to resolve.

Is the attacker's server still active? The GitHub dead drop was gone by 13:40 UTC, so new infections can't find a command server. The exfil domain and any second stage are still not fully characterized, so assume stolen secrets are in the attacker's hands.

Were enterprise customers affected? LiteLLM says customers pulling from its private enterprise registry weren't affected.

What's still unverified

LiteLLM's report says the investigation is ongoing. As of this writing, how the attacker first got into the internal deployment, the second-stage server address and payload, and the number of affected installs haven't been publicly confirmed.

Sources

This guide reflects public reports as of October 22, 2026; follow LiteLLM's incident page for updates.

#litellm#pypi#supply-chain-attack#python#security#credential-stealer#2026

← Back to all posts