LiteLLM 1.107.0 compromised on PyPI and Docker: what to do

Convika Team30 min read

  • security
  • python
  • supply-chain
  • litellm
On this page

As of October 24, 2026, 06:27 JST (October 23, 21:27 UTC). Every fact that can change carries the time it was checked, in JST (UTC+9). This page covers the LiteLLM compromise of October 21, 2026 only, not the one of March 2026.

litellm 1.107.0 on PyPI (October 21, 2026, 11:39–13:23 UTC) and LiteLLM's Docker tags v1.107.0, latest, main-latest, and main-stable (11:46–14:20 UTC) held a credential stealer. If you installed it then or ran an image pulled then, treat the host as compromised, rebuild on 1.106.2, and rotate every key it could reach.

Don't run pip install first: remove the implant from the shell before any Python starts, then reinstall pinned to litellm==1.106.2. LiteLLM's incident report says it plainly: "Any Python start in the affected environment runs cook.pth and sitecustomize.py again, and that includes running pip." The same files also start a loader that can fetch and run more code. In JST, the PyPI window is 20:39 to 22:23 and the Docker window is 20:46 to 23:20 on October 21.

As of 06:11–06:27 JST on October 24: PyPI no longer serves 1.107.0 and lists 1.106.2 as the latest release; latest, main-latest, and main-stable on Docker Hub and GHCR resolve to the 1.106.2 images. LiteLLM announced its completed investigation, dated October 23, in its GitHub issue at 21:15 UTC that day; it replaces its first account of how the attacker got in, and plans a new release. There is no CVE. GitHub's Advisory Database lists 1.107.0 in the malware advisory GHSA-92x9-889m-jgmw (updated October 22, 00:32 UTC), and the OSV database in MAL-2026-2144, the same records as the March versions; LiteLLM has published no advisory of its own.

Below: a 2-minute check for each way LiteLLM gets onto a machine, what the malware does, the cleanup steps in order, the credentials to rotate by type, which versions are clean, the Docker and Kubernetes details, and a timeline in UTC and JST.

Am I affected? The 2-minute check by install path

Two rules before you start. Every check below runs from the shell and starts no Python, so it can't set the payload off again. And a hit confirms exposure, but a miss doesn't clear you: LiteLLM's report says that not finding the files "does not prove the environment is clean".

How LiteLLM got onto the machineYou're exposed ifCheck (shell only)
pip install litellm or litellm[proxy], with no pin or with -U, into a venv or a system PythonThe install ran between 11:39 and 13:23 UTC on October 21Look for litellm-1.107.0.dist-info and cook.pth in site-packages (commands)
A requirements.txt with litellm>=, ~=, or no versionAn install from it ran in that windowThe same, and your build or CI log for litellm-1.107.0
uv: uv add, uv lock --upgrade, uv sync, uv pip install, uvxIt resolved litellm in that windowThe version line under name = "litellm" in uv.lock; for uvx, search ~/.cache/uv for cook.pth
Poetry: poetry add, poetry update, poetry lockIt resolved litellm in that windowThe version line under name = "litellm" in poetry.lock, then the venv's site-packages
pipx: pipx install 'litellm[proxy]' or pipx upgradeThe install or upgrade ran in that windowSearch your home directory for cook.pth
conda or mamba from conda-forgeNot through conda: conda-forge's newest litellm is 1.106.0, with no 1.107.0 (checked 05:39 JST). A pip install inside a conda env is the first rowSearch $CONDA_PREFIX for cook.pth
A package that depends on litellmIts version range allows 1.107.0 and the install ran in that window. In their newest PyPI releases (checked 05:39 JST), dspy asks for >=1.65.8, smolagents' litellm extra >=1.60.2, langchain-litellm >=1.101.0,<2.0.0, and crewai's litellm extra >=1.88.6,<2; aider-chat pins ==1.81.10 and openhands-ai ==1.84.1The first row's check
A lockfile, Dependabot, or Renovate change that moved litellm to 1.107.0Any machine installed from it: CI on the pull request, a teammate's laptop, a deploy. After 13:23 UTC only a cache could still serve the filesgit log --all -S 1.107.0 over your lockfiles (commands)
Docker: litellm/litellm, ghcr.io/berriai/litellm, litellm-database, or litellm-non_root at v1.107.0, latest, main-latest, or main-stableA container started from an image pulled between 11:46 and 14:20 UTC. The GHCR tags were gone by 13:51 UTCCompare docker images --digests with the bad digests
Your own image built FROM one of those tags, or with an unpinned pip install litellmIt was built in that window. A RUN step on top of the bad image starts PythonList the image's files without starting it (commands)
Kubernetes, including the litellm-helm chartA pod started in the window from one of those tags, or from an image pulled then. The published chart runs the image tagged with its own version by default: chart 1.106.2 runs ghcr.io/berriai/litellm:1.106.2, the clean digest (checked 06:10 JST). A 1.107.0 chart and an unprefixed 1.107.0 image tag don't exist now; whether they existed in the window is not statedhelm history <release> for upgrades on October 21, pod image IDs with kubectl, and crictl images --digests on each node
A registry mirror or pull-through cache (Harbor, Artifactory, a Docker mirror)It synced one of the four tags in the window. Your copy keeps the bad image after LiteLLM repointed the tagLook up the tag's digest in your mirror
CI runners: GitHub Actions, GitLab CI, CircleCI, BuildkiteA job installed or ran LiteLLM in the window, or later restored a cache made thenThe job's log for litellm-1.107.0; delete pip and uv caches saved that day
Platform builds: Railway, Render, Fly.io, Heroku, Vercel's Python runtimeA build in the window resolved litellm without a pinThe build log for litellm-1.107.0. That deploy ran the payload with the service's environment variables
LiteLLM Enterprise, from LiteLLM's private registryNot affected, per LiteLLM: "Our private registry is not affected"Nothing to check
Built from the GitHub v1.107.0 source tagLiteLLM: the official tag "does not contain the malicious files"Nothing to check for the tag itself
The pre-releases 1.107.0rc1 and 1.107.0rc2Clean, per Upwind's and SafeDep's analyses. A plain pip install doesn't pick pre-releases without --preNothing to check

