~/blog / security / LiteLLM Got Compromised Again: Anatomy of the 1.107.0 Supply Chain Attack
dispatch - Security
SECURITY10/23/2622 MIN

LiteLLM Got Compromised Again: Anatomy of the 1.107.0 Supply Chain Attack

byDhruv VakharwalaBackend & Systems Engineer

How GitHub tokens leaked into AI gateway logs let attackers ship a credential-stealing LiteLLM 1.107.0 to PyPI and Docker, and what to rotate now.

⚠ TL;DR

On October 21, 2026, a malicious LiteLLM 1.107.0 was published to PyPI and Docker by LiteLLM's own release pipeline. The attacker used three developers' stolen GitHub CLI tokens, which LiteLLM believes were lifted from the logs of an internet-exposed internal LiteLLM deployment. They overrode branch protection, built the release from a lookalike repo (Berri-AI/litellm), approved it, and deleted the run records. The package installed a .pth startup hook that stole cloud, SSH, browser, CI and AI tool credentials plus every provider key in the LiteLLM proxy database, and an hourly remote code loader. It was live for about 1h44m on PyPI and about 2.5h on Docker, with roughly 49,000 downloads. If you installed it: kill the loader, rebuild, and rotate everything that machine could reach.

Introduction

On October 21, 2026, LiteLLM 1.107.0 went out to PyPI and Docker with a credential stealer inside. It was the second time this year that LiteLLM's releases had been compromised.

The first was in March, through a poisoned security scanner. Afterwards, the team did what you're supposed to do: moved releases to a private repo, switched to Trusted Publishing, made Docker tags immutable, and started signing images.

This time, none of those controls failed. The malicious release was built by LiteLLM's real pipeline, published with LiteLLM's real publishing identity, signed, and pushed to the real registries. Every safeguard did exactly what it was configured to do. The attacker just had the credentials of the people the safeguards were built to trust.

Kevin Beaumont's reaction: "LiteLLM got compromised... again."

So how do you poison a release pipeline that was hardened specifically against this?

Turns out, it starts with logs.

Let me walk through each link in this chain.


What Is LiteLLM and Why Attackers Want It

If you haven't used it: LiteLLM is a Python SDK and proxy from BerriAI that puts 100+ LLM providers behind one OpenAI-compatible API. You point your apps at LiteLLM, and LiteLLM holds the keys for OpenAI, Anthropic, Bedrock, Vertex, Azure and the rest.

That makes it one of the most credential-dense pieces of software in a modern stack:

  • It runs on developer laptops, in CI jobs, and as a production AI gateway in Kubernetes
  • The proxy stores every upstream provider key in its database
  • It usually sits next to cloud roles, database URLs and internal service tokens
  • It gets about 23 million downloads a week, according to Socket

So a poisoned release lands on the machine that holds the keys for every app behind it.


Layer 1: The Entry Point - An Exposed Internal Gateway

LiteLLM's post-mortem says it found "an unexpected, innocuously named user account" on an internal LiteLLM deployment that was exposed to the public internet.

How the attacker got that account is still unconfirmed. LiteLLM suspects "a recently patched vulnerability," but hasn't said which one.

What the attacker found inside is the important part: the deployment's logs contained GitHub CLI OAuth tokens for three LiteLLM developers.

Why this link broke: An internal tool was reachable from the internet. Internal deployments tend to get weaker auth, slower patching and more verbose logging than production, because "nobody outside can reach it." That assumption stops being true the moment it's exposed.


Layer 2: The Pivot - GitHub Tokens in AI Gateway Logs

Here's where it gets interesting.

Why were GitHub tokens sitting in an LLM gateway's logs at all? LiteLLM's explanation:

"we have observed specific situations in which Claude Code will embed a user's GitHub CLI OAuth token into a response body, and in situations where response bodies are logged, these tokens may appear in LiteLLM logs."

That's LiteLLM's own finding. I couldn't find a public response from Anthropic at the time of writing, so treat the exact mechanism as LiteLLM's account.

But the general problem doesn't depend on the details. An AI gateway sees everything that passes through a model conversation: prompts, tool outputs, file contents, command results. When coding agents work in your terminal, secrets end up in that context all the time. If the gateway logs full request and response bodies, those logs become a store of secrets that nobody protects like one.

