NEWS
LiteLLM Says Gateway Logs Fed the 1.107.0 Attack
Stolen GitHub tokens, which LiteLLM says may have sat in its own proxy logs, signed 1.107.0 wheels that stole cloud and AI keys.
1.107.0 sat on PyPI for 103 minutes on October 21 and dropped a credential stealer that ran every time Python started. The same build went out on Docker Hub and GHCR under v1.107.0, latest, main-latest, and main-stable. ClickPy counted about 49,000 downloads of the PyPI wheels before the index pulled them.
The official v1.107.0 git tag did not contain the malware, which lived in the seven wheels and the Docker images. Those artifacts were signed by LiteLLM’s own release pipeline after stolen GitHub CLI tokens walked through controls the project added after a March attack on the same package, an open-source AI gateway that records about 23 million weekly downloads.
Malicious LiteLLM 1.107.0 Sat on PyPI for 103 Minutes
LiteLLM, built by BerriAI, is a proxy that gives one interface to OpenAI, Anthropic, Google Gemini, and other model hosts. Version 1.107.0 reached PyPI at 11:39:51 UTC on October 21 and stayed up until 13:23 UTC. Docker Hub served image index sha256:1bbcb1443bc962e45f5aad53fe3d2c30600e0379715fb6c664cdd3ac126c339e from 11:46:56 to 14:20 UTC. GHCR pointed the same tags at infected indexes from 11:46:56 until 13:51 UTC.
CEO Krrish Dholakia, CTO Ishaan Jaffer, Director of Security Oliver Jensen, and Lead Rust Engineer Nate Armstrong signed the company’s incident post. Status is incident contained, investigation in progress, last updated October 21 at 20:30 UTC. They tell OSS users to pin your version to 1.106.2 or any explicit release from before that morning. Enterprise customers pulling the private registry were not hit. That registry is also shipping an embargoed fix for a separate bug whose disclosure is set for October 27.
AFFECTED LITELLM ARTIFACTS ON OCTOBER 21
| Artifact | Where it shipped | Window (UTC) | Outcome |
|---|---|---|---|
| litellm==1.107.0 (all seven wheels) | PyPI | 11:39:51 to 13:23 | Removed |
| v1.107.0, latest, main-latest, main-stable | Docker Hub | 11:46:56 to 14:20 | Removed or repointed |
| v1.107.0, latest, main-latest, main-stable | GHCR (litellm, litellm-database, litellm-non_root) | 11:46:56 to 13:51 | Tags made unavailable |
| Official v1.107.0 git tag | GitHub | Not applicable | No malware in the tagged source |
Socket’s scanner flagged the seven wheels about seven minutes after they landed. Lidor Machluf opened GitHub issue 47826 around 12:06 UTC and later showed that the files inside the official Docker image were byte-identical to the PyPI copies. Wiz researchers Benjamin Read and Rami McCarthy, citing ClickPy, put downloads of the bad version at about 49,000. Anyone who ran Python after that install, including in a proxy container or a CI runner, should treat secrets on that host as stolen.
The Release Pipeline Was Already Hardened Once This Year
This was the second time in 2026 that a LiteLLM PyPI release carried a startup hook. On March 24, versions 1.82.7 and 1.82.8 went out after TeamPCP poisoned Aqua Security’s Trivy scanner and LiteLLM’s CI installed that scanner unpinned. CloudSEK later mapped 2,500 companies and 434,000 CI/CD pipelines in a reconstructed exposure set from that campaign. Those packages were live about 40 minutes.
LiteLLM then hardened our CI release pipeline, in the company’s words. Releases moved to a private repo. Publish used GitHub Trusted Publishers. Docker tags were made immutable and images were signed. The normal flow cuts a release-candidate branch on Friday, qualifies it in public CircleCI and private Buildkite, builds artifacts in the private repo, tests those artifacts, then publishes from an account locked to a laptop that stays in the office. No Docker image is rebuilt at publish time.
On October 19 the team temporarily widened release rights so two internal engineers could ship daily bugfix backports. Socket researchers said the public evidence does not tie October to the March operator: the embedded RSA keys, the exfil path, and the backdoor behavior differ. The October steal still used the same class of Python startup hook.
Claude Code Tokens in Gateway Logs Opened the Door
LiteLLM says the attacker held three engineers’ GitHub CLI OAuth tokens. One token could run workflows. One belonged to one of the two GitHub organization owners and was used to turn off branch protection and force-push a workflow change. A second account approved the production gate. A third deleted staging and production workflow records after the publish, which cut the logs investigators could read. All tokens used in the attack have been revoked, the company said.
We identified an unexpected, innocuously named user account in our internal LiteLLM deployment, which was exposed to the public internet. We believe the attacker gained access to this deployment, obtained the GitHub CLI OAuth tokens from its logs, and subsequently used those credentials to compromise our release workflow.
Oliver Jensen, Director of Security, LiteLLM incident report
The same post says Claude Code can embed a user’s GitHub CLI OAuth token in a response body, and that those tokens can show up in LiteLLM logs when responses are stored. How the unexpected account got into the internet-facing deploy is still open. Maintainers currently suspect a recently patched bug.
WHAT WE KNOW
- Stolen tokens: Three GitHub CLI OAuth tokens from engineer accounts were used to launch, approve, and then erase a private releaser run.
- Owner override: Branch protection blocked the workflow edit at 10:08:29 UTC, then a privileged token forced commit 9cf97e935aeaf791b9c8db224a2bb3ee156134ce through five seconds later.
- Monday change: Release rights had been widened on October 19 for two engineers shipping customer backports.
WHAT IS UNCONFIRMED
- Log harvest: LiteLLM believes the tokens were copied from logs on that public internal deploy, after Claude Code wrote them into responses.
- Initial entry: Maintainers have not confirmed how the unexpected user reached that deploy.
- Actor link: Socket finds no evidence that the October operator is the March TeamPCP cluster.
Trusted Publishers and image signing still ran. They signed whatever the private workflow was pointed at. People tokens, not a broken publisher integration, were the path around the March rebuild.
Berri-AI, One Hyphen From the Real Org
The poisoned artifacts were not built from BerriAI/litellm. Machluf’s issue traces SLSA provenance on every v1.107.0 image to the lookalike repo Berri-AI/litellm at commit 20535d8fa11f786a6c24df7488a0e449ed5f09ad, via private workflow run 39587059944. The same releaser had recorded BerriAI/litellm as the source for v1.106.2 earlier that morning. github.com/Berri-AI now returns 404. The commits remain readable through the parent repo because Git shares objects on a fork, and they sit on no branch and no tag.
Between 08:56 and 10:46 UTC the attacker pushed 10 commits, most with author Berri-AI and the Proton address ckdi82fkoos@proton.me. The first nine were made in GitHub’s web editor, so they carry GitHub’s verified badge. That badge only means the click happened on github.com. The first three commits, inside two minutes, added cook.pth, cook.py, and sitecustomize.py. Four more set the version back from 1.108.0 to 1.107.0. After a 39-minute gap, a commit added data = “cook” to pyproject.toml, which told the maturin build tool to pack a cook/ folder into each wheel. A last, unsigned commit from a noreply address copied that folder into the Dockerfiles.
THE MORNING THE RELEASER RAN ON BERRI-AI
- 08:56 to 10:46 UTC: Ten commits land on the hyphenated fork, adding the three startup files, the maturin data line, and the Docker COPY.
- 10:08 UTC: Branch protection rejects the workflow edit, then a privileged token force-pushes it.
- 10:46 UTC: Staging runs against Berri-AI/litellm at 20535d8f and finishes at 11:08:09.
- 11:08 to 11:11 UTC: Production starts; a second developer token clears the approval gate.
- 11:39:51 to 11:40:13 UTC: Seven wheels upload to PyPI. Docker publication follows at 11:46:56.
- 11:53 UTC: A third token deletes the workflow records. By 12:24 the workflow is restored. PyPI removes 1.107.0 by 13:23. GHCR tags are gone by 13:51. Docker Hub is cleaned by 14:20.
Read and McCarthy noted verbose names, broad try/except blocks, and a Windows marker path under /tmp/, and said the payload looks like it was built with iterative model prompting. The git tag stayed clean the whole time, which is why a source audit of v1.107.0 would have passed.
cook.pth Fires on Every Python Start
Each wheel placed three extra files in the root of site-packages, beside LiteLLM rather than inside it. Nothing in the app has to import litellm. Python’s site module loads .pth files run at interpreter startup, and it also imports sitecustomize.py. pip, an editor language server, and a CI job that never calls the gateway all fire the same hooks. Isolated mode with python -I still loads them. Only python -S, which skips the site module, skips the payload.
THREE FILES DROPPED IN SITE-PACKAGES
- cook.pth: A 12-byte file whose only line is import cook, hashed SHA256 7f191632e7320705d154b85204f1ae546b543acbd7e8861229b76f5042b92bdb, which makes cook.py run on every start.
- cook.py: A 22,252-byte one-liner that decompresses and runs the privcollect stealer, then writes /tmp/.pc_once_72129118c59f5636c0bb so the sweep runs once per host.
- sitecustomize.py: A 25,509-byte loader that forks a daemon, holds /tmp/remote_code_loader.lock, and every 3600 seconds reads a GitHub dead-drop for a domain, then execs whatever that host returns at /codepy.
Aikido malware hunter Ilyas Makari said to check /tmp/.pc_once_* (exfil already fired if that file exists) and /tmp/remote_code_loader.lock (the daemon ran or is still running). The marker path often does not exist on Windows, so a missing file proves nothing. sitecustomize.py’s fork path is built for Linux and macOS.
The dead-drop was github.com/letters9813/pythonclient-test/main/domain.txt. Machluf’s issue says its history was localhost, then test.test.com, then empty from October 20 at 09:15:14 UTC. GitHub took the repo down around 13:30 UTC on October 21. No second-stage commands are known to have reached victims through it. One commit would re-arm every infected host. LiteLLM’s first cleanup rule is not to start pip inside a suspect venv, because that start would run both files again.
What the Stealer Took From Proxy Hosts
privcollect harvests across 16 modules, then encrypts the bundle with AES-256-CBC, wraps the key with a hardcoded RSA-4096 public key, and POSTs it in plain HTTP to www.litellm.dev/healthcheck with an Internet Explorer 7 user-agent and header X-Token. litellm.dev was registered on October 13, eight days before the release, and is not the project’s domain. LiteLLM’s real site is litellm.ai. Makari stressed that the lookalike is attacker-controlled.
Cloud Keys, Browser Vaults, and AI Tool Files
Wiz and Aikido both describe a sweep built for a developer laptop and for a cloud node. Browser vaults and cookies come from Chrome, Firefox, Edge, and Safari. Cloud collection reads ~/.aws, environment variables, and EC2/ECS instance metadata, plus GCP service-account keys and application-default credentials, and Azure CLI tokens. It also hits live instance-metadata endpoints. SSH keys, GPG keys, TLS keys, and Bash, Zsh, and Fish history go out with them.
On Linux the stealer reads every other process’s environment from /proc. It also takes Docker config and Swarm secrets, Kubernetes service-account tokens, GitLab runner files, Jenkins keys, Vault tokens, git and npm and PyPI config, and credential files for Claude Code, Claude Desktop, Cursor, GitHub Copilot, Windsurf, Gemini CLI, Amazon Q, Codex CLI, Aider, Continue, and Hugging Face. That is the set of tools sitting next to an LLM gateway on a working machine.
The Query Against the Proxy Credential Table
LiteLLM Proxy keeps upstream provider keys in Postgres. If DATABASE_URL is set, a child process uses Prisma and runs SELECT * FROM “LiteLLM_CredentialsTable” LIMIT 500, and also reads LiteLLM_ProxyModelTable. A single infected proxy can therefore hand over the OpenAI, Anthropic, Cohere, and other keys it was meant to hold in one place. Jensen’s post says that if 1.107.0 ever ran on a host with outbound internet, assume the send succeeded. Finding the marker confirms it. Not finding it does not clear the host, because containers and tmp cleanup wipe the file.
WHAT THE STEALER TARGETED
| Class | Copied from the host |
|---|---|
| Browsers | Passwords, cookies, and history from Chrome, Firefox, Edge, and Safari |
| Cloud | AWS files, env, IMDS; GCP keys; Azure CLI tokens; Kubernetes and Docker secrets |
| AI tools | Claude Code, Cursor, Copilot, Codex, Gemini CLI, and related app configs |
| LiteLLM Proxy | LiteLLM_CredentialsTable and LiteLLM_ProxyModelTable, cap 500 rows |
| Host keys | SSH, GPG, TLS, shell history, git/npm/PyPI config, other processes’ env on Linux |
Cleanup starts in the shell, not in Python. Delete cook.pth, cook.py, sitecustomize.py, and matching.pyc files from every affected site-packages tree, kill the daemon holding the lock, then rebuild the venv or image on 1.106.2. 1.107.0rc1 and 1.107.0rc2 are clean. Rotate the instance or task IAM role, every provider key in the proxy database, master and virtual keys, the DATABASE_URL password, and any secret that was in the environment of any process on that host, from a machine that never ran 1.107.0.
Stolen AWS Roles Then Listed Bedrock Models
The hourly loader looks dormant. The stealer did not. Wiz Research said it saw abuse of stolen credentials starting about 4.5 hours after the send. Between 16:20 and 17:01 UTC on October 21, from 168.100.8.57 and 165.227.201.8, the operator checked AWS instance-role credentials taken from CI runners and Kubernetes nodes. Each check used sts:GetCallerIdentity, then bedrock:ListFoundationModels and a ListModels call, a pattern Wiz called consistent with LLMjacking, running someone else’s models on their bill. In a smaller set of accounts the same operator listed IAM roles, users, and access keys and ran broader discovery such as ListAccounts and DescribeTrails.
Jensen’s note already told operators to search CloudTrail for GetCallerIdentity from the instance role around install time, then for use of that role from outside their network. Those two IPs and the Bedrock list calls are the concrete follow-on. Pinning 1.106.2 does not pull a stolen IAM role back. Uninstalling LiteLLM also does not remove cook.pth, which is why a lockfile grep and a site-packages search still matter on hosts that already moved off 1.107.0.
LiteLLM’s public post still showed last updated October 21 at 20:30 UTC, with the investigation open and the next promised status note already in the past. The wheels are gone. The tokens are revoked. Hosts that started Python on 1.107.0, or that pulled latest during that Docker window, still need new keys issued from a clean machine.
-
BUSINESS2 months agoWarsh Rejects Rate Guidance and Still Moves Markets
-
BUSINESS1 month agoDana-Farber Exits an MGB Medicare Advantage Network Early
-
NEWS2 months agoNASA Launches the Roman Space Telescope’s Cosmic Bet
-
NEWS2 months agoOpenAI’s Cyber Letter Puts the Defense Bill on Governments
-
NEWS2 months agoRussia Recycles Its Old Warning Over Storm Shadow Plants
-
ENTERTAINMENT4 weeks agoPrimetime’s $2.7 Million Preview Tops Pitt’s Costlier Film
-
BUSINESS2 months agoJet Drones Lock Down Kyiv and Strip Kherson of Heat
-
ENTERTAINMENT2 months agoMidnight Spider-Man Turns Peter Parker Into Oscorp’s Hybrid