The shell check, step by step

On Linux or macOS, search for the planted files and the dist-info folder. On a large disk, replace / with the places your Python environments live: your home directory, /usr, /opt, and your projects.

# 1. The planted files and the 1.107.0 install record
find / \( -path /proc -o -path /sys \) -prune -o \( -name cook.pth -o -name 'litellm-1.107.0.dist-info' \) -print 2>/dev/null
# sitecustomize.py has legitimate uses; the planted one is 25,509 bytes in site-packages
# (dist-packages for Debian and Ubuntu's system Python)
find / \( -path /proc -o -path /sys \) -prune -o \( -path '*site-packages*' -o -path '*dist-packages*' \) -name sitecustomize.py -size 25509c -print 2>/dev/null

# 2. The markers the payload leaves
ls -la /tmp/.pc_once_72129118c59f5636c0bb /tmp/remote_code_loader.lock 2>/dev/null

# 3. The background daemon: the process holding the lock (Linux and macOS)
#    sudo, so lsof also shows processes of other users, such as a service account
sudo lsof /tmp/remote_code_loader.lock 2>/dev/null

If you have already run pip install litellm==1.106.2 over 1.107.0, the 1.107.0 dist-info folder may be gone. Steps 2 and 3 still apply, because the daemon and the markers live outside site-packages. On Linux, sudo fuser -v /tmp/remote_code_loader.lock also names the daemon. A container has its own /tmp, so these checks on the host don't see inside it: for a container, compare its image digest and rebuild it.

Check what your lockfiles have held, including in old commits and pull requests, in every folder of the repository:

grep -A1 '^name = "litellm"$' uv.lock poetry.lock 2>/dev/null
grep -n -i -E '^litellm(\[[^]]*\])? *==' requirements*.txt 2>/dev/null
git log --all --oneline -S '1.107.0' -- ':(glob)**/uv.lock' ':(glob)**/poetry.lock' ':(glob)**/Pipfile.lock' ':(glob)**/requirements*.txt'

On Windows, in PowerShell. Upwind reports that on Windows the daemon fails to start, but cook.py still runs; there, cook.pth is 13 bytes, because of the CRLF line ending.

Get-ChildItem -Path $env:USERPROFILE, $env:LOCALAPPDATA, $env:ProgramData, 'C:\Program Files', 'C:\Python*' -Recurse -Force -ErrorAction SilentlyContinue -Include cook.pth, 'litellm-1.107.0.dist-info'

For Docker, list local images with their digests and compare them with the digest table. To look inside an image you built without starting it, create a container and list its files; docker create doesn't run anything.

docker images --digests | grep -i litellm
docker ps -a | grep -i litellm
docker export "$(docker create your-image:tag)" | tar -t | grep -E 'cook\.pth|litellm-1\.107\.0'

For Kubernetes, list your Helm release's upgrades, the image IDs running pods report, and the images each node still holds:

helm history <release> -n <namespace>   # an upgrade dated October 21, between 11:46 and 14:20 UTC?
kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}/{.metadata.name}{"\t"}{range .status.containerStatuses[*]}{.imageID}{" "}{end}{"\n"}{end}' \
  | grep -E '1bbcb1443bc9|c00f9d92c427|7b00e91cd2fe|bd1aeee419ea|56b74573eed6|24581ddc3076|827dd893c74c|645a67d81062|f75c8edae59b'
crictl images --digests | grep -i litellm   # on each node

For GitHub Actions, list the runs of October 21 and search their logs:

gh run list --repo OWNER/REPO --created 2026-10-21 --limit 200
gh run view RUN_ID --repo OWNER/REPO --log | grep -m1 -E 'litellm-1\.107\.0|litellm==1\.107\.0'