GitHub CLI OAuth tokens carry the full permissions of the user. One of the three belonged to a GitHub organization owner. And on October 19, two days before the attack, LiteLLM had temporarily expanded release permissions for two internal engineers to handle daily backports.

Why this link broke: Logging full bodies in an AI gateway is convenient for debugging and terrible for secrets. Combine that with broad, long-lived CLI tokens and you get a log file that can publish a package used by millions.


Layer 3: The Escalation - Turning the Release Pipeline Against Itself

On the morning of October 21, the attacker used the three tokens to walk the release through every gate. All times UTC:

TimeWhat happened
08:56 - 10:46Ten attacker commits pushed to a lookalike repo, Berri-AI/litellm (note the hyphen)
10:08:29GitHub branch protection rejects an unauthorized change to the private release workflow
10:08:34Five seconds later, the org owner's token overrides the protection and applies the change
10:46The modified workflow starts a staging run, building from Berri-AI/litellm instead of BerriAI/litellm
11:08Production pipeline begins
11:11A second developer's token satisfies the production approval gate
11:39 - 11:40Seven malicious 1.107.0 wheels uploaded to PyPI
11:46Infected Docker images published; latest, main-latest and main-stable now point to them
11:53 - 11:54A third developer's token deletes the staging and production workflow run records

Each token had a job: the first changed the pipeline, the second approved the release, the third cleaned up.

The one-line payload injection

The application code was never touched. The attack lived entirely in packaging.

LiteLLM builds its wheels with maturin. The attacker added one line to pyproject.toml:

● ● ●DIFF
 [tool.maturin]
 manifest-path = "litellm-rust/crates/python-bridge/Cargo.toml"
 module-name = "litellm.rust_bridge._native"
 python-source = "."
+data = "cook"
 python-packages = ["litellm_cli"]

That tells maturin to ship whatever is in a cook/ directory as package data, which pip then installs into the root of site-packages. The cook/ directory held the payload. It wasn't in the published source tarball, and the official v1.107.0 git tag doesn't contain the change either. If you audited the tag, you saw clean code.

The "Verified" badge and the provenance nobody read

Two more details matter here:

  • The attacker's commits in the lookalike repo were authored as Berri-AI <[email protected]>, and nine of the ten showed GitHub's "Verified" badge. That badge means GitHub signed the commit for whichever account made it. It says nothing about whether that account is who you think it is.
  • The Docker images carried valid build provenance. Socket noticed that the provenance listed the source owner as Berri-AI, one hyphen away from the real BerriAI. The v1.106.2 provenance shows the real owner. The signature was valid, but the source repo in it was wrong.

Why this link broke: Every control in the pipeline trusted a credential as a stand-in for a person. Branch protection, approval gates and audit logs are all two-person rules on paper. With three stolen tokens, one attacker was all three people.


Layer 4: The Payload - What the LiteLLM 1.107.0 Malware Does

Each malicious wheel installed three extra files into site-packages:

FileRole
cook.pthStartup hook: makes Python import cook on every interpreter start
cook.pyCredential stealer that calls itself privcollect
sitecustomize.pyDetached loader daemon that checks for remote code every hour

How the cook.pth startup hook works

Python's site module processes .pth files in site-packages at startup, and any line beginning with import gets executed. So cook.pth runs the stealer every time Python starts in that environment - your app, a test run, pip itself, a one-off script. You never had to import litellm.

sitecustomize.py is another module Python imports automatically at startup. Wiz said using both .pth and sitecustomize.py together "appears to be the first documented case in the wild."

What the stealer collected

The stealer ran 16 collection modules in a background thread. Per the analysis in GitHub issue #47826 and StepSecurity's write-up:

