LiteLLM 1.107.0 supply chain attack explained: how the stealer runs, who is affected, and which keys to rotate A poisoned LiteLLM 1.107.0 release on PyPI and Docker Hub stole credentials from every machine that ran it, according to LiteLLM's October 21, 2026 incident post. PyPI hosted the malicious package from 11:39:51 to 13:23 UTC and Docker Hub tags pointed at it from 11:46:56 to 14:20 UTC; Wiz counted about 49,000 downloads and observed stolen AWS keys tested against Bedrock from 16:20 UTC. The payload — cook.pth, cook.py and sitecustomize.py in site-packages — executes on every Python interpreter start without importing LiteLLM, harvesting cloud, SSH, CI, browser and AI tool credentials plus every provider key in the LiteLLM proxy database; LiteLLM attributes the breach to stolen GitHub CLI tokens from three engineers and a release workflow run against a lookalike repo, Berri-AI/litellm, and advises pinning 1.106.2 and rotating every reachable secret. LiteLLM 1.107.0 supply chain attack explained: how the stealer runs, who is affected, and which keys to rotate On October 21, 2026, a poisoned LiteLLM 1.107.0 reached PyPI and Docker for a few hours and stole credentials from every machine that ran it. The facts are spread across LiteLLM's incident post, a long GitHub issue and two vendor write-ups that do not fully agree. This page puts them in one place, explains why the payload runs without anyone importing LiteLLM, and turns the exposure windows into a check you can run. This explains reporting by LiteLLM, "Security incident, October 21, 2026" LiteLLM docs blog, October 21, 2026 https://docs.litellm.ai/blog/security-incident-oct-21-2026 . Read the original first: https://docs.litellm.ai/blog/security-incident-oct-21-2026 https://docs.litellm.ai/blog/security-incident-oct-21-2026 In one minute - litellm==1.107.0 on PyPI all seven wheels and the v1.107.0, latest, main-latest and main-stable Docker tags carried a credential stealer and a remote-code loader. PyPI had it from 11:39:51 to 13:23 UTC on October 21; Docker Hub tags pointed at it from 11:46:56 to 14:20 UTC. - The payload is three files in site-packages: cook.pth, cook.py and sitecustomize.py. Python runs them on every interpreter start, so any python, pip or pytest run in that environment fires the stealer again. - It takes cloud, SSH, CI, browser and AI tool credentials, plus every provider key stored in the LiteLLM proxy database. Wiz counts about 49,000 downloads and saw stolen AWS keys being tested against Bedrock from 16:20 UTC. - LiteLLM says attackers used stolen GitHub CLI tokens from three engineers, disabled branch protection with an org owner's token, and ran the real release workflow against a lookalike repo, Berri-AI/litellm. - Fix: pin 1.106.2, delete the files from the shell, rebuild, and rotate every secret the machine could reach. LiteLLM's private enterprise registry was not affected. What happened, in order LiteLLM is an open-source gateway: one API in front of OpenAI, Anthropic, Bedrock and many other model providers, often run as a proxy that stores every provider key in its own database. That last detail is why this incident matters more than a typical poisoned package. LiteLLM's own timeline, all UTC on Wednesday, October 21, 2026: - 10:08:29: branch protection rejected an unauthorized change to the release workflow. Five seconds later a privileged developer credential overrode it. - 11:08:53: the production release pipeline started. At 11:11:18 a second developer credential satisfied the approval gate. - 11:39:51 to 11:40:13: seven malicious wheels uploaded to PyPI. - 11:46:56: the poisoned container images published to Docker Hub and GHCR. - 11:53:49 to 11:54:01: a third developer credential deleted the production and staging workflow records. - By 13:23: PyPI removed the release. GHCR tags were unavailable by 13:51, and Docker Hub tags were removed or repointed at 14:20. The first public report came at 12:06 UTC, in GitHub issue 47826 by the Upwind research team, about 26 minutes after the wheels landed. Wiz puts the attacker's pushes to the lookalike repository at 08:56 to 10:46 UTC, so the setup started about two hours before the release ran. Why the stealer runs on every Python start Nobody has to import LiteLLM for this payload to run. It uses two documented features of Python's site module, which runs at interpreter startup unless Python is started with -S. - A .pth file in site-packages is read at startup. Most lines just add paths, but Python's documentation says lines starting with import are executed. cook.pth is 12 bytes: import cook. So every interpreter that uses that site-packages runs cook.py. - After the paths are set up, Python tries to import a module named sitecustomize. A sitecustomize.py dropped into site-packages runs automatically too. The wheel shipped the three files under its .data/purelib folder, which pip installs straight into the site-packages root. The sdist carried one extra line in pyproject.toml, data = "cook" in the tool.maturin section, which tells the build tool to include them. That line is not in the public v1.107.0 tag. This has a practical trap. LiteLLM's post suggests running pip show litellm to check exposure, and also warns not to start with pip install because any Python start reruns the payload. Both are true, and they collide: pip is a Python program, so pip show in an infected environment runs the stealer again. Check from the shell first, with the commands at the end of this page. Pulling the image is a different case. A docker pull only unpacks layers to disk and starts no process, so the payload does not run. It runs on docker run, on a build with a RUN step on top of the image, or on docker exec. One contributor in the GitHub issue reproduced this with a marker .pth file. A pulled copy is still poisoned, so delete it. What it took and where it sent it Upwind decoded the stealer statically from the wheel; it calls itself privcollect. It runs once in a background thread and needs no command server. Its targets include: - Cloud credentials: AWS, Azure and gcloud files, kubeconfig, and instance metadata. It validates AWS role credentials with STS GetCallerIdentity before sending them. - SSH keys, GPG keys, TLS private keys, .netrc, .git-credentials, .npmrc, .pypirc and other package registry tokens, .env files and shell histories. - CI secrets: GitLab runner, Jenkins, Vault, Terraform and Packer files, the Kubernetes service account token and /run/secrets. - AI tool credentials and configs for Claude, Codex, Gemini, Cursor, Windsurf, Copilot and others, plus Hugging Face tokens. - The environment variables of every process it can read through /proc. - If DATABASE URL is set, it dumps the LiteLLM proxy tables that hold upstream provider keys LiteLLM CredentialsTable and LiteLLM ProxyModelTable . The data is encrypted with a random AES-256 key wrapped in RSA-4096 and posted to http://www.litellm.dev/healthcheck. That domain is not LiteLLM's theirs is litellm.ai ; Upwind says it was registered on October 13. A network capture cannot show what was taken. sitecustomize.py adds a second stage on Linux and macOS: a detached daemon that checks a file in the GitHub repo letters9813/pythonclient-test every hour, and if it names a domain, downloads code from it and runs it. The sources disagree on whether that ever fired. Upwind says the file was empty from October 20 at 09:15 UTC, and Wiz says it was never populated with a live domain. SafeDep says hosts that ran the package earlier may have executed second-stage code and that it could not recover the server address. Treat a host that ran 1.107.0 as possibly having run unknown code. How a hardened release pipeline still shipped it LiteLLM says that after a prior incident in March it moved releases to a private repo, published through GitHub Trusted Publishing, made Docker tags immutable and signed its images. Final publishing was limited to one account tied to one laptop in its office. Each control was beaten by a different stolen credential: - Trusted Publishing trusts the workflow, not the person. The attacker ran the real workflow, so PyPI accepted the upload as legitimate. - Branch protection blocked the first attempt to point the releaser at another source repo. One of the stolen tokens belonged to one of LiteLLM's two GitHub organization owners, and it disabled the protection and force-pushed the change. - The approval gate needed a second person. A second engineer's stolen token approved the run. - On the Monday before, LiteLLM had widened release permissions so two engineers could publish bugfix backports. That widening is what put a workflow-capable token in reach. - Image signing worked as designed. The images were signed by LiteLLM's own pipeline, so a signature check passes. What gives the attack away is the source: Upwind found that the SLSA provenance attached to every v1.107.0 image names https://github.com/Berri-AI/litellm, with a hyphen, as the source repo. A check that the provenance names the expected repository would have rejected these images; a check that only verifies the signature would not. The lesson for anyone running a release pipeline: signing proves which pipeline built an artifact, not which code it built. Pin the source repository in your provenance policy, and treat organization owner tokens on developer laptops as the keys to every release. Where the GitHub tokens came from LiteLLM's working explanation, posted October 21 at 21:22 UTC: it found an unexpected, innocuously named user account in an internal LiteLLM deployment that was exposed to the public internet. It believes the attacker got into that deployment, read GitHub CLI OAuth tokens from its logs, and used them on the release workflow. It suspects entry was through a recently patched vulnerability and has not confirmed which. How did GitHub tokens end up in an AI gateway's logs? LiteLLM says it has observed specific situations in which Claude Code embeds a user's GitHub CLI OAuth token in a response body, and that where response bodies are logged, the tokens can appear in LiteLLM logs. It did not say which situations. Read that as a general rule rather than a one-off. A gateway that logs full requests and responses for coding agents is logging whatever those agents read, and coding agents read config files, environment variables and command output. Those logs are a secrets store and need the same protection as one: no public exposure, short retention, and redaction where your gateway supports it. What attackers did with the keys Wiz reports post-exploitation activity from 16:20 to 17:01 UTC on October 21, about four and a half hours after exfiltration. The attacker took AWS instance role credentials harvested from CI runners and Kubernetes nodes in several environments and ran the same three calls on each: sts:GetCallerIdentity, then bedrock:ListFoundationModels, then a ListModels call. That pattern is LLMjacking: checking whether stolen cloud credentials can run paid AI models, so someone else pays the inference bill. In a small number of environments Wiz also saw IAM enumeration ListRoles, ListUsers, ListAccessKeys . The source IPs Wiz names are 168.100.8.57 and 165.227.201.8. The same week, Symantec described an autonomous agent that hijacks exposed model servers. The common thread is that access to AI models is now something attackers steal and resell, and a gateway holds the keys to all of them at once. Check your machines Start from the shell, without running Python in an environment that might be infected. Look for the planted files and the dist-info folder. A sitecustomize.py inside site-packages is rare; read any you find before deleting it. find / \ -name cook.pth -o -name cook.py -o -name sitecustomize.py \ -path ' site-packages ' 2 /dev/null find / -type d -name 'litellm-1.107.0.dist-info' 2 /dev/null ls -la /tmp/.pc once 72129118c59f5636c0bb /tmp/remote code loader.lock 2 /dev/null The .pc once file is written only after a successful upload, so finding it confirms exfiltration. Not finding it proves nothing, because containers and tmp cleanup remove it. The lock file means the hourly daemon ran; kill the detached python process that holds it. Check lock files and requirements in your repos for the bad version: grep -rnE 'litellm==1\.107\.0' --include=' .txt' --include=' .toml' . 2 /dev/null grep -rn -A1 'name = "litellm"' --include=' .lock' . 2 /dev/null | grep '1.107.0' Check Docker hosts for the poisoned image and any container made from it. The Docker Hub index digest LiteLLM gives is sha256:1bbcb1443bc962e45f5aad53fe3d2c30600e0379715fb6c664cdd3ac126c339e: docker images --digests | grep -i litellm docker ps -a --format '{{.Image}} {{.CreatedAt}} {{.Names}}' | grep -i litellm On AWS, look for the Bedrock probe from the IPs Wiz names, in each region you use: aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=ListFoundationModels --start-time 2026-10-21T11:00:00Z --output json | grep -E '168\.100\.8\.57|165\.227\.201\.8' Who is affected | Case | Status | |---|---| | pip or uv installs that resolved litellm to 1.107.0 unpinned, or a range like =1.106 between 11:39:51 and 13:23 UTC on October 21 | Affected. Assume the stealer ran and the upload succeeded if the host had outbound internet. | | Docker: litellm, litellm-database or litellm-non root at v1.107.0, latest, main-latest or main-stable, run from about 11:47 UTC until the tags were pulled 13:51 on GHCR, 14:20 on Docker Hub | Affected if a container ran. Proxies with DATABASE URL set also leaked every stored provider key. | | The same image pulled into a mirror or cache but never run | Payload did not run, based on how image pulls work. The image is still poisoned; delete it and check nothing was built on top of it. | | Pinned to 1.106.2 or earlier, by version, lock file hash or image digest | Not affected by this release. Re-check that no lock file was refreshed during the window. | | Other packages that depend on litellm | Affected if they let pip pick the newest litellm during the window. Check the installed version from the shell. | | LiteLLM enterprise customers on the private registry | Not affected, per LiteLLM. | What to do - Check every host, container and CI runner from the shell for cook.pth, cook.py, sitecustomize.py and litellm-1.107.0.dist-info before you run any Python there. - Delete the planted files and their pycache copies, kill the detached daemon, then rebuild the environment or image from scratch pinned to litellm==1.106.2. - Rotate everything the host could reach: the instance or task IAM role, every provider key in the LiteLLM proxy database, master and virtual keys, the DATABASE URL password, CI tokens, SSH keys, package registry tokens and AI tool tokens. - Search CloudTrail for GetCallerIdentity and Bedrock ListFoundationModels calls from unfamiliar addresses since 11:00 UTC on October 21. - If you run a LiteLLM proxy, take it off the public internet, review its user accounts, and stop logging full response bodies unless you redact secrets. - Pin dependencies and images by hash or digest, and if you verify provenance, require the exact source repository, not just a valid signature. What is still unknown - LiteLLM has not confirmed how the attacker first got into its internal deployment. It suspects a recently patched vulnerability and has not named it. - LiteLLM did not say in which situations Claude Code puts a GitHub token into a response body. This page found no Anthropic statement on it as of October 22. - The 49,000 download figure is Wiz's reading of ClickPy data. Downloads include mirrors and CI runs and are not a count of victims. - Upwind and Wiz say the second-stage loader never received a live domain. SafeDep says earlier hosts may have run second-stage code. Nobody has published proof either way. - LiteLLM's post gives the compromise time as 4:00 AM PST in one place, which is later than the 10:08 UTC first event in its own timeline. The detailed timeline is used here. - The claim that a docker pull alone does not run the payload comes from a contributor's reproduction in the GitHub issue and from how image pulls work, not from LiteLLM. Sources AI News Report https://theainewsreport.com/ · every headline, every morning.