What a result means

  • litellm-1.107.0.dist-info, cook.pth, or the 25,509-byte sitecustomize.py in site-packages: the implant is installed. Go to the cleanup steps.
  • /tmp/.pc_once_72129118c59f5636c0bb: the stealer sent your data. Upwind and SafeDep both report that it writes this file only after a successful upload.
  • /tmp/remote_code_loader.lock: the hourly daemon ran. If sudo lsof names a process, it is still running.
  • A hit under /var/lib/docker or /var/lib/containerd is a stored image or container layer. An image that never started is inert; use the Docker commands below to see whether a container ran from it.
  • Nothing found on a host that installed or ran LiteLLM in the window: LiteLLM's advice still applies. "If 1.107.0 was ever installed on a host with outbound internet, assume the exfil succeeded." Containers and /tmp cleanup remove the marker.

What the malware does

Each of the seven 1.107.0 wheels (Linux, macOS, and Windows) put three extra files at the root of site-packages, per the analysis Upwind posted in LiteLLM's issue and SafeDep's write-up. Python runs two of them whenever it starts, so they run in every Python process that uses that environment, the venv or interpreter 1.107.0 was installed into, even one that never imports litellm: your app, pip, an IDE's language server, a CI job.

  • cook.pth holds one line, import cook. Python runs that line at startup, which loads cook.py.
  • cook.py is a stealer that calls itself privcollect. Per SafeDep's module table and Upwind's decode, it collects SSH keys; AWS, Azure, gcloud, Kubernetes, and Docker credentials; GitHub CLI and .git-credentials tokens; .npmrc and .pypirc; .env files and private keys; browser passwords and cookies; shell histories; the tokens and configs of AI coding tools (Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Copilot, Windsurf, Aider, Continue, Goose, opencode, Qwen, Crush, Kiro, Zed, VS Code's mcp.json for MCP servers, Hugging Face, and, in Upwind's decode, the VS Code extensions Cline, Roo Code, Cody, and Claude Code, plus Amp, Factory, Trae, and Ollama); the environment variables of every process it can read through /proc/*/environ; and cloud credentials from the instance metadata service.
  • If DATABASE_URL is set, it dumps the LiteLLM proxy's LiteLLM_CredentialsTable and LiteLLM_ProxyModelTable, the tables that hold every upstream provider key the proxy stores.
  • It encrypts what it collects (AES-256-CBC, with the key wrapped in RSA-4096) and posts it to http://www.litellm.dev/healthcheck. That domain is not LiteLLM's: the project's own is litellm.ai.
  • sitecustomize.py starts a detached daemon that holds /tmp/remote_code_loader.lock. Every hour it reads a file in the GitHub repository letters9813/pythonclient-test, and if the file holds a domain, runs the stealer again and downloads and executes http://<domain>/codepy. Upwind found the file empty since October 20, 09:15 UTC; SafeDep saw the account return 404 at 13:40 UTC on October 21, and Wiz says GitHub took the repository down. SafeDep adds that hosts whose daemon ran before then "could have received and run second-stage code", and that it could not recover a server address.

The stolen credentials were used. Wiz reports that between 16:20 and 17:01 UTC on October 21, about 4.5 hours after exfiltration, an attacker validated AWS instance-role credentials "harvested from compromised CI runners and Kubernetes nodes" with sts:GetCallerIdentity, followed by bedrock:ListFoundationModels and a ListModels call, "consistent with LLMjacking": using someone else's cloud account to run AI models. In a few environments it also listed IAM roles, users, and access keys. Wiz names two source IP addresses in its post. Wiz also puts downloads of 1.107.0 at about 49,000, from ClickPy data (Wiz's figure, updated October 22).

Indicators of compromise

TypeValueSource
Files in site-packagescook.pth (12 bytes; 13 on Windows), cook.py (22,252 bytes), sitecustomize.py (25,509 bytes), as the wheels' records give them for Linux and macOSUpwind, in LiteLLM's issue
SHA-256, Linux and macOScook.pth 7f191632e7320705d154b85204f1ae546b543acbd7e8861229b76f5042b92bdb; cook.py 9dd0a3926f96a37eb86cf1ee2310d9be93139cc4ebef3ef9e2efe9531390be25; sitecustomize.py 34bef04fe88ac9d0aae0475630b1738681d15b94f233ac8aa46eacf5968305faSafeDep, Wiz
SHA-256, Windows (CRLF line endings)cook.pth 84dc8c82c0a4a4a62552ef826b49fcc86d1a5c4a5d0f2c06665723f93d129d7d; cook.py 03ac2a8b752605d1d7710979c17ff18b03dd978203ba23ba97fd8ce3d6f897ea; sitecustomize.py b6122440d0d9cb1eb1ade9933f49214da8c31288de7710c076c2680dfa528465SafeDep, Wiz
Host markers/tmp/.pc_once_72129118c59f5636c0bb, /tmp/remote_code_loader.lock, temp folders /tmp/.pc_sq_* and /tmp/.pc_e_*, the environment variable PRIVCOLLECT_ACTIVE=1 inside infected Python processes, a detached Python process in its own session with working directory /Upwind, SafeDep
NetworkPOST http://www.litellm.dev/healthcheck with the header X-Token: XWiA8gZNa7NcX2XRoAEb; raw.githubusercontent.com/letters9813/pythonclient-test/main/domain.txt; Python processes calling 169.254.169.254Upwind, SafeDep
Cloud logssts:GetCallerIdentity, then bedrock:ListFoundationModels, from outside your networkWiz
GitCommits from Berri-AI <ckdi82fkoos@proton.me> in the lookalike repository github.com/Berri-AI/litellm, and a last one, 20535d8fa11f786a6c24df7488a0e449ed5f09ad, from Berri-AI <Berri-AI@users.noreply.github.com>LiteLLM, Upwind, Wiz

Clean up a host that ran litellm 1.107.0

The order comes from LiteLLM's report, whose remediation steps credit the researcher who reported the incident. Steps 1 to 3 happen in the shell, before any Python starts.

1. Don't start Python in the affected environment

No pip install, no pip show, no python -c, no notebook kernel, and no editor opening the project, since a language server starts Python too. Every Python start runs cook.pth and sitecustomize.py again. LiteLLM's report suggests pip show litellm as a check, but the same report says any Python start re-runs the payload, so use the shell check instead.

2. Delete the planted files from every affected site-packages

Set SP to each site-packages folder the check found, then remove the three files and their compiled copies: rm -f "$SP/cook.pth" "$SP/cook.py" "$SP/sitecustomize.py" "$SP"/__pycache__/cook*.pyc "$SP"/__pycache__/sitecustomize*.pyc. On Windows, in PowerShell: Remove-Item "$SP\cook.pth", "$SP\cook.py", "$SP\sitecustomize.py", "$SP\__pycache__\cook*.pyc", "$SP\__pycache__\sitecustomize*.pyc" -Force -ErrorAction SilentlyContinue.

3. Kill the daemon

Find the process holding the lock with sudo lsof /tmp/remote_code_loader.lock (or sudo fuser -v /tmp/remote_code_loader.lock on Linux) and stop it with kill. Run the lsof line again: it should name no process.

4. Rebuild from scratch on 1.106.2

LiteLLM's report: "Reinstall pinned to litellm==1.106.2. Rebuilding images and venvs from scratch is better." Delete the venv and recreate it with litellm==1.106.2. For Docker, pin the 1.106.2 image by digest, such as litellm/litellm@sha256:7294b90f868c5c38c99b83021c7ba46f9b439e95f8e32964c925c7d2563a03f7. Clear the caches that could hand 1.107.0 back: uv cache clean litellm, the pip cache folder (~/.cache/pip on Linux, ~/Library/Caches/pip on macOS, %LOCALAPPDATA%\pip\Cache on Windows), and your CI caches (gh cache list, then gh cache delete).

5. Assume the data left the host

If the host had outbound internet, LiteLLM tells you to assume the exfiltration succeeded. The marker file confirms it; its absence proves nothing.

6. Rotate every credential the host could reach

LiteLLM names four in particular: "the instance/task IAM role", "every provider key stored in the LiteLLM proxy DB, plus the master and virtual keys", "the DATABASE_URL password", and "any secret that was in the environment of any process on the host". The rotation table covers the rest of what the stealer reads.

7. Search your cloud logs for use of the stolen keys

LiteLLM: "Check CloudTrail for GetCallerIdentity from the instance role around install time, then for use of the role's credentials from outside your infrastructure." For example, aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=GetCallerIdentity --start-time 2026-10-21T11:00:00Z, in each Region you use, us-east-1 included. Then look for ListFoundationModels and other Bedrock calls from addresses outside your network, the pattern Wiz saw.

Credentials to rotate, by type

The middle column is where the stealer looks, per SafeDep's module table and Upwind's decode. Rotate what the host could reach, starting with what costs money or ships code.

CredentialWhere the stealer reads itWhat to do
AWS instance or ECS task roleThe instance metadata service (IMDSv2, then v1) and ECS task credentialsRevoke the role's active sessions in IAM, then check CloudTrail (step 7). Wiz saw these used within hours
AWS, GCP, and Azure user credentials~/.aws/credentials, gcloud credentials, Azure CLI tokens, environment variablesDeactivate and replace access keys; delete and recreate service-account keys; sign out the CLIs and revoke their sessions
AI provider keys behind a LiteLLM proxyLiteLLM_CredentialsTable and LiteLLM_ProxyModelTable, through DATABASE_URLCreate new keys at each provider (OpenAI, Anthropic, Bedrock, Vertex, Azure OpenAI, and the rest), update the proxy, then revoke the old ones
LiteLLM master key and virtual keysThe proxy's environment and databaseReplace the master key and reissue virtual keys. Virtual keys are what your users hold, so tell them
The proxy's database passwordDATABASE_URL in the environmentChange the password and update DATABASE_URL
AI coding tool loginsClaude Code, Claude Desktop, Codex auth.json, Cursor, Copilot, Gemini CLI, Windsurf, Kiro, Zed, Hugging Face tokenLog out and back in, and revoke API keys in each provider's console
API keys in other AI tools' config filesAider (~/.aider.conf.yml), Continue (~/.continue config and .env), Goose, opencode, Qwen (~/.qwen/oauth_creds.json), Crush; in Upwind's decode also Cline, Roo Code, Cody, Amp, Factory, Trae, and OllamaReplace every key those files hold, including the LiteLLM virtual key a client used to reach your proxy
MCP server tokensVS Code's and Cursor's mcp.json, Windsurf's mcp_config.json, Kiro's ~/.kiro/settings/mcp.json, and the configs of the tools aboveRevoke and reissue each token those files hold
GitHubgh CLI's hosts.yml, .git-credentialsRevoke the GitHub CLI's authorization under Settings, Applications, Authorized OAuth Apps, and replace personal access tokens
npm, PyPI, Cargo, RubyGems, Maven, Gradle, Composer.npmrc, .pypirc, and the other registry configsRevoke the tokens, and check that no new versions of your own packages went out since October 21
SSH and GPG keys~/.ssh/*, GPG private keysMake new keys, then remove the old public keys from servers, GitHub, and GitLab
TLS private keys/etc/ssl/private, Let's Encrypt folders, *.pem, *.key, *.pfxReissue the certificates and revoke the old ones
Kubernetes and DockerThe service-account token, ~/.kube/config, ~/.docker/config.jsonRotate the service account and kubeconfig credentials; revoke registry tokens
CI and infrastructure secretsGitLab runner config, Jenkins keys, .vault-token, Terraform and Packer credentials, /run/secrets/*Rotate each, starting with tokens that can deploy
Browser passwords and cookies, on a laptopChromium's saved passwords (decrypted) and cookies, Firefox logins.jsonChange the passwords and sign out every session, starting with email, cloud consoles, and GitHub
Everything else in an environment variable or a .env file/proc/*/environ of every process it can read, and .env, .env.local, .env.production, .env.development in your home and working foldersRotate it. On a shared host, that includes other services' secrets

