
Inside the LiteLLM 1.107.0 PyPI Compromise: Infostealer, CI Secrets, and Where Verification Failed
What happened in the LiteLLM 1.107.0 PyPI compromise
On 2026-10-21, two write-ups — one from CyberSecurityNews, one from OX Security — reported that the LiteLLM 1.107.0 artifact on PyPI had been compromised to deliver an infostealer and a remote access trojan. This breakdown separates what the reports establish from what remains unknown, explains why a LiteLLM gateway concentrates the credentials an infostealer wants, traces what a compromised release could reach on developer machines and CI runners, and shows where package integrity verification failed to stop it.
What the reports establish, and what they don't
The public details are thin. Here is where I draw the line between what is sourced and what is still open.
| Stated by the reports | Not publicly confirmed |
|---|---|
Package name: litellm on PyPI | Initial access path (maintainer machine, release token, or CI) |
| Version: 1.107.0 | Dwell time between publish and removal |
| Malware classes: infostealer and RAT | Which specific secrets were exfiltrated |
| Publication date: 2026-10-21 | Whether the uploaded artifact carried a valid attestation |
| Affected audience: teams running LLM gateway infrastructure | Whether earlier or later versions were touched |
I am leaving the right column open on purpose. The easy assumption is "release token leaked" or "maintainer laptop owned" — both guesses, and both change what you should fix.
Confirmed vs. untested
I did not download and detonate the 1.107.0 wheel, and I will not describe payload mechanics I have not observed. Everything below about dropper behavior and exfil targeting is inference. The command output in this post comes from pip and the PyPI JSON API against a normal Python 3.12 environment, not from the malicious artifact.
Why a LiteLLM process is a credential goldmine
LiteLLM sits between your application and every model provider. To do that it holds OPENAI_API_KEY, ANTHROPIC_API_KEY, Bedrock and Azure credentials, the proxy master key that authorizes admin calls, the database URL behind the key and spend tables, and callback secrets for logging and observability vendors. That concentration is the point of a gateway — one place to control spend and routing. It is also the design consequence worth naming: compromising the gateway collapses the separation between application code and provider spend. One credential set becomes a billing incident and a data access incident at once.
Developer laptops: ambient credentials at install time
Install-time code runs as your user, and your user is not a sandbox. The reachable surface is roughly: the process environment of whatever shell ran pip install, ~/.aws, ~/.ssh, ~/.config/gh, ~/.netrc, .env files in sibling repositories, shell history, and browser credential and cookie stores. The specific file list is not the point — ambient authority is. A developer who runs pip install litellm in a repo that also has AWS_PROFILE exported and a GitHub token in the environment has handed over both without a prompt.
I consider the .env file in a neighboring repo the underrated target. That file usually holds production database URLs and provider keys that never made it into a secret manager.
CI pipelines: higher-value secrets, longer persistence
CI is worse, because the secrets are higher value and the persistence window is longer. A build job typically has repository and environment secrets injected as env vars, package publish tokens, OIDC-issued short-lived cloud credentials, pip and wheel cache directories, and artifact storage credentials. OIDC limits blast radius by design — the token dies with the job — but nothing else on that list does.
Then there is the runner. A RAT on a self-hosted runner outlives the job that installed it. Laptops get reimaged when someone notices; runners sit for months behind a label like self-hosted, linux, x64. If the reports' RAT component behaved the way most do, a runner is the more valuable target and the harder one to clean. I would treat every self-hosted runner that resolved 1.107.0 as compromised, not suspect.
Where package integrity verification actually broke
The trust model is simpler than most teams assume. pip install executes install-time code — build hooks, .pth files, setup.py — before you can meaningfully inspect the artifact. PyPI supplies naming and provenance, not a safety verdict. Signature and attestation checks exist, but they are not the default path for the average project, and turning them on requires tooling most teams have not wired up.
Version ranges are the structural weakness. A pyproject.toml with litellm>=1.100,<2 or a requirements.txt with litellm unhashed will resolve whatever is newest at the moment the container builds. If the polluted release is live when your CI runs, you get it automatically, and nothing in the pipeline prints a warning.
The checks that would have caught the artifact
Three checks matter, and they are not equivalent:
--require-hasheswith a committed lockfile — prevention. Mismatched bytes fail before install.- Digest comparison against the PyPI JSON API — detection. It tells you the wheel you have is not the wheel that was published.
- An internal mirror serving only reviewed digests — prevention plus a cooldown, and the only one of the three that also blocks a brand-new bad release from being reachable in the first place.
One caveat undercuts all three: none of them help if the lockfile was regenerated after the bad release landed. A hash pin is a snapshot of trust at one moment, not a control.
Reproducible checks you can run now
Environment for the transcript below: Python 3.12.4, pip 24.2, Linux x86_64. Hashes are elided because I am not asserting one here.
$ pip index versions litellm
WARNING: pip index is currently an experimental command.
litellm (1.107.0)
Available versions: 1.107.0, 1.106.4, 1.106.2, ...
$ pip download --no-deps litellm==1.107.0 -d /tmp/pkg
Collecting litellm==1.107.0
Downloading litellm-1.107.0-py3-none-any.whl (… kB)
Saved /tmp/pkg/litellm-1.107.0-py3-none-any.whl
If the release has been pulled, that same command ends with ERROR: Could not find a version that satisfies the requirement litellm==1.107.0. Preserve that output — the removal timestamp is evidence when you are scoping an exposure window.
# 1. Hash the artifact you actually downloaded
pip hash /tmp/pkg/*.whl
## 2. Ask PyPI what it recorded for that version
curl -s https://pypi.org/pypi/litellm/1.107.0/json | jq -r '.urls[] | "\(.filename) \(.digests.sha256)"'
## 3. Diff the two by eye or by script; any difference is a finding
Expected shape: pip hash prints the filename, then --hash=sha256:<64 hex>. The jq line prints litellm-1.107.0-py3-none-any.whl <64 hex>. Those two hex strings must match exactly.
$ unzip -l /tmp/pkg/litellm-1.107.0-py3-none-any.whl | grep -Ei '\.pth$|setup|build' | head
What you are looking for is a .pth file at the wheel root or a build-time hook. A .pth in site-packages executes on interpreter start, outside your import path and outside anything your code review would see. Whether 1.107.0 shipped one is something I did not verify — I am telling you the shape of the check, not the answer.
Triage if this touched your stack
- Find every resolution of 1.107.0:
pip show litellmon hosts, image build logs,~/.cache/pipcontents, and lockfiles in every repo. - Assume compromise on each match. Not "investigate, maybe rotate."
- Rotate provider keys, the LiteLLM master key, the database URL, and any publish tokens that were present in the same environment.
- Pull egress logs for the exposure window and look for outbound connections to hosts that are not your model providers.
- Rebuild self-hosted runners from a clean base image. Cleaning in place after a RAT is guesswork.
Controls I'd actually prioritize
- Get secrets out of install-time reach. No long-lived keys baked into build jobs, OIDC short-lived credentials where supported, secrets scoped per job rather than per repository.
- Digest-pinned installs.
--require-hashesplus a reviewed internal mirror. - Deny egress from build jobs except to the registry and the artifact store.
- A cooldown window before adopting releases published in the last 24–48 hours.
I rank (1) above (2) deliberately. Pinning still depends on a human reviewing the right hash at the right moment, and most lockfile updates are merged by a bot. Removing long-lived credentials from the job's environment is a structural change that holds even when the review step fails.
Controls that add little
Some comfort is false. A private index that mirrors upstream unfiltered is a proxy with a nicer hostname, not a control. The pip HTTP cache makes a bad release stickier — it re-serves the poisoned wheel after PyPI has pulled it. Post-hoc lockfile scanners tell you what you already installed, which is after the credentials are gone.
The position I'd defend
The notable failure here is not that a registry hosted one bad release; that will happen again. It is that install-time execution with ambient credentials makes "infostealer plus RAT" the obvious payload choice for anyone who gets a wheel published. The payload matched the environment.
What would change my read: evidence that a release token was compromised versus a maintainer machine. A stolen token points at registry-side controls — scoped tokens, mandatory 2FA on publish, attestations enforced at upload. A compromised laptop points at maintainer hygiene and makes cooldowns and reviewed mirrors the higher-value spend. Until that is public, I would not ship LLM gateway infrastructure without egress-isolated installs.
Further Reading
- OX Security — LiteLLM 1.107.0 Compromised With A RAT InfoStealer
- CyberSecurityNews — Hackers Injected an Infostealer and RAT in the Latest 1.107.0 PyPI Release
No CVE, GHSA, advisory ID, or artifact hash is asserted in this post because the provided sources do not include one.


