LiteLLM 1.107.0 compromised: what we measured on our own gateway
On 21 October 2026 LiteLLM's release chain delivered poisoned artifacts: seven wheels of litellm==1.107.0 on PyPI, live from 11:39:51 to 13:23 UTC, plus the same build pipeline in the Docker tags v1.107.0, latest, main-latest and main-stable, live from 11:46:56 to 14:20 UTC. The times come from the vendor's incident post. Every wheel carries three extra files that run the next time the Python interpreter starts. For operators of their own AI gateway the version question is the smaller job. The bigger one sits in their own database, which holds the provider keys and, in our case, the full text of requests.
A supply chain attack on software hits not the code in the repository but the path by which users obtain it. For LiteLLM 1.107.0 the source tag on GitHub did not contain the malware, according to the vendor, yet the packages delivered on PyPI and in the Docker tags were built from a fork carrying it. You were affected if you pulled the package or a moving tag such as latest inside the window. This post is the cross-check on our own gateway: digest, version and spend logs, measured on 22 October. What to rotate after a hit is in the sister post on key rotation.
Correction of 5 November 2026: The first version of this post, from 22 October, repeated the vendor's assumption that the GitHub tokens were stolen from the logs of an internal LiteLLM instance. The vendor corrected that itself on 23 October (section "Where the tokens came from"). We rewrote that section, the third FAQ answer and the conclusions drawn from it, brought the version information up to 5 November and removed an unsourced duration for the March releases.
What the release contained
Upwind's analysis in the public issue #47826 (opened 21 Oct, 12:06 UTC) lists the files with size and hash: cook.pth at 12 bytes, cook.py at 22,252 bytes, sitecustomize.py at 25,509 bytes (Wiz gives the last size). pip places them in the site-packages root, and cook.pth contains an import. From then on cook.py runs every time that interpreter starts, whether or not LiteLLM is in use.
cook.py calls itself privcollect and gathers what sits on a machine with a cloud connection: host and network data, saved passwords and cookies from browsers, SSH keys, shell histories, credentials for AWS, Azure, Google Cloud, Kubernetes, Docker and GitHub CLI, .env files, private keys, and the environment variables of every readable process via /proc/*/environ. One detail matters to gateway operators: if DATABASE_URL is in the environment, the process starts Prisma and reads the tables LiteLLM_CredentialsTable and LiteLLM_ProxyModelTable, which means every provider key the gateway holds in its database. It also takes role credentials from the cloud metadata APIs and tests them with sts:GetCallerIdentity. Wiz saw such checks on 21 Oct between 16:20 and 17:01 UTC and classed them as active use of stolen roles.
The second part lives in sitecustomize.py: a detached daemon with the lock file /tmp/remote_code_loader.lock that reads the file domain.txt from a GitHub repo every hour and runs whatever code it names through exec(). According to Upwind that file had been empty since 20 Oct, 09:15 UTC, so the loader slept and one commit would wake it. Datadog reports the repo has since been removed from GitHub. Exfiltration goes out as a POST to http://www.litellm.dev/healthcheck, packed with AES-256-CBC and a key wrapped with RSA-4096-OAEP, so the traffic cannot be read on the wire. According to Upwind, litellm.dev was registered on 13 Oct 2026 and is not a project domain (litellm.ai). OSV groups the case under MAL-2026-2144 together with the March releases 1.82.7 and 1.82.8.
Where the tokens came from
The vendor changed its account. The incident post of 21 October said the attacker pulled the GitHub CLI OAuth tokens from the logs of an internal LiteLLM instance that was exposed to the internet. That post also contains the observation that Claude Code, in certain cases, embeds the user's token in a response text. On 23 October the vendor wrote that its first post had suspected the tokens were in the logs of an internal instance, but that the request logs showed the first token came from a different service.
The chain according to the vendor (post of 23 Oct, with timeline):
- On 27 May 2026 a configuration file holding the admin key of an internal cloud-agent platform, the LiteLLM Agent Platform, was committed to a public repo (LiteLLM-Labs/lite-harness). About three hours later the file was deleted, but, according to the vendor, nobody followed through on revoking the key (human error), and the commit stayed in the public history until 22 October.
- On 10 October the attacker used that key to call the platform's API and got back the GitHub CLI token of one of the two organization owners.
- With that token the attacker had CI workflows send secrets to a foreign endpoint and obtained the tokens of two maintainers.
- On 21 October the attacker used the owner's token to switch off branch protection on the release repo, changed the workflow to build from the fork Berri-AI/litellm, and approved the release with a maintainer's token. According to the vendor, the approval rule had been loosened temporarily earlier that week, so that any two engineers could trigger and approve a release.
- At 11:53 and 11:54 UTC the attacker deleted the run records of the production and the staging run with the third token.
The token from a Claude Code response text no longer appears in this chain. The observation is still in the vendor's first post, but after the correction it does not explain this incident. As a general question it stands, because response texts can end up in logs.
How long each artifact was reachable
| Artifact | Start (UTC) | End (UTC) | Duration in minutes | Source |
|---|---|---|---|---|
| PyPI wheels | 11:39:51 | 13:23 | 103 | Vendor, 21 Oct 2026 |
| GHCR tags | 11:46:56 | 13:51 | 124 | Vendor, 21 Oct 2026 |
| Docker Hub tags | 11:46:56 | 14:20 | 153 | Vendor, 21 Oct 2026 |
All times come from the vendor; we computed the durations. Datadog gives different end times (PyPI 13:20, images 13:53). Our own measurements follow in the section below.
On the Docker tags after 14:20 UTC: the vendor writes that the tags were then removed or repointed to verified clean images. Our registry query on 22 Oct around 04:05 UTC (from the gateway host) showed clean indexes on latest, main-latest and main-stable with push times between 17:50:38 and 17:51:10 UTC. That fits the vendor's statement if the tags were removed in between; what a pull returned in that phase we did not measure. Whether a given run saw that is something only your own pull log can tell. We can no longer measure the intermediate states of the tags.
State at our measurement on 22 Oct around 04:05 UTC: 1.107.0 was gone from PyPI, v1.107.0 no longer existed on Docker Hub and GHCR (404 in each case), and latest, main-latest, main-stable and v1.106.2 all pointed on GHCR to the same clean index sha256:7294b90f868c…. State on 5 November according to PyPI: 1.107.0 still does not exist, new are 1.106.3 to 1.106.7 and 1.108.0 (uploaded 4 November 2026).
What we measured on our own gateway
We run LiteLLM as a gateway in front of four RTX 5090. Measured on 22 Oct 2026 between 04:00 and 04:25 UTC on the host of the container litellm-router, read only. The proxy address cannot be reached from the internet; on our own LAN the listener was reachable (item 8). How we otherwise check the gateway is in our post on the gateway security check.
- The running image is
ghcr.io/berriai/litellm:v1.100.4, pulled 30 Sep, container active since 3 Oct 21:33 UTC according todocker inspecton 22 Oct (our sister post gives a start on 20 October for 24 October; we did not resolve the difference, and both dates precede the incident), internal version 1.100.4. Its GHCR digestsha256:ed8efb7e8c58…matches the digest in the local image store, so our image is the unchanged upstream artifact. - The three poisoned index digests from the incident post (
1bbcb1443bc9…,c00f9d92c427…,7b00e91cd2fe…) appear nowhere in the local image store. - We run version tags, and
latestwas never in use here. That decision was the actual protection on that day. The two self-built images (auto-router and log view, both built on 21 Oct) do not contain thelitellmpackage at all; the only pip-installed LiteLLM on the host is 1.97.0 in an old test environment. - The file search for
cook.pthandcook.pyreturns no hits. The one forsitecustomize.pyreturns three hits from the bundled ddtrace and OpenTelemetry packages of the same image. Without a hash that is a ghost hunt. - The gateway stores full texts:
store_prompts_in_spend_logs: true, retention 30 days. On 22 Oct the spend log table held 1,170,710 rows from 3 Oct to 22 Oct on 20 GB, 60,952 requests of them in the last 24 hours. Full request bodies sit in 215,607 rows of the columnproxy_server_request, response texts in 205,131 rows ofresponse; the columnmessagesis empty everywhere in this version. The table lives in the same database the malware reaches throughDATABASE_URL. The reports name only the credentials and model tables as targets. - Searching the stored full texts for key patterns: the string
github_pat_occurs in 94 rows (as of 04:07 UTC), 84 of them created before 04:00 UTC, spread over the time since 3 October. A key of this class in full length, meaning the prefix followed by twenty or more characters, appears in none of the 1.17 million rows. The short formsghp_,gho_,ghu_,ghs_andghr_at key length do not either. The masked context of the newest hits shows agent prompts that run through this gateway and discuss token leaks. Whoever searches such a table for a prefix gets a list of passages where someone writes about keys, and mistakes it for a find. - Anthropic-shaped traffic is the exception: seven requests over the path
anthropic_messages, eleven rows with Claude model names, all between 21:33:43 and 21:34:57 UTC on 3 October, which are the start checks when the container was created. We do not use Claude Code through this gateway. - The proxy address has no public DNS entry (four candidate names, all NXDOMAIN), no virtual host at our edge points at this port, and the listener sits on the LAN address, and on 127.0.0.1 for the upstream lane. The public chat interface in front of it is a separate service with a login.
The numbers in items 5 to 7 come from our own evaluation of the spend log table on 22 Oct. We did not keep an evidence file for them, so read them as a snapshot.
| Measure | Value | Date and carrier |
|---|---|---|
| Rows in the spend log table | 1,170,710 | 22 Oct 2026, Postgres of the gateway host |
| Table size | 20 GB | same |
| Rows with a request body | 215,607 | same |
| Rows with a response text | 205,131 | same |
Rows containing the string github_pat_ | 94 | 22 Oct 2026 04:07 UTC, same |
| Keys in full length | 0 | same |
What operators should check now
The order follows the state of the evidence.
First, what ran on 21 October between 11:46 and 14:20 UTC. You prove that with the digest in the image store and the deploy log. If you run latest, you also need the registry push timestamps, otherwise it stays open whether the run pulled poison or hit an error. Then every machine where Python installed something that day: CI runners, notebooks, development machines. A pip show litellm today proves little, because a file deleted later leaves no trace in the package list. The three file names, the lock file under /tmp and the marker /tmp/.pc_once_72129118c59f5636c0bb are the better anchors, and the published hashes make the clean distinction.
With a hit, the rule is: make no more Python calls in the affected environment at first, because every start runs the file again. Remove the files and the loader, rebuild the image or environment, then rotate everything the environment could reach: provider keys in the gateway, master and virtual keys, the database password and the instance's cloud role. The vendor assumes the exfiltration succeeded as long as the environment had internet access. A deleted file is no all-clear for the same reason. The individual steps are in our post Which keys to rotate after LiteLLM 1.107.0.
On logs, your own configuration tells you more than any outside advice: are request and response texts stored? Ours is switched on, and 20 GB of full text over 30 days is the concrete answer to what would be readable after the fact. If you do not need it, switch the storage off or shorten the period. If you do need it, know that keys and tokens can appear in it without anyone writing them there on purpose. After the vendor's correction this has nothing to do with the cause of this attack; it is a separate risk of that table.
The more useful lesson sits in the cause chain: a key in git history that, per the vendor, was not revoked, and an unrestricted GitHub CLI token. If you run a gateway or a CI system, first check which keys were ever in a repo and whether they are still valid. The hub AI in production collects further posts on the subject. How we set up AI gateways for customers is described on the page Corporate LLM in your own house.
Further Reading
- Contained: Oct 21, 2026 LiteLLM release v1.107.0 Compromised, LiteLLM, 21 Oct 2026 (with correction notice of 23 Oct)
- Root cause analysis of the 21 October incident, LiteLLM, 23 Oct 2026
- Issue #47826 in the LiteLLM repo, file hashes and payload analysis by Upwind, 21 Oct 2026
- LiteLLM supply chain compromise: What you need to know, Datadog Security Labs, 21 Oct 2026
- LiteLLM Supply Chain Compromise, Wiz, 21 Oct 2026
- LiteLLM PyPI Package and Docker Images Compromised to Steal Cloud and AI Credentials, Socket, 21 Oct 2026
- MAL-2026-2144, OSV, as of 5 Nov 2026
- litellm on PyPI (JSON interface), version list, retrieved 5 Nov 2026
- Simple Icons: pypi.svg, github.svg and docker.svg (logos, CC0-1.0, as of 5 Nov 2026; trademarks stay with their owners, for GitHub see the logo guidelines)
- Running an AI gateway in house: our setup and the admin takeover case from early October
Do we have to change versions right away?+
Anyone who never installed or pulled 1.107.0 changes nothing because of this incident. On 21 October the vendor named 1.106.2 as the pin. Since then PyPI lists further releases (1.106.3 to 1.106.7, and 1.108.0 on 4 November), and the vendor rotated its Docker image signing key. What matters is what ran in your container on 21 October between 11:46 and 14:20 UTC. The image digest and the deploy log prove that; the tag name alone does not.
Is searching for cook.py enough?+
No. cook.pth, cook.py and sitecustomize.py are on the vendor's checklist, but sitecustomize.py is a common file name for automatic instrumentation. In our running image the search returns three hits, all from the bundled ddtrace and OpenTelemetry packages. Only the published hashes from issue 47826 settle it.
Why do the spend logs matter if our instance was never attacked?+
With DATABASE_URL set, the malware reads the provider keys from the gateway's database. In our case the same database also holds the full text of requests. The vendor also writes that Claude Code, in certain cases, embeds the user's GitHub CLI token in a response text, and response texts end up in logs. The question for every operator: do I store full texts, and which keys inside them would still hurt after 30 days of retention?
senn-tech