Which LiteLLM version is safe

On PyPI, 1.106.2. It is the latest release PyPI lists (checked 06:11 JST), uploaded on October 21 at 10:44 UTC, an hour before the bad release. pypi.org/pypi/litellm/1.107.0/json returns 404, and there is no 1.107.1 or later. LiteLLM's guidance: "pin your version to 1.106.2 or any explicit version published prior to now", and "All previous versions are safe." The March versions 1.82.7 and 1.82.8 are the exception; PyPI removed them too, but a private mirror may still hold them. Upwind checked every wheel of 1.103.6, 1.103.7, 1.104.4, 1.104.5, 1.105.2, 1.105.3, 1.106.0, 1.106.1, 1.106.2, 1.107.0rc1, and 1.107.0rc2 and found them clean.

On Docker, the 1.106.2 images, pinned by digest. As checked at 06:11 JST, v1.106.2, latest, main-latest, and main-stable all resolve to the same digest on Docker Hub and GHCR, per image; v1.107.0 returns "not found" on both.

Image1.106.2 digest (clean)
litellm/litellm, ghcr.io/berriai/litellmsha256:7294b90f868c5c38c99b83021c7ba46f9b439e95f8e32964c925c7d2563a03f7
litellm/litellm-database, ghcr.io/berriai/litellm-databasesha256:35f2cec3a14bb297e08bfab56c1fc377432bc4071aaf71978fdff043a5d91e79
litellm/litellm-non_root, ghcr.io/berriai/litellm-non_rootsha256:07fabea33dfc2b127e865c03112dfc76936334b29028ec05cdd355af2385dc04

