LiteLLM 1.107.0 shipped a credential vacuum that ran on every python
At 11:39 UTC on October 21, 2026, the BerriAI release pipeline built LiteLLM 1.107.0, uploaded the wheels to PyPI, and tagged four Docker images on Docker Hub and GHCR. For roughly the next 100 minutes, every pip install litellm==1.107.0, every docker pull berriai/litellm:latest, and every CI job pinned to main-stable or main-latest installed a credential stealer and a second stage loader. PyPI quarantined the version at 13:20 UTC. The Docker Hub tags were pulled by roughly 14:20 UTC per the Datadog Security Labs timeline. By the time most teams saw the headline, the wheel had already been sitting on their runners for about two hours.
The affected artifacts were litellm 1.107.0 on PyPI and the v1.107.0, latest, main-latest, and main-stable tags on both Docker Hub and GHCR. The Chainguard writeup confirms the git tag of the same name does not contain the malicious code. The payload lived only in the prebuilt wheels and in the container images that bundled them. The last clean release called out by Datadog and Chainguard is 1.106.2.
Three files that ran before anyone imported litellm
The malicious wheel added three files to Python's site-packages, as peers of the litellm/ directory rather than inside it. The three files are cook.pth, cook.py, and sitecustomize.py. The placement is the whole trick.
A .pth file is a line oriented configuration file that CPython's site module reads at interpreter startup, and any line beginning with import is executed before user code runs. The cook.pth file contained an import cook line. The companion sitecustomize.py is auto imported by the same site module on every interpreter start, provided it exists anywhere on sys.path. Together they guarantee the payload runs the instant a Python process begins. Nobody needs to import litellm. Running pip, pytest, ansible, a Jupyter kernel, or a container ENTRYPOINT written in Python is enough.
cook.py is the credential stealer. sitecustomize.py is an hourly beacon that fetches a domain name from raw.githubusercontent.com/letters9813/pythonclient-test/main/domain.txt, then downloads and executes whatever Python code sits at that domain. The dead drop file only ever held test content before the GitHub account was taken down, so no second stage was observed in the wild, but the loader was armed to pull anything the operator decided to drop. Local artifacts are a lockfile at /tmp/remote_code_loader.lock (so only one copy runs at a time) and a run once marker at /tmp/.pc_once_72129118c59f5636c0bb. Those two paths are the fastest tell on a host.
Removing the litellm/ directory does not remove the three files, because they sit beside the package, not inside it. Running pip uninstall litellm on the compromised interpreter re executes cook.pth and sitecustomize.py before the uninstall completes. The LiteLLM security advisory is explicit that any Python start in the environment re triggers the payload, which includes pip itself.
site bootstrap, not into the litellm package. Any interpreter that touches the affected site-packages runs it, whether or not the running program ever uses LiteLLM.
Why the wheel diverged from the tag
The source is clean. The wheels are not. That gap exists because the build was published through LiteLLM's legitimate release workflow by someone holding three engineers' stolen GitHub CLI OAuth tokens, per the reporting on BerriAI's own statement. The release ran against a lookalike repository named Berri-AI/litellm, which differs from the real BerriAI/litellm by a single hyphen. The build provenance recorded the lookalike as the source of truth. The attestation looked fine to a human skimming the summary. The commit it attested, and the three files it signed into the wheel, were not from the real history.
That is the shape of the attack class. Signed provenance ties an artifact to a repository and a commit. It does not tell you whether the repository is the one you meant. When the pipeline, the signing key, the registry account, and the GitHub organization all accept a lookalike fork as a legitimate upstream, every downstream verification check reports green on code that was never in the real upstream history. SLSA level three attestations do not catch this on their own; the identity of the upstream repository is the piece that has to be audited by hand.
What came out of the pipe
cook.py targets categories, not single files. From hosts that ran it, Datadog and the Socket writeup enumerate the collection scope:
- Cloud. AWS, GCP, and Azure credentials, including values pulled from the IMDS endpoint on cloud instances; Kubernetes kubeconfig; Docker config; HashiCorp Vault tokens; Terraform state secrets.
- Developer surface. SSH, GPG, and TLS private keys;
.git-credentials;.npmrc;.pypirc;.envfiles; shell history. - AI providers. Local session files and API keys for Claude, Codex, Gemini, Cursor, and GitHub Copilot.
- Browsers. Saved passwords and cookies.
- LiteLLM proxy. On hosts configured as the LiteLLM proxy, the backing database tables that store upstream provider credentials. That is the fattest prize of the lot.
Everything collected is encrypted with AES 256 under an RSA 4096 session key and POSTed to www.litellm[.]dev/healthcheck, with an X-Token header set to XWiA8gZNa7NcX2XRoAEb. The exfil host is a lookalike of the official litellm.ai, which is why the request looks ordinary in a flow log.
Where a CVSS queue dropped it to the floor
There is no CVE for this event. There will not be one. CVE identifiers describe a flaw in a product; this is an unauthorized release of a legitimate product, published through the real pipeline with signed attestations. Scanner feeds have nothing to match on because nothing in the installed package metadata is wrong. The version string is 1.107.0. The wheel hashes resolve. The signature verifies. The SBOM looks correct.
A triage queue sorted by CVSS would show this at the bottom, below every CVSS 7 and higher advisory, because the queue is literally empty of entries that cover it. The dashboards all report green while every Python interpreter on affected hosts continues to obey cook.pth and keeps the hourly beacon alive.
How TRIS scored it
TRIS, the Threat and Risk Intelligence Scoring engine inside CVEasy AI, walks each finding through separate layers instead of collapsing to a single number. That shape matters here because the CVE and CVSS layers both contribute zero on this event, yet the finding belongs at the top of the queue.
Threat. Confirmed exploitation in the registry itself. Datadog, Chainguard, and the StepSecurity analysis independently reproduced the wheel content, the import time execution, and the exfil endpoint. A proven live payload with observed command and control is top band on this layer, not a theoretical proof of concept.
Reach. LiteLLM is a widely deployed AI gateway with a proxy mode that often holds the organization's root AI provider credentials in a database. On a proxy host, the blast radius is the full set of downstream LLM keys and the spend attached to them. On a developer laptop that happened to pip install litellm into a venv, the blast radius is whatever that laptop had in ~/.aws/credentials and shell history. The two numbers are not comparable, and any score that returns one value for both is useless.
Exposure. The score pivots on whether this interpreter actually ran during the window. CVEasy AI joins the inventory against /tmp/remote_code_loader.lock, /tmp/.pc_once_72129118c59f5636c0bb, the install logs for litellm==1.107.0, and container digests that match the four Docker tags during 11:39 to roughly 14:20 UTC on October 21, 2026. A host with none of those drops to the lowest band on this finding. A host with any one of them lands in the ACT (act now) band regardless of what else is on its triage queue this week.
Impact. The payload harvests long lived cloud and AI credentials. Those credentials often drive automation on other hosts, so the rotation surface is wider than the compromise surface. TRIS multiplies the rotation surface into the impact tier, which usually pushes a proxy host from high to top band.
Same package, same date, two different pieces of work: a sandboxed dev container that never pulled the bad digest scores a flat low, and the exposed proxy scores ACT with a rotation plan attached. The finding without a CVE still lands with a specific action list rather than a yellow banner.
What to do today
Treat any host that executed Python during the window as fully compromised. The window is 11:39 UTC to roughly 14:20 UTC on October 21, 2026, for direct PyPI installs, and until Docker Hub quarantine for pulls that landed later from cached mirrors. Fast checks, in order:
- Grep installation logs and build caches for a
litellm-1.107.0wheel. If any CI job installed that pin, that runner is in scope. - Enumerate every
site-packageson the host and look forcook.pth,cook.py, andsitecustomize.py. Finding one confirms exposure; absence does not prove the host is clean, because a later clean install may have deleted the file while leaving credentials already gone. - Check for
/tmp/remote_code_loader.lockand/tmp/.pc_once_72129118c59f5636c0bb. Either marker confirms the loader actually ran. - In egress logs, hunt POST requests to
www.litellm[.]dev/healthcheckand GET requests toraw.githubusercontent.com/letters9813/pythonclient-test/main/domain.txt.
Rotate everything the stealer touches. Cloud provider keys, including role credentials fetched via IMDS on affected cloud instances. All AI provider API keys. SSH keys. .git-credentials. Any Vault token the host held. On a LiteLLM proxy host, rotate every upstream provider key stored in the proxy database and invalidate every downstream virtual key the proxy issued.
Rebuild, do not upgrade in place. Revert production LiteLLM installs to 1.106.2 on fresh interpreters and rebuild container images from clean base digests. A naive pip install --upgrade litellm on a compromised interpreter re executes cook.pth before pip even finishes resolving. The only safe path is a clean host, a clean venv, and a clean image.
sitecustomize.py was reaching out from compromised hosts for the whole window. Any host that still has an outbound network path and still has that file on disk is still beaconing.
How CVEasy AI surfaces this
CVEasy AI, the number one local-first CTEM (Continuous Threat Exposure Management) platform, ingests Python registry events, container digest changes, and researcher reports as first class inputs beside the NVD feed. TRIS joins the registry event to your installed package inventory and your container runtime inventory, scores the finding against the four layers above, and lands the ACT item in your queue in minutes.
Three decisions it answers without a CVE: which hosts actually ran this interpreter during the window; which cloud and AI keys those hosts could reach; and which downstream services depend on the keys that need rotation first. The join runs on your hardware. Package inventories, container digests, and shell history on each host stay local to that host. TRIS pulls the join into the queue and writes the rotation plan without sending any of it to a third party.
The attack that never gets a CVE is the one most likely to be running right now in your pipeline. Build the queue so a finding without a number can still reach the top of it.
Sources: Datadog Security Labs, Chainguard Unchained, Socket, LiteLLM security advisory, StepSecurity, The Hacker News, Wiz.