CategoryTargets
CloudAWS, Azure, GCP and Kubernetes credentials; AWS IMDSv2 and ECS task role credentials (validated with sts:GetCallerIdentity); GCP and Azure metadata tokens
DeveloperSSH keys, GPG keys, .git-credentials, GitHub CLI, .npmrc, .pypirc, Maven, Gradle, Cargo, Composer, Docker configs, .env files, *.pem / *.key / *.pfx
CI / infraKubernetes service account tokens, /run/secrets, GitLab Runner, Jenkins, Vault, Terraform, Packer
BrowsersChromium saved passwords (decrypted), cookies and web data; Firefox logins
AI toolsConfigs and tokens for 20+ AI coding tools, including Claude, Codex, Gemini, Cursor, Windsurf, Copilot and Hugging Face tokens
ProcessesEnvironment variables of every readable process on Linux
LiteLLM proxyIf DATABASE_URL is set, it dumps the proxy's credentials and model tables - every upstream provider key
HistoryShell, database and REPL histories, network info

The data was encrypted (AES-256-CBC, key wrapped with RSA-4096) and sent over plain HTTP to www.litellm.dev/healthcheck - a domain registered on October 13 to look like LiteLLM infrastructure. The real project domain is litellm.ai. Because of the encryption, you can't tell from a packet capture what was taken.

The dormant second stage

The sitecustomize.py loader forked a detached daemon, took a lock file so only one copy ran, and every hour read a domain name from a file in the attacker's GitHub repo. If a domain was there, it would download Python code from it and execute it.

At release time that file was empty, so the second stage never fired, and GitHub took the repo down that afternoon. But the daemon keeps running on any infected host until you kill it. StepSecurity's warning is worth repeating: "killing the daemon is part of recovery, not just removing the package."

On Windows, the loader can't fork and fails silently, but the stealer still runs.


Layer 5: The Fallout

Exposure window

ArtifactWindow (UTC, Oct 21)
PyPI litellm==1.107.0 (all 7 wheels: macOS, Linux glibc and musl, Windows)11:39 - 13:23
GHCR v1.107.0, latest, main-latest, main-stable (litellm, litellm-database, litellm-non_root)11:46 - 13:51
Docker Hub litellm/litellm same tags11:46 - 14:20

Wiz counted about 49,000 downloads of the malicious version, citing ClickPy. That number includes mirrors and CI caches, so it isn't a count of victims, but it's still a large number. Anything with an open-ended constraint like litellm>=1.100 or a floating latest image tag picked it up automatically.

Detection was fast. Socket's scanner flagged the wheels about seven minutes after they hit PyPI, and the compromise was reported publicly in GitHub issue #47826 at 12:06 UTC. LiteLLM had the package off PyPI within about 1h44m.

The stolen keys got used

The stolen credentials were put to use quickly. Wiz observed the attacker validating stolen credentials about four and a half hours after exfiltration, between 16:20 and 17:01 UTC, from two IPs:

  • Each harvested AWS role credential (from CI runners and Kubernetes nodes) was checked with sts:GetCallerIdentity
  • Then came Bedrock and model-listing calls, which Wiz called "consistent with LLMjacking" - using stolen cloud access to run expensive AI models on the victim's bill
  • In a small number of environments, the attacker enumerated IAM roles, users and access keys, and checked CloudTrail configuration

Response

LiteLLM's post said the compromise was "Contained," with the official v1.107.0 tag clean and "All previous versions are safe." Enterprise customers using LiteLLM's private registry were not affected. CEO Krrish Dholakia said: "Our immediate priority is ensuring customers are protected and preventing this from happening again."

The safe version to pin is 1.106.2.


Why LiteLLM's March Hardening Didn't Stop It

After March, LiteLLM's pipeline had most of what supply chain guidance recommends. Here's how each control held up:

Control added after MarchWhat the attacker did
Releases from a private repoUsed developer tokens that already had access
Branch protection on the release workflowOrg owner's token overrode it in five seconds
Approval gate for productionA second stolen token approved it
Trusted Publishing to PyPIRan the real pipeline, so PyPI accepted the real identity
Immutable Docker tagsPublished a brand-new version instead of retagging an old one
Signed imagesThe legitimate pipeline signed them
Audit trail of workflow runsA third stolen token deleted the run records

LiteLLM put it this way: the attacker "had to compromise multiple independent security controls to be successful." That's true, but the controls weren't independent. They all bottomed out in the same thing - a GitHub token - and the attacker had three.

Trusted Publishing and signing answer the question "did this come from our pipeline?" They don't answer "should our pipeline have built this?" When the pipeline itself is hijacked, the signature is real.

March vs October