A new release is planned. LiteLLM's registries have been locked to new releases since October 21. Its post of October 23 says: "At this point, we are confident in our ability to safely issue a release by EOD today, providing our users with a fix for removing the eMudhra certificate authority from our trust store." PyPI still listed 1.106.2 as the latest at 06:27 JST on October 24. When the release comes, check it before you move to it: Upwind's report asked the maintainers to "Treat the next release as untrusted" until the release environment is audited, and LiteLLM says it will share its process changes "next week". For the eMudhra certificates themselves, see Chrome's eMudhra root removal on October 27: what to do.

One more date: October 27. LiteLLM says its private enterprise registry carries "an embargoed release that remediates a vulnerability whose disclosure date is scheduled for October 27." LiteLLM hasn't said whether 1.106.2 has that vulnerability. If your proxy is reachable from the internet, watch for the October 27 disclosure.

The Docker and Kubernetes case

LiteLLM's report lists these image indexes as compromised. The platform digests are Wiz's; LiteLLM gives only the first 12 characters for the -database and -non_root indexes.

ImageBad index digest (LiteLLM)Bad platform digests (Wiz)
ghcr.io/berriai/litellm and docker.io/litellm/litellmsha256:1bbcb1443bc962e45f5aad53fe3d2c30600e0379715fb6c664cdd3ac126c339eamd64 bd1aeee419ea36da9aa2b657288da4101887be468f020ac63d22ba6ed84a3ea5, arm64 56b74573eed6e4aafe4b50603dbe7853e0345f188c8e44a09f1098caf1f4bcdf
ghcr.io/berriai/litellm-databasesha256:c00f9d92c427…amd64 24581ddc30767f16ecb69d2b0517556f2cd7dea56971adb42a3d1beb97d69523, arm64 827dd893c74cf621a860a66a3a447a5c732533f087589ce905573e285c0a239c
ghcr.io/berriai/litellm-non_rootsha256:7b00e91cd2fe…amd64 645a67d810628252d63df85dcc8824c58bcc77cf8383f4d3e6321f0ccc3ce9e4, arm64 f75c8edae59b72b1ebfd617295833f465c85bb2fab9c1d60f21b496db86cb8da

