Get the Zoom link
Cloud Security Office Hours Banner

LiteLLM 1.107.0 Release Hijack 2026

Step-by-step kill chain mapped to MITRE ATT&CK Cloud, sourced from official post-mortems and primary technical analyses.

October 2026 Critical PyPI / AI Infra

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.

136 days between the key's exposure and its first malicious use
~1h 44m live on PyPI, about 2h 33m on Docker Hub
~49,000 downloads of the malicious release, per Wiz via The Register
Threat actor: unidentified; LiteLLM's record does not discuss identity or motive

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 ↗
🔑 Credential Exposure: A Key in a Public Commit
01
An admin key for an internal service was committed to a public repository, removed within hours, and never revoked
T1552.001 - Credentials in Files

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.

Committed: 18:53:14 UTC May 27, opencode.json in LiteLLM-Labs/lite-harness
Removed: about three hours later
Revoked: not before the attack, which LiteLLM puts down to human error
In public history until: October 22
Committed SecretUnrevoked KeyT1552.001
⚡ Initial Access: The Key Still Worked
02
More than four months later the key still opened the Agent Platform API
T1078 - Valid Accounts

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.

Window: 21:13 to 21:28 UTC October 10
Source: 69.48.229.143
Credential: the admin key from the May commit
Admin APIValid CredentialT1078
🔑 Credential Access: The Owner's Token in an Agent Config
03
One API call returned every managed agent's configuration, including a GitHub organization owner's OAuth token
T1528 - Steal Application Access Token

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.

Request: GET /api/v1/managed_agents/agents?limit=100
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
GitHub CLI TokenAgent PlatformT1528
🔑 Credential Access: Two More Maintainers via CI
04
Throwaway branches ran workflows that posted repository secrets to a webhook, and the runs were then deleted
T1677 - Poisoned Pipeline Execution

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:11:45 UTC October 10: six secrets posted, including a second maintainer's token
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
webhook.siteCI SecretsRun DeletionT1677
🔀 Execution: Five Seconds Past Branch Protection
05
Branch protection rejected the workflow change, and the owner's token overrode it five seconds later
T1677 - Poisoned Pipeline Execution

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:29 UTC: push rejected by branch protection
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
Branch Protection BypassLookalike RepositoryRelease WorkflowT1677
🔀 Supply Chain: Published and Signed
06
Seven wheels and four Docker tags went out through the real pipeline, and the images carried a valid signature
T1195.002 - Compromise Software Supply Chain

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.

PyPI: 11:39:51 to 13:23 UTC
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
PyPIGHCRDocker HubCosignT1195.002T1553.002T1070
🔀 Execution: A Stealer on Every Python Start
07
cook.pth ran the stealer on every Python start, and a detached daemon waited for a second stage that was never armed
T1546.018 - Event Triggered Execution: Python Startup Hooks

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.

Files: cook.pth, cook.py, sitecustomize.py
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
cook.pthsitecustomize.pyCredential StealerT1546.018T1041
💥 Impact: Stolen Cloud Roles in Use
08
Hours after the release, stolen AWS instance role credentials were tested from outside
T1078.004 - Cloud Accounts

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.

16:20 to 17:01 UTC: instance role credentials validated from external addresses
CloudTrail signal: GetCallerIdentity around install time, then use of the role from outside
LiteLLM's position: assume every token, key and authenticator was exfiltrated
Instance RoleCloudTrailT1078.004

🛡 How to Defend Against This Chain

Revoke first, clean the repository second. This entire chain rests on a key that was removed from view and left valid. The moment a secret lands in a public commit, treat it as burned and revoke it before anything else. Put a named owner and a deadline on the revocation, because this one was noted down and forgotten.
Keep personal OAuth tokens out of services. An experimental tool held the GitHub CLI token of an organization owner. Give each service a scoped credential of its own, such as a GitHub App installation token, and treat any personal token found in a service config as an incident to fix.
Limit who can override branch protection. A rejected push became an accepted one in five seconds because an owner can override protection. Keep owner accounts few, require phishing-resistant sign-in, and alert on every override. LiteLLM notes the audit log recorded each one, which helps only if someone is watching it.
Do not relax release gates in a hurry. The approval rule was loosened that same week so any two engineers could release. Temporary changes to a release gate need an expiry and a review, since two stolen maintainer tokens then satisfy a two-person rule. Prefer an approval that requires a hardware-backed human action.
Pin release inputs, and verify more than signatures. The signed images came from a real pipeline fed by the wrong source, after an owner token rewrote a repository name that had been hardcoded. Alert on any change to a release workflow, and compare published artifacts with the source tag. Consumers should pin by digest and hold new versions for a soak period rather than following latest.
Assume the host is lost if the package ran. Remove the payload files before running any Python, including pip, and kill the process holding /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.

Related defense topics