March 2026October 2026
Versions1.82.7, 1.82.81.107.0 (PyPI + Docker)
EntryPoisoned Trivy security scanner in CI (TeamPCP)Exposed internal LiteLLM deployment
What was stolen to publishPyPI publishing tokenThree developers' GitHub CLI tokens
Startup hooklitellm_init.pthcook.pth + sitecustomize.py
Exfil domainmodels.litellm.cloudwww.litellm.dev

Socket said it found no evidence linking the two, noting different encryption keys, exfiltration and backdoor behavior - while adding, "That doesn't rule out the same operator using different tooling." Wiz also said the October malware showed strong signs of AI-assisted writing.


LiteLLM 1.107.0 Attack Timeline

Date (UTC)Event
Mar 24, 2026First compromise: LiteLLM 1.82.7 and 1.82.8 published with stolen PyPI token via the Trivy compromise
Oct 13litellm.dev registered; attacker GitHub account letters9813 created
Oct 18Dead-drop repo for the remote loader created
Oct 19LiteLLM temporarily expands release permissions for two engineers
Oct 21, 08:56Attacker starts pushing commits to lookalike Berri-AI/litellm
Oct 21, 10:08Branch protection rejects the workflow change, then gets overridden
Oct 21, 11:39Malicious wheels on PyPI
Oct 21, 11:46Malicious Docker images live; Socket flags the wheels
Oct 21, 11:53Workflow run records deleted
Oct 21, 12:06Public report in GitHub issue #47826
Oct 21, 13:23PyPI removes 1.107.0
Oct 21, 14:20Docker Hub tags removed or repointed to clean images
Oct 21, 16:20 - 17:01Wiz observes stolen AWS credentials being validated and Bedrock probed

Am I Affected? How to Check, Clean Up and Recover

Step 0: Check if you installed LiteLLM 1.107.0

Check from the shell, not from Python. Every Python start in an infected environment re-runs the payload, so don't use python, pip or uv to investigate an environment you suspect.

● ● ●BASH
# Payload files anywhere on the box
find / -path '*site-packages*' \( -name 'cook.pth' -o -name 'cook.py' \) 2>/dev/null
find / -path '*site-packages*' -name 'litellm-1.107.0.dist-info' 2>/dev/null

# Host markers
ls -la /tmp/.pc_once_72129118c59f5636c0bb /tmp/remote_code_loader.lock 2>/dev/null

# Lockfiles and requirements in your repos
grep -rnE 'litellm[^0-9]*1\.107\.0' --include='*.txt' --include='*.lock' --include='*.toml' .

# Docker images you pulled
docker images --digests | grep -i litellm

Also check for any sitecustomize.py in site-packages you didn't put there. For containers, check which image digest your deployments pulled on October 21 - a floating latest tag tells you nothing about what was running at the time.

Finding the files confirms exposure. Not finding the marker file doesn't clear you: the marker is only written after a successful upload, and you may have already rebuilt the environment.

Step 1: Kill the loader first

Before you clean anything, stop the daemon:

● ● ●BASH
# Find and kill whatever holds the loader's lock
lsof /tmp/remote_code_loader.lock
kill -9 <pid>

Then remove cook.pth, cook.py, sitecustomize.py and their __pycache__ entries from every affected site-packages. Better yet, don't repair - rebuild the virtualenv or image from scratch.

Step 2: Pin a clean version

● ● ●BASH
# In a fresh virtualenv, not the infected one
pip install 'litellm==1.106.2'

For Docker, pin v1.106.2 by digest, not by tag, and purge cached v1.107.0 layers from your registries, pull-through caches and PyPI mirrors (some mirrors kept serving it after PyPI removed it).

Step 3: Rotate, in the right order

If 1.107.0 ran on a host with outbound internet access, assume exfiltration succeeded. Rotate everything that host could reach, roughly in order of blast radius:

PriorityWhat to rotate
1Cloud credentials: IAM role sessions, access keys, service accounts, Kubernetes tokens
2Source and package registry tokens: GitHub, PyPI, npm, Docker registries
3LiteLLM proxy: every provider API key in the database, the master key, salt key, virtual keys, and the DATABASE_URL password
4CI secrets, Vault tokens, Terraform credentials
5SSH and GPG keys, TLS private keys
6AI tool tokens (Claude, Codex, Cursor, Gemini, Copilot, Hugging Face)
7Browser-saved passwords and sessions on developer machines