LiteLLM says all affected Docker Hub tags "and image variants" were removed or repointed by 14:20 UTC, but it lists no Docker Hub digests for litellm/litellm-database and litellm/litellm-non_root. If you ran either with one of the four tags in the window, treat it like its GHCR twin.

  • Pulling isn't running. The payload fires when a Python interpreter starts from the image: docker run, a RUN step in a build on top of it, docker exec into its container. A commenter's reproduction with a test image, posted in LiteLLM's issue on October 22, found that unpacking an image to local storage, the step a pull performs, set nothing off, while docker run did. A host that only pulled or mirrored the image holds an inert copy; delete it.
  • Inside the image, the files sit in the app's venv: Upwind found them under /app/.venv/lib/python3.13/site-packages/ in the amd64 ghcr.io/berriai/litellm image.
  • A proxy with a database is the worst case. The -database variant is built for a proxy with DATABASE_URL set, and the stealer dumps the provider keys from that database on first start.
  • The Docker Hub floating tags now point to 1.106.2. Docker Hub's API shows latest, main-latest, and main-stable on all three images pushed again between 17:50 and 17:52 UTC on October 21, to the 1.106.2 digests (checked 06:11 JST).
  • A signature check would have passed. LiteLLM signs its images, and its report says the hijacked workflow "signed and published the poisoned artifacts". Compare digests, not signatures, for this incident.
  • Kubernetes nodes count too. The stealer reads instance-metadata credentials, and Wiz saw credentials taken from Kubernetes nodes in use. If a pod ran the bad image, rotate the node's instance role and the pod's service account, not only the proxy's keys.
  • The published Helm chart pins the image to its own version by default. The chart at oci://ghcr.io/berriai/litellm-helm fills an empty image.tag with its appVersion: chart 1.106.2 has appVersion 1.106.2 and runs ghcr.io/berriai/litellm:1.106.2, which resolves to the clean digest (checked 06:10 JST). A 1.107.0 chart and an unprefixed 1.107.0 image tag both return "not found" now; LiteLLM's report doesn't say whether either existed in the window, so check helm history <release> for an upgrade on October 21. If you set image.tag to latest or another floating tag, the chart's pullPolicy: Always (in its values file on the main branch, read 05:38 JST) meant every pod start in the window pulled the bad image.

How the attacker got in, and what to check in your own setup

LiteLLM announced its completed investigation, dated October 23, in its GitHub issue on October 23 at 21:15 UTC (October 24, 06:15 JST), and calls it "the canonical source of truth where the status updates and the narrative below disagree". Its account, in order:

  1. May 27, 2026: a configuration file committed to the public repository LiteLLM-Labs/lite-harness held the master key of a deployment of the LiteLLM Agent Platform, a tool LiteLLM uses to orchestrate agents. The commit was force-pushed over, but "key revocation did not occur."
  2. October 10: the attacker used that key on the Agent Platform's API, which returned the configuration of every managed agent, including the GitHub CLI OAuth token of one of LiteLLM's two GitHub organization owners. That token "was not associated with a service account and was not scoped down."
  3. October 10, minutes later: with the owner's token, the attacker pushed throwaway branches to two repositories whose workflows posted the repositories' CI secrets to a webhook.site endpoint, then deleted the runs. Two of those secrets held the GitHub CLI tokens of two more maintainers.
  4. October 21: the owner's token overrode branch protection to point the release workflow at the lookalike Berri-AI/litellm, the second maintainer's token approved the release, the third deleted the run records, and the owner's token force-pushed the change out of the visible history.

This replaces LiteLLM's first account. Its first post suspected that the tokens were taken from the logs of an internal, internet-facing LiteLLM deployment, and described Claude Code putting GitHub CLI tokens into logged response bodies. The October 23 post says: "Our first post suspected the tokens were taken from the logs of an internal LiteLLM deployment. The request logs show the first token came from a different service." The first post still carries the old passage (checked 06:25 JST).

