LiteLLM 1.107.0 - A Key Nobody Revoked → An Owner Token Inside an Agent Platform → CI Secrets Posted to a Webhook → A Lookalike Repository in the Release Workflow → Validly Signed Malware
This is LiteLLM's second supply chain compromise of 2026, and it is a separate incident from the March TeamPCP attack. After March the project moved releases to a private repository, adopted Trusted Publishers, made Docker tags immutable and began signing images. The October attack went around those controls with something simpler. On May 27 an administrative key for an internal service, the LiteLLM Agent Platform, was committed to a public repository. The team removed the file about three hours later and noted the key needed revoking, and nobody did. On October 10 an attacker used it to read that platform's agent configuration, which held the GitHub CLI OAuth token of one of the organization's two owners. On October 21 the attacker changed the release workflow to build from a lookalike repository and published litellm==1.107.0 to PyPI, GHCR and Docker Hub with a credential stealer inside. LiteLLM's first post suspected tokens scraped from logs of an internal deployment. Its later attack-chain record corrects that, and this page follows the correction. The PyPI release was live for about an hour and 44 minutes and Docker Hub tags for about two and a half hours. Wiz, as reported by The Register, put downloads at about 49,000.
Disclosure: research published by Wiz is one of the sources cited on this page, and the site's author works at Wiz. Every source is linked so you can read it directly.
📄 LiteLLM - Attack chain record (canonical) ↗ 📄 LiteLLM - Initial incident post ↗ 📄 StepSecurity - Payload analysis ↗At 18:53:14 UTC on May 27, 2026, an opencode.json file was committed to the public LiteLLM-Labs/lite-harness repository. It held an administrative key to an internal deployment of the LiteLLM Agent Platform, an experimental service for self-hosted agents that file pull requests.
The team removed the file about three hours later and noted the keys needed revoking. Nobody followed through, and the original commit stayed in public history until October 22. Removing a secret from a repository's current state leaves it valid and, in this case, still readable.
Removed: about three hours later
Revoked: not before the attack, which LiteLLM puts down to human error
In public history until: October 22
Between 21:13 and 21:28 UTC on October 10, the attacker made authenticated API requests with the admin key from 69.48.229.143. That is 136 days after the commit. LiteLLM's records do not say when the attacker found the key.
Source: 69.48.229.143
Credential: the admin key from the May commit
At 21:27:10 a GET /api/v1/managed_agents/agents?limit=100 returned the configuration of every managed agent. The platform had been given a GitHub token so it could open pull requests, and that token was the GitHub CLI OAuth token of one of the organization's two owners, in place of a scoped service credential.
LiteLLM notes that such tokens are unscoped and are not subject to GitHub's super-user 2FA reauthentication. With it, the attacker held owner capabilities over the whole organization. An experimental internal tool had become the most valuable system LiteLLM ran.
Returned: all managed agent configurations
Token: GitHub CLI OAuth token of one of two organization owners
Why it mattered: unscoped, and no 2FA reauthentication prompt
Within the hour the attacker used CI to collect more identities. At 22:11:45 a workflow on a branch of BerriAI/litellm posted six secrets to a webhook.site endpoint, including GH_TOKEN, a second maintainer's GitHub CLI token. At 22:12:09 a workflow in BerriAI/litellm-auto posted RELEASE_PLEASE_TOKEN, a third maintainer's token. Both runs were deleted within about three minutes.
Between 12:08 and 12:30 UTC on October 12, workflows on a variety of private repositories posted all their secrets to the same endpoint. LiteLLM says those were minimally sensitive and played no part in the path to the poisoned artifacts. The domain litellm.dev was registered on October 13, per StepSecurity.
22:12:09 UTC October 10: a third maintainer's token posted
22:14 UTC: both runs deleted with the owner's token
October 13: litellm.dev registered, per StepSecurity
On October 21 at 10:08:29 UTC, commit 9cf97e9 was pushed to main of BerriAI/project-releaser with the second maintainer's token, and branch protection rejected it. The change added a source_owner input where the workflow had hardcoded BerriAI. At 10:08:34 the owner's token pushed the same commit, overriding the protection.
At 10:46:39 staging was dispatched from Berri-AI/litellm, a lookalike organization one hyphen away from the real one. The production gate was approved at 11:11:18 with the second maintainer's token. LiteLLM says that approval rule had been relaxed earlier that week, allowing any two engineers to trigger and approve a release so that bugfix backports could go out.
10:08:34 UTC: owner token overrides protection
10:46:39 UTC: staging dispatched from Berri-AI/litellm
11:11:18 UTC: production gate approved by a second account
Seven litellm==1.107.0 wheels reached PyPI between 11:39:51 and 11:40:13, and images followed at 11:46:56 on Docker Hub and GHCR under v1.107.0, latest, main-latest and main-stable. The official v1.107.0 source tag did not contain the malicious files, and the private enterprise registry was not affected.
The images carry a valid signature from the previous signing key, because the real pipeline signed them. LiteLLM believes that key was exposed and rotated it on October 23, so verification alone will pass on these images, and digests have to be checked against the affected list. At 11:53:49 the third maintainer's token deleted the production and staging run records, removing the workflow logs.
Docker Hub: 11:46:56 to 14:20 UTC
GHCR: until 13:51 UTC
Signature: valid under the previous key, rotated October 23
Cleanup: run records deleted at 11:53:49 UTC
Each wheel installs cook.pth, cook.py and sitecustomize.py. The .pth file runs on every interpreter start, whether or not anything imports litellm, and that includes pip itself, which is why LiteLLM warns against reinstalling before removing the files. This is the same class of startup trick as the March attack.
StepSecurity counts 16 collection modules covering browser data, SSH and cloud credentials, Kubernetes and CI secrets, instance metadata, the environment of readable processes, configs for over 20 AI coding tools, and on proxy hosts the LiteLLM database with its stored provider keys. Data left by plain HTTP POST to www.litellm.dev, wrapped in AES-256 and RSA-4096. A daemon polled a GitHub-hosted domain.txt every hour for code to run. That file was empty at release, so StepSecurity says the second stage was never armed while the dead drop existed. Infected hosts keep polling the same URL, though, and anyone who re-registers the account could reach them, which is why the daemon has to be killed.
Trigger: every Python interpreter start, including pip
Exfiltration: HTTP POST to www.litellm.dev/healthcheck, RSA-wrapped
Loader: hourly domain.txt poll, empty at release
Between 16:20 and 17:01 UTC on October 21, stolen AWS instance role credentials were validated from external addresses, as reported by The Hacker News from Wiz's observations. StepSecurity says to look in CloudTrail for GetCallerIdentity calls from the instance or task role around install time, then check whether those credentials were used from outside your infrastructure.
LiteLLM now assumes every token, key and authenticator was taken. It is revoking tokens across internal systems and vendor platforms, cut its GitHub organization down to one temporarily promoted account, and removed GitHub permissions from the release workflow. Its process-improvement write-up was still promised at the time of the attack-chain record.
CloudTrail signal: GetCallerIdentity around install time, then use of the role from outside
LiteLLM's position: assume every token, key and authenticator was exfiltrated
🛡 How to Defend Against This Chain
latest./tmp/remote_code_loader.lock. Rebuild from a clean version and rotate everything reachable: the instance role, provider keys in the proxy database, master keys and DATABASE_URL. A missing marker file proves nothing.