LiteLLM Supply Chain Attack. Find It. Rebuild. Rotate.
LiteLLM 1.107.0 on PyPI and Docker shipped a credential stealer and a remote code loader. Clear steps to find every affected host, and a reactive hunt to scope the damage across your estate or your clients'.
At a Glance
Facts from BerriAI's incident updates, PyPI and independent researcher analysis. Last updated 22 October 2026, 00:00 BST.
Contained
PyPI quarantined and removed 1.107.0, and BerriAI removed the infected Docker images on 21 October. The current release on PyPI is 1.106.2.
Full compromise
No CVE. Any host that installed 1.107.0 should be treated as compromised. The stealer runs on every Python start, with no import needed.
21 Oct, 12:39 to 15:20 BST
PyPI wheels were live from 12:39 to 14:23 BST. Docker tags v1.107.0, latest, main-latest and main-stable were affected until about 15:20 BST. Earlier versions are clean.
Pin 1.106.2
Pin 1.106.2 by hash or image digest. Rebuild affected hosts, then rotate every credential they could reach from a clean machine.
Lookalike domain litellm[.]dev and the dead drop GitHub account are registered.
All seven malicious 1.107.0 wheels are uploaded to PyPI.
Infected images are published. Tags v1.107.0, latest, main-latest and main-stable point to them on Docker Hub and GHCR.
PyPI has removed 1.107.0. Unpinned pip installs from 12:39 to 14:23 BST may have pulled it.
The affected GHCR tags are unavailable. Docker Hub tags still point to the infected image.
All affected Docker Hub tags are removed or repointed to clean images. BerriAI says stolen GitHub CLI OAuth tokens from three engineers were used to publish the release.
Find every install, rebuild those hosts, rotate credentials and block the attacker infrastructure.
What to Do in the Next 24 Hours
Proactive on the left. Reactive on the right. Tick both tracks.
Find and pin
- Search lockfiles, requirements, images, CI caches and internal mirrors for litellm 1.107.0.
- Pin litellm==1.106.2 by hash, and LiteLLM images by digest, never by a floating tag.
- Block litellm[.]dev at DNS and egress, and alert on requests to raw.githubusercontent.com/letters9813.
- Purge cached 1.107.0 wheels and images from mirrors, proxies and registries.
Rebuild and rotate
- Check hosts for cook.pth, cook.py and an unexpected sitecustomize.py in site-packages.
- Look for a persistent background Python process holding /tmp/remote_code_loader.lock.
- Search proxy and DNS logs for POSTs to litellm[.]dev/healthcheck and requests to the dead drop.
- Rebuild affected hosts, then rotate cloud, GitHub, LLM provider and CI credentials from a clean machine.
Proactive: Find LiteLLM 1.107.0
Do not start with pip or Python on a suspect host. Any Python start there runs the malware again. Use the shell and your tooling instead.
Search code and lockfiles
Look for the bad version in every repository, including projects that use an open constraint such as litellm>=1.x, which may have resolved to 1.107.0.
grep -rniE "litellm[=<>~! ]*1\.107\.0([^0-9rc]|$)" . --include=requirements*.txt --include=*.lock --include=pyproject.toml --include=Pipfile*Check hosts and virtual environments
Search from the shell, without starting Python.
find / -type d -path "*site-packages/litellm-1.107.0.dist-info" 2>/dev/null
find / -name cook.pth -path "*site-packages*" 2>/dev/null
ls -la /tmp/.pc_once_72129118c59f5636c0bb /tmp/remote_code_loader.lock 2>/dev/nullCheck containers and Kubernetes
Check which image digests your deployments actually pulled on 21 October. A deployment on a floating tag that pulled or restarted that afternoon may have the infected image.
docker images --digests | grep -E "berriai/litellm|litellm/litellm"
kubectl get pods -A -o jsonpath='{range .items[*]}{.status.containerStatuses[*].imageID}{"\n"}{end}' | grep -i litellm
docker exec <container> ls -la /app/.venv/lib/python3.13/site-packages/cook.pthPin 1.106.2 by hash
Pin litellm==1.106.2 with the hashes PyPI publishes for it, so a tampered or replaced file cannot install. The eight hashes cover the manylinux and musllinux x86_64 and aarch64 wheels, the macOS x86_64 and arm64 wheels, the Windows wheel and the source archive. We checked them against PyPI on 21 October 2026.
# requirements.txt: pin the last clean release by hash (values published by PyPI 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
# Install on a clean build host, never on a suspect one
pip install --require-hashes -r requirements.txt
# Docker: pin by digest, not tag
docker pull ghcr.io/berriai/litellm:v1.106.2
docker inspect --format "{{index .RepoDigests 0}}" ghcr.io/berriai/litellm:v1.106.2Use exact pins rather than open ranges such as litellm>=1.x. For Docker, pin v1.106.2 by digest and verify the signature with cosign as the LiteLLM README describes.
Block the infrastructure
Block litellm[.]dev and www[.]litellm[.]dev at your DNS resolver, proxy and egress firewall, and alert on requests to raw.githubusercontent.com/letters9813/. The real LiteLLM domain is litellm.ai. Purge any cached 1.107.0 files from PyPI mirrors, caching proxies and internal registries.
Free Checker Scripts
Tested by the CybaVerse threat hunt team. Run them on developer laptops, CI runners, servers and LiteLLM proxy hosts to get a clear verdict, whether you look after your own business or your clients' estates.
check-litellm-1107.sh
Sweeps every site-packages and dist-packages directory (system Python, venvs, conda, pipx, ~/.local and /app/.venv) for 1.107.0 metadata and the payload files by hash. It checks the marker and lock files, pip and uv caches, lockfiles, Dockerfiles, compose and Helm files, Docker images by tag and digest, running containers, processes holding the loader lock, and DNS and proxy logs for litellm[.]dev.
sudo bash check-litellm-1107.sh /srv/code /homePass the folders that hold your projects and lockfiles. Run as root so it can read every environment, process and log. Use PY_ROOTS="/opt /home" to narrow the site-packages sweep.
Check-LiteLLM-1107.ps1
Runs on Windows PowerShell 5.1 and PowerShell 7. It sweeps site-packages on all fixed drives, checks lockfiles and manifests under the paths you give it, Docker images, Python command lines, the hosts file and the DNS client cache.
powershell -ExecutionPolicy Bypass -File .\Check-LiteLLM-1107.ps1 -ScanPath C:\src,C:\UsersUnzip the download first, then run from an elevated PowerShell. Use -PyRoot to narrow a slow sweep.
Read-only by design
Both scripts read files and process information only. They never start Python or pip on the host, because that would run the malware again. They read package METADATA and RECORD files directly instead. The only other commands they run are read commands such as docker images, docker image inspect, a docker exec directory listing inside running containers (Linux script), and journalctl. Nothing is written, deleted or killed. Review any script before you run it.
Verdicts
- CLEANNothing found. A clean result is not proof, because containers and /tmp cleanup remove markers.
- COMPROMISED1.107.0 is installed, payload files, a marker or lock file, a loader process, a matching image, or DNS or log traces of litellm[.]dev were found.
- UNKNOWNNeeds review: for example a cook.py with a different hash, a floating LiteLLM image built on 21 October, or a suspicious Python command line.
- REFERENCED_ONLY1.107.0 is referenced in a lockfile, manifest or cache but is not installed on this host. Check every machine or CI job that used those files.
Exit codes
| Code | Meaning |
|---|---|
| 0 | CLEAN |
| 1 | COMPROMISED |
| 2 | UNKNOWN, needs review |
| 3 | REFERENCED_ONLY |
MSPs and internal IT teams can use the exit code in RMM or deployment tooling to flag hosts at scale.
Know the limits
- Only the prefixes of the litellm-database and litellm-non_root image digests have been published, so the scripts match on those prefixes.
- Images that are not running are not unpacked, because that would mean creating containers. A floating-tag image is flagged UNKNOWN by its build date only.
- On Windows the loader can't fork and the /tmp marker doesn't exist, so marker checks rarely fire, and process environment variables can't be read. Linux DNS caches are not inspected, so use resolver, proxy or EDR logs.
- Removed hosts, short-lived CI runners and rebuilt containers leave no local traces. Check CI logs and lockfile history for installs from 12:39 to 14:23 BST on 21 October, and image pulls from 12:46 to about 15:20 BST.
Reactive: How to Hunt
Assume any host that installed 1.107.0 sent its secrets to the attacker. Hunt to scope the damage, then rebuild and rotate. CybaOps can run these hunts across your estate or your clients'.
Payload files and markers
The malware adds three files to the root of site-packages. Any of them, or the marker files, means the host ran the payload. A missing marker does not clear a host.
find / \( -name cook.pth -o -name cook.py -o -name sitecustomize.py \) -path "*site-packages*" -ls 2>/dev/null
ls -la /tmp/.pc_once_* /tmp/remote_code_loader.lock /tmp/.pc_sq_* /tmp/.pc_e_* 2>/dev/nullPersistent background Python process
On Linux and macOS a detached Python daemon polls the dead drop every hour. Its parent is PID 1 and its working directory is /. Kill it before cleanup.
lsof /tmp/remote_code_loader.lock 2>/dev/null
fuser -v /tmp/remote_code_loader.lock 2>/dev/null
ps -eo pid,ppid,etime,cmd | awk '$2==1 && /python/' Network activity
Search DNS, proxy, firewall and CI egress logs from 21 October onwards. Any call to the exfiltration domain is strong evidence the stealer ran. Any request to a /codepy path means attacker code ran, and the scope is larger.
# Adjust log paths to your proxy or DNS logs
grep -Ei "litellm\.dev|letters9813|/codepy" /var/log/squid/access.log /var/log/syslog 2>/dev/null | tail -100Rebuild and rotate
Rebuild affected hosts and containers from clean images, then rotate credentials from a clean machine: cloud keys and instance roles, GitHub, PyPI and npm tokens, SSH and GPG keys, Kubernetes and Vault tokens, CI secrets, AI coding tool tokens, browser passwords and every LLM provider API key. On LiteLLM proxy hosts, rotate every key stored in the proxy database, the master key and the database password. Check cloud logs for use of those credentials from outside your infrastructure.
Tested detection pack: KQL and Sigma
IOC and behaviour hunts from the CybaVerse threat hunt team, built from the confirmed indicators below: payload files written to site-packages, the marker and lock files, calls to the exfiltration domain and dead drop, and the detached loader process. Any hit on litellm[.]dev or a /codepy path is strong evidence. Run them in CybaOps or any SIEM that supports KQL or Sigma.
The KQL queries are built for Microsoft Sentinel and Defender XDR. Test them in your environment first. Search from 21 October 2026 onwards.
KQL hunting queries (Sentinel and Defender XDR)
Loading queries. If they don't appear, use the download link below.Sigma: File event: LiteLLM 1.107.0 payload files written to site-packages
title: LiteLLM 1.107.0 Payload Files Written to Python site-packages
id: 0d9b6c2e-3a41-4f7e-8b15-6e2c9d4a1f70
status: experimental
description: cook.pth / cook.py written into site-packages or dist-packages, or the privcollect marker / loader lock created (LiteLLM 1.107.0 supply-chain malware).
references:
- https://docs.litellm.ai/blog/security-incident-oct-21-2026
author: Cybaverse
date: 2026-10-21
tags:
- attack.persistence
- attack.t1546.018
- attack.t1195.001
logsource:
category: file_event
product: linux
detection:
selection_pth:
TargetFilename|contains:
- '/site-packages/'
- '/dist-packages/'
TargetFilename|endswith:
- '/cook.pth'
- '/cook.py'
selection_markers:
TargetFilename:
- '/tmp/.pc_once_72129118c59f5636c0bb'
- '/tmp/remote_code_loader.lock'
condition: 1 of selection_*
falsepositives:
- Unlikely; no legitimate package is known to install a top-level cook.pth
level: criticalSigma: Proxy: exfiltration, dead drop access or second stage
title: LiteLLM 1.107.0 Stealer Exfiltration or Dead-Drop Access
id: 6e3f1a8b-9c27-4d50-a2e4-1b7d8f0c5e93
status: experimental
description: Proxy request to the privcollect exfiltration endpoint (http://www.litellm.dev/healthcheck), the GitHub dead drop read by sitecustomize.py, or a /codepy second-stage fetch. litellm.ai is the legitimate project domain and is not matched.
author: Cybaverse
date: 2026-10-21
tags:
- attack.exfiltration
- attack.t1041
- attack.command-and-control
- attack.t1102.001
logsource:
category: proxy
detection:
selection_exfil:
cs-host|endswith: 'litellm.dev'
selection_deaddrop:
c-uri|contains: '/letters9813/pythonclient-test/'
selection_token:
cs-headers|contains: 'XWiA8gZNa7NcX2XRoAEb'
selection_stage2:
c-uri|endswith: '/codepy'
condition: 1 of selection_*
falsepositives:
- Security researchers or sinkhole testing
level: criticalSigma: DNS: query for the exfiltration domain
title: DNS Query for LiteLLM 1.107.0 Exfiltration Domain
id: 2b8c4d7e-5f19-4a63-9e01-7c3a6b2d8f14
status: experimental
description: DNS lookup of litellm.dev / www.litellm.dev (lookalike registered 13 Oct 2026, used by the privcollect stealer). Does not match the legitimate litellm.ai.
author: Cybaverse
date: 2026-10-21
tags:
- attack.exfiltration
- attack.t1041
logsource:
category: dns
detection:
selection:
query|endswith: 'litellm.dev'
condition: selection
falsepositives:
- Researchers or blocklist validation
level: highIndicators of compromise
Confirmed by LiteLLM's incident post (image digests), PyPI and independent analysis by security researchers. Only the prefixes of the litellm-database and litellm-non_root digests have been published.
# Defanged. Re-fang only inside your security tooling.
# Network
hxxp://www[.]litellm[.]dev/healthcheck (exfiltration, HTTP POST)
litellm[.]dev, www[.]litellm[.]dev (lookalike domain, registered 13 Oct 2026)
hxxps://raw[.]githubusercontent[.]com/letters9813/pythonclient-test/main/domain.txt (hourly dead drop)
hxxp://<domain from dead drop>/codepy (second stage; any request means remote code ran)
HTTP header X-Token: XWiA8gZNa7NcX2XRoAEb
User-Agent Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 10.0; WOW64; Trident/7.0)
Historic dead drop values: localhost:18909, test.test.com:18909
# Package
litellm==1.107.0 (PyPI, all wheels). Wheel SHA-256 (litellm-1.107.0-cp310-abi3-...):
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 2befe2a79fb4fbbd9a543b5b40b530c8cffa8771bab22b8fb22b2e4ae3b1f819 (sdist, tampered but no payload)
# Docker (tags v1.107.0, latest, main-latest, main-stable while they pointed here)
ghcr[.]io/berriai/litellm:v1.107.0, litellm-database:v1.107.0, litellm-non_root:v1.107.0, docker[.]io/litellm/litellm:v1.107.0
image index sha256:1bbcb1443bc962e45f5aad53fe3d2c30600e0379715fb6c664cdd3ac126c339e (litellm, ghcr and Docker Hub)
amd64 manifest sha256:bd1aeee419ea36da9aa2b657288da4101887be468f020ac63d22ba6ed84a3ea5
arm64 manifest sha256:56b74573eed6e4aafe4b50603dbe7853e0345f188c8e44a09f1098caf1f4bcdf
litellm-database index sha256:c00f9d92c427... (prefix only published)
litellm-non_root index sha256:7b00e91cd2fe... (prefix only published)
# Source
github[.]com/Berri-AI/litellm (lookalike repo), commit 20535d8fa11f786a6c24df7488a0e449ed5f09ad
# Host
# Host files in site-packages (SHA-256, Linux/macOS then Windows CRLF copy)
cook.pth 7f191632e7320705d154b85204f1ae546b543acbd7e8861229b76f5042b92bdb
84dc8c82c0a4a4a62552ef826b49fcc86d1a5c4a5d0f2c06665723f93d129d7d
cook.py 9dd0a3926f96a37eb86cf1ee2310d9be93139cc4ebef3ef9e2efe9531390be25
03ac2a8b752605d1d7710979c17ff18b03dd978203ba23ba97fd8ce3d6f897ea
sitecustomize.py 34bef04fe88ac9d0aae0475630b1738681d15b94f233ac8aa46eacf5968305fa
b6122440d0d9cb1eb1ade9933f49214da8c31288de7710c076c2680dfa528465
/tmp/.pc_once_72129118c59f5636c0bb (written only after successful exfiltration)
/tmp/remote_code_loader.lock (held by the background loader)
/tmp/.pc_sq_* /tmp/.pc_e_* (temporary directories)
PRIVCOLLECT_ACTIVE=1 (environment variable)
Detached python process, parent PID 1, working directory /LiteLLM 1.107.0 FAQ
Short answers based on BerriAI's incident updates and researcher analysis as of 21 October 2026.
What happened to LiteLLM?
On 21 October 2026, LiteLLM version 1.107.0 was published to PyPI and as Docker images with malware added. LiteLLM is a widely used Python SDK and proxy for calling many AI model providers. The malicious release was built from a lookalike GitHub repository, not from the real LiteLLM code.
What does the malware do?
A file called cook.pth runs at every Python start and launches a stealer that names itself privcollect. It takes cloud credentials, SSH and GPG keys, browser data, AI coding tool credentials, environment variables and LiteLLM proxy database keys, then sends them encrypted to a lookalike domain. A second file starts a background process that checks an attacker-controlled GitHub file every hour and can download and run more code.
Which versions are affected?
Only 1.107.0: all of its PyPI wheels and the v1.107.0 Docker images, plus the latest, main-latest and main-stable tags while they pointed to it on 21 October. Earlier releases, including 1.106.2, are clean.
When was the exposure window?
On 21 October 2026 the malicious PyPI wheels were live from 12:39 to 14:23 BST (11:39 to 13:23 UTC). The Docker tags v1.107.0, latest, main-latest and main-stable pointed to the infected image from 12:46 BST until about 15:20 BST on Docker Hub, and until 14:51 BST on GHCR. Check CI logs, lockfile history and image pulls in those windows.
How did the attacker publish it?
BerriAI says the release was published through three engineers' GitHub accounts using stolen GitHub CLI OAuth tokens, which have been revoked. Its investigation is ongoing.
Is uninstalling enough?
No. Treat any host that installed 1.107.0 as compromised. Rebuild it from a clean image, then rotate every credential it could reach from a separate clean machine. Do not run pip or Python on a suspect host before removing the payload, because that runs the malware again.
Is there a CVE?
No. This is a malicious release, not a software flaw, so there is no CVE. It is tracked in BerriAI's public incident report and in malicious package databases.
What should MSPs do first?
Search every client environment for 1.107.0 in lockfiles, images and CI caches, block the lookalike domain at egress, and prioritise developer laptops, CI runners and LiteLLM proxy hosts, which hold the most valuable credentials.
Detect and Remediate the LiteLLM Attack Across Your Estate
CybaOps finds compromised packages and exposed credentials across every system you look after, whether that's your own business or your clients'. It runs threat hunts through our SOC and drives remediation through to a verified fix, so proactive remediation and reactive hunting happen in one place.