What the chain says to check in your own setup:

  • Keys that were ever public. Search your public repositories and their history for LiteLLM master keys and other API keys, and revoke any key that was ever committed, even if the commit was rewritten.
  • Personal GitHub tokens in agents and CI. The stolen tokens were GitHub CLI OAuth tokens, which start with gho_, per GitHub's documentation; personal access tokens start with ghp_ or github_pat_. Search agent configs, CI secrets, and logs for them, and replace them with tokens scoped to one job. LiteLLM notes that GitHub CLI tokens "are post-2fa and not subject to GitHub's 'super-user' 2fa reauth flows".
  • Your GitHub organization's audit log. Look for branch-protection overrides, deleted workflow runs, and workflow runs on branches nobody on your team made.
  • CI secrets. If a workflow you didn't write ran in one of your repositories, rotate every secret that workflow could read.

Not the March 2026 LiteLLM compromise

Searches for "litellm compromised" also return the March 2026 incident. It is a different event, with different versions, and the Docker image was not affected then.

March 24, 2026October 21, 2026
PyPI versions1.82.7 and 1.82.81.107.0
Window, per LiteLLMFrom 10:39 UTC "for about 40 minutes"11:39 to 13:23 UTC on PyPI; 11:46 to 14:20 UTC on Docker
Official Docker image"not impacted"v1.107.0, latest, main-latest, main-stable compromised
How it ranlitellm_init.pth (1.82.8) and code in proxy_server.pycook.pth and sitecustomize.py
Where data wentmodels.litellm.cloudwww.litellm.dev
Origin, per LiteLLM"the Trivy dependency used in our CI/CD security scanning workflow"A LiteLLM Agent Platform master key committed to a public repository on May 27 and not revoked ("key revocation did not occur"); through it, an organization owner's GitHub CLI token on October 10, then two maintainers' tokens from CI secrets; a release built from the lookalike repository Berri-AI/litellm
OSV and GitHub recordsMAL-2026-2144 and PYSEC-2026-2 in OSV; GHSA-5mg7-485q-xm76 and the malware advisory GHSA-92x9-889m-jgmw on GitHubAdded to MAL-2026-2144 on October 21 and to GHSA-92x9-889m-jgmw by October 22, 00:32 UTC

Timeline in UTC and JST

Times are from LiteLLM's two posts and its maintainers' comments unless a row names another source.

UTCJSTEvent
May 27, 18:53May 28, 03:53A config file with the LiteLLM Agent Platform's master key is committed to the public LiteLLM-Labs/lite-harness; per LiteLLM, "key revocation did not occur"
Oct 10, 21:13–21:28Oct 11, 06:13–06:28The attacker calls the Agent Platform's API with that key; at 21:27 it returns an organization owner's GitHub CLI token
Oct 10, 22:11–22:12Oct 11, 07:11–07:12Throwaway workflows in two LiteLLM repositories post CI secrets, two more maintainers' tokens among them, to webhook.site
Oct 13, 00:45Oct 13, 09:45litellm.dev registered (Upwind, from public registry data)
Oct 21, 08:56–10:4617:56–19:4610 commits in the lookalike Berri-AI/litellm add the payload (Upwind, Wiz)
Oct 21, 10:0819:08The owner's stolen token overrides the branch protection on the private release workflow
Oct 21, 10:4419:44litellm 1.106.2, the current clean release, uploaded to PyPI (PyPI)
Oct 21, 10:4619:46The modified workflow starts a staging run from Berri-AI/litellm
Oct 21, 11:1120:11The second maintainer's stolen token approves the production gate
Oct 21, 11:39–11:4020:39–20:40Seven malicious 1.107.0 wheels uploaded to PyPI
Oct 21, 11:4620:46The bad Docker images published under four tags
Oct 21, 11:5320:53The third maintainer's stolen token deletes the workflow records
Oct 21, 12:0621:06Upwind's report opens LiteLLM's GitHub issue #47826
Oct 21, 12:09–12:2421:09–21:24The owner's token reverts the workflow change and force-pushes it out of the visible history
Oct 21, 13:2322:23PyPI has removed 1.107.0
Oct 21, 13:5122:51The GHCR tags unavailable; Docker Hub's still resolve to the bad image
Oct 21, 14:2023:20All affected Docker Hub tags removed or repointed
Oct 21, 16:20–17:01Oct 22, 01:20–02:01Stolen AWS credentials validated and used to list Bedrock models (Wiz)
Oct 21, 17:50–17:52Oct 22, 02:50–02:52Docker Hub's latest, main-latest, and main-stable pushed to the 1.106.2 digests (Docker Hub API)
Oct 21, 18:30Oct 22, 03:30LiteLLM publishes its incident post
Oct 21, 21:22Oct 22, 06:22LiteLLM's first account of the attack chain, replaced on October 23
Oct 22, 02:19Oct 22, 11:19Releases still paused; updates move from every 2 hours to "significant developments"
Oct 23, 21:15Oct 24, 06:15LiteLLM announces its completed investigation, dated October 23, in its issue: its "canonical source of truth", with a release planned "by EOD today"
Oct 27Oct 27Disclosure date of the vulnerability LiteLLM's embargoed enterprise release fixes

Tell the people who use your proxy