Rotate from a clean machine. Rotating from an infected one just hands the new secrets over.

Step 4: Hunt for credential abuse

● ● ●BASH
# Who called GetCallerIdentity around the exposure window?
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=GetCallerIdentity \
  --start-time 2026-10-21T11:00:00Z --end-time 2026-10-22T00:00:00Z

Look for your instance or task role credentials being used from IPs outside your infrastructure, Bedrock ListFoundationModels or invocation calls you don't recognize, and IAM enumeration (ListRoles, ListUsers, ListAccessKeys). Check your provider dashboards (OpenAI, Anthropic, etc.) for usage spikes on keys that lived in the LiteLLM database.

Step 5: Block and alert

  • Block litellm.dev and www.litellm.dev at DNS and egress
  • Alert on any request to a /codepy path - that means second-stage code ran and your scope is much bigger
  • Alert on new .pth or sitecustomize.py files appearing in site-packages

Indicators of compromise

TypeValue
Exfil domainlitellm[.]dev, www.litellm[.]dev/healthcheck (plain HTTP POST)
Exfil headerX-Token: XWiA8gZNa7NcX2XRoAEb
Second stagehttp://<domain>/codepy
Dead dropraw.githubusercontent.com/letters9813/pythonclient-test/main/domain.txt (deleted)
Lookalike repogithub.com/Berri-AI/litellm, commit 20535d8fa11f786a6c24df7488a0e449ed5f09ad
Attacker identityBerri-AI <[email protected]>
Filescook.pth, cook.py, sitecustomize.py in site-packages
Markers/tmp/.pc_once_72129118c59f5636c0bb, /tmp/remote_code_loader.lock, env PRIVCOLLECT_ACTIVE=1
Credential validation IPs (Wiz)168.100.8[.]57, 165.227.201[.]8
Docker Hub image indexsha256:1bbcb1443bc962e45f5aad53fe3d2c30600e0379715fb6c664cdd3ac126c339e

Per-wheel SHA-256 hashes and every image digest are in GitHub issue #47826 and LiteLLM's incident post.

Long-term hardening

If you consume packages:

AreaActionWhy
Pin exactlyLock exact versions with hashes (uv lock, pip install --require-hashes)litellm>= pulled 1.107.0 automatically
Release cooldownDon't install versions younger than a few days (uv exclude-newer, Renovate minimumReleaseAge, Dependabot cooldown)This one was live for under two hours
Pin images by digestNever deploy latest or main-stableFloating tags pointed at the malware for hours
Egress controlAllowlist outbound traffic from CI runners and gatewaysNo egress, no exfiltration
Short-lived cloud credsIMDSv2, scoped roles, short sessionsLimits what a stolen session can do
Watch startup hooksAlert on new .pth / sitecustomize.py filesThe whole attack lived there

If you run an AI gateway:

AreaActionWhy
Don't log full bodiesTurn off or redact request/response body logging in productionGateway logs held the tokens that started this
Never expose internal gatewaysPut them behind SSO or a private networkThe entry point was an internet-facing internal deployment
Lock down the key databaseRestrict who and what can reach DATABASE_URL; consider a secrets manager for provider keysThe stealer went straight for it
Per-team virtual keysIssue scoped, revocable keys instead of sharing provider keysMakes rotation and blast radius manageable

If you publish packages:

AreaActionWhy
Scoped, short-lived tokensPrefer fine-grained, expiring tokens over broad CLI OAuth tokens for org adminsOne owner token beat branch protection
Alert on overrides and deletionsWatch GitHub audit log events like protected_branch.policy_override and workflows.delete_workflow_runBoth happened here, minutes apart
Check provenance contentVerify the source repo in your build provenance, not just that a signature existsThe provenance said Berri-AI the whole time
Diff artifacts against the tagRebuild from the tag and compare before or right after publishingThe tag was clean; the wheels weren't
Strong approversRequire approvals from hardware-key-backed sessions, not tokensA stolen token approved production

The Bigger Picture

Let's zoom out.

