Connect with us

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.

Published

on

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

  1. 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.
  2. 10:08 UTC: Branch protection rejects the workflow edit, then a privileged token force-pushes it.
  3. 10:46 UTC: Staging runs against Berri-AI/litellm at 20535d8f and finishes at 11:08:09.
  4. 11:08 to 11:11 UTC: Production starts; a second developer token clears the approval gate.
  5. 11:39:51 to 11:40:13 UTC: Seven wheels upload to PyPI. Docker publication follows at 11:46:56.
  6. 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.

Harry is the editor and publisher of MY WORLD NEWS 24, an independent title under his own ownership. Ten years of reporting and then editing taught him that a global readership is not served by assuming everyone lives in the same country. Stories here state currencies, units and time zones explicitly, name the country a law or a company belongs to, and explain local context rather than treating it as known. That care extends to sourcing: a claim is anchored to the filing, statement, transcript or dataset that made it, wherever in the world it was issued, and each figure is checked against that source before publication. The site reports news, business and technology, science and sports, entertainment, lifestyle and travel, and auto and gaming, all with the same standard of evidence. Mistakes are fixed under a corrections policy anyone can read, and the page carries a note saying what was changed. Harry reads every message sent by readers and replies from support@myworldnews24.com.

Click to comment

Leave a Reply

Your email address will not be published. Required fields are marked *

Trending