If your LiteLLM proxy served your own users or customers, reissuing its virtual keys breaks every client that still holds an old one. Ask your coding agent to write one HTML page for them: which keys stopped working and from when, where to get a new key, and whether they need to rotate anything of their own.

Convika turns that file into a link: sign up free, drop the file on the upload screen, check the private preview, and publish (publish an HTML page). On the free plan the page is public: search engines and Convika's gallery may list it, so leave out key prefixes, hostnames, and anything internal.

Publish my notice page

Updates to this page

  • October 24, 2026, 06:27 JST: LiteLLM's completed investigation, dated October 23 and announced at 21:15 UTC, withdraws its first post's suspicion that the tokens came from the logs of an internal LiteLLM deployment; the attacker started from an Agent Platform master key committed to a public repository on May 27. The section on how the attacker got in, the timeline, and the March comparison follow the October 23 account, and the version section adds the release LiteLLM plans.
  • October 24, 2026, 06:15 JST: first version, with every changeable fact checked again between 06:10 and 06:15 JST.

This page changes when LiteLLM publishes the release it planned on October 23, when it shares its process changes, when it names the vulnerability behind its October 27 disclosure, when a CVE or a LiteLLM advisory is published, and when anyone reports new use of the stolen credentials. The same week, for developers: Node.js October 2026 security release: what to update.

Frequently asked questions

Is litellm 1.107.0 compromised?

Yes. All seven litellm 1.107.0 wheels on PyPI, live from 11:39 to 13:23 UTC on October 21, 2026, contained a credential stealer and a remote-code loader, per LiteLLM's incident report. PyPI has removed the release.

How do I check if I installed litellm 1.107.0 without running Python?

Search for cook.pth and a litellm-1.107.0.dist-info folder from the shell, with find, and look for /tmp/.pc_once_72129118c59f5636c0bb and /tmp/remote_code_loader.lock. Don't run pip show: any Python start in the affected environment runs the payload again.

Which LiteLLM version is safe to install?

1.106.2, which PyPI lists as the latest release (checked October 24, 06:11 JST). Pin it with litellm==1.106.2, and pin the Docker image by its 1.106.2 digest. LiteLLM says all versions before 1.107.0 are safe, March's 1.82.7 and 1.82.8 excepted.

Is the LiteLLM Docker latest tag safe now?

As checked on October 24 at 06:11 JST, latest, main-latest, and main-stable on Docker Hub and GHCR resolve to the 1.106.2 images. Between 11:46 and 14:20 UTC on October 21, they pointed to the compromised image. A copy pulled then, or synced by a mirror, is still the bad image.

Does docker pull alone run the malware?

No. The payload runs when Python starts inside the image, as with docker run, a RUN step in a build, or docker exec. A commenter's reproduction with a test image, posted in LiteLLM's issue, found that unpacking an image, as a pull does, set nothing off. Delete the pulled copy anyway.

What is cook.pth?

A 12-byte file (13 bytes on Windows) the bad wheels put in site-packages. It holds import cook, which Python runs at every start, so it loads cook.py, the credential stealer. Its SHA-256 on Linux and macOS is 7f191632e7320705d154b85204f1ae546b543acbd7e8861229b76f5042b92bdb.

Does uninstalling litellm remove the malware?

Not safely. pip uninstall starts Python, which runs the payload first, and the hourly daemon can keep running outside site-packages. Delete the files from the shell, kill the process holding /tmp/remote_code_loader.lock, and rebuild the environment.

Were LiteLLM Enterprise customers affected?

No, per LiteLLM: customers running the private enterprise release from its private registry were not affected.

Is this the same as the March 2026 LiteLLM compromise?

No. March affected 1.82.7 and 1.82.8 on PyPI, and LiteLLM said its official Docker image was not impacted. October affected 1.107.0 on PyPI and four Docker tags.

Is there a CVE for the LiteLLM 1.107.0 compromise?

No CVE as of October 24, 06:11 JST. GitHub's Advisory Database lists 1.107.0 in the malware advisory GHSA-92x9-889m-jgmw (updated October 22, 00:32 UTC), and the OSV database in the malware record MAL-2026-2144; both also cover 1.82.7 and 1.82.8. LiteLLM has published no advisory of its own for it.

How did the attacker publish LiteLLM 1.107.0?

Per LiteLLM's completed investigation of October 23: a LiteLLM Agent Platform master key, committed to a public repository on May 27 and not revoked ("key revocation did not occur"), let the attacker read an organization owner's GitHub CLI token on October 10. With it, the attacker took two maintainers' tokens from CI secrets, and on October 21 used the three tokens to point the release workflow at a lookalike repository, approve the release, and delete the records. LiteLLM's first post had suspected the logs of an internal LiteLLM deployment; the October 23 post replaces that.

Were the stolen credentials used?

Yes, per Wiz: between 16:20 and 17:01 UTC on October 21, an attacker validated stolen AWS instance-role credentials and used them to list Amazon Bedrock models. Check CloudTrail for GetCallerIdentity and Bedrock calls from outside your network.

Sources

Times are in JST (UTC+9), on October 24, 2026.