1. AI gateways are high-value targets now. A LiteLLM proxy holds every model key a company has, sits next to cloud credentials, and logs everything developers and agents send to models. This payload went straight for the proxy database and for AI coding tool configs.

2. Logs are a secret store whether you meant them to be or not. Agents read files, run commands and pass results through model conversations. Anything that enters that context can end up in a gateway log. If you log bodies, you're storing secrets.

3. Signatures prove origin, not intent. Trusted Publishing, signed images and provenance are all valuable, and they did their job here. They proved the artifacts came from LiteLLM's pipeline. They can't prove the pipeline wasn't hijacked. You have to read what the provenance says, not just check that it exists. Same lesson as the eMudhra CA key compromise a few days earlier: a valid signature only tells you which key signed something.

4. Two-person rules need two people. Branch protection plus approvals plus audit logs looks like defense in depth. When every one of them is satisfied by a token, three stolen tokens make one attacker into a whole team.

5. AI tooling keeps getting hit in 2026. LiteLLM in March, the Vercel breach through an AI tool's OAuth access in April, and LiteLLM again in October. Attackers go where the credentials are, and AI tooling now holds a lot of them.


FAQ

Which LiteLLM versions are affected?

Only litellm==1.107.0 on PyPI (all seven wheels) and the v1.107.0 Docker images on GHCR and Docker Hub, plus the latest, main-latest and main-stable tags while they pointed at it on October 21. LiteLLM says all previous versions are safe. Pin 1.106.2.

Is LiteLLM safe to use now?

The malicious version was removed from PyPI and Docker on October 21, and LiteLLM revoked the compromised credentials. Pin an exact earlier version such as 1.106.2, pin Docker images by digest, and wait for LiteLLM's full root cause report before moving to new releases.

How do I know if I installed LiteLLM 1.107.0?

Look for cook.pth, cook.py or a litellm-1.107.0.dist-info folder in any site-packages directory, the lock file /tmp/remote_code_loader.lock, and 1.107.0 in your lockfiles. Check from the shell, not from Python, because starting Python in an infected environment re-runs the payload.

What did the LiteLLM malware steal?

Cloud credentials (AWS, Azure, GCP, Kubernetes), SSH and GPG keys, GitHub and package registry tokens, browser passwords and cookies, CI secrets, AI coding tool tokens, environment variables of running processes, and every provider API key stored in the LiteLLM proxy database.

Did Claude Code cause the LiteLLM hack?

Not directly. LiteLLM says it has seen Claude Code put a user's GitHub CLI token into a response body, which its gateway then logged. The attacker read those logs on an internal deployment that was exposed to the internet. The underlying problems were the exposed deployment and logging full request and response bodies. I couldn't find a public response from Anthropic at the time of writing.

Is this the same attack as the March 2026 LiteLLM compromise?

There's no evidence linking them. March came through the poisoned Trivy scanner and a stolen PyPI token; October came through stolen GitHub tokens. Socket found different encryption keys, exfiltration and backdoor behavior, but said that doesn't rule out the same operator.


Conclusion

An internal LiteLLM deployment was exposed to the internet. Its logs held three developers' GitHub tokens. One token overrode branch protection, one approved the release, one deleted the evidence. The real pipeline built a poisoned package from a lookalike repo, signed it, and published it to PyPI and Docker. For under two hours, every unpinned install got a credential stealer - and within five hours, the stolen keys were being tested against Bedrock.

All of this got past controls that LiteLLM added specifically because of the attack in March.

If you installed 1.107.0, don't just upgrade. Kill the loader, rebuild, and rotate everything that machine could reach. If you didn't, use this as the push to pin exact versions, pin images by digest, and stop logging full bodies in your AI gateway.


◆ KEY TAKEAWAY

Signed doesn't mean safe: LiteLLM 1.107.0 was built, signed and published by the real pipeline using stolen developer tokens that leaked through AI gateway logs. If you installed it, kill the loader before touching Python, rebuild from scratch, and rotate every credential the host could reach. Everyone else: pin exact versions with hashes, pin images by digest, add a release cooldown, and lock down your AI gateway's logs and key database.


References

© 2026 Dhruv VakharwalaNext.js · TypeScript · Tailwind · Cloudflare