Poisoned Pipeline

Poisoned Pipeline / Incidents / AS313

AS313 · 2026-10

LiteLLM 1.107.0 built from a lookalike repository shipped a startup-hook stealer and hourly loader

PyPIDocker / containersGit / GitHubGitHub ActionsDeveloper workstationLinux distro / packagesmacOS softwareWindows software

On 21 October 2026, litellm==1.107.0 reached PyPI with a
credential stealer and a remote-code loader in all seven wheels
(Linux, macOS, Windows), and the official Docker images v1.107.0,
latest, main-latest, and main-stable on Docker Hub and GHCR carried
the same code. The v1.107.0 git tag on BerriAI/litellm is clean. The
release was built from an attacker-controlled lookalike repository,
Berri-AI/litellm, where the attacker had stacked ten commits on top
of the tag that add a cook/ payload directory and one line, data =
"cook", to the [tool.maturin] section of pyproject.toml. Maturin
copies that directory into the wheel's .data tree, so every wheel
installs cook.pth, cook.py, and sitecustomize.py into site-packages.
The sdist carries the tampered build config but omits the payload
directory. The attacker's commits themselves live in the lookalike
repository; the equivalent change to the real one (commit 783c43bc
adding data = "cook") is what the tag audit misses.

cook.pth and sitecustomize.py run when any Python process starts on
the host - not only processes that import litellm. cook.py is a
one-layer exec(zlib.decompress(base64.b64decode(...))) stealer with
sixteen collection modules: browser password and history databases,
SSH keys, GPG keys, TLS private keys, cloud credential files (AWS,
Azure, gcloud, kubeconfig, Docker, rclone, .netrc), CI and
infrastructure secrets (GitLab runner config, Jenkins admin password
and master key, Vault tokens, Terraform and Packer credentials,
Kubernetes service-account tokens, /run/secrets), AI coding tool
credentials (Claude Code, Cursor, Copilot, Gemini CLI, Codex, and
others), environment variables including /proc/<pid>/environ, AWS
instance-metadata role credentials, and - picked for LiteLLM users -
the LiteLLM proxy's own database tables holding LLM provider API
keys. Results are AES-256-CBC encrypted with the key wrapped under
an embedded RSA-4096 public key and POSTed to
www.litellm[.]dev/healthcheck with a hard-coded X-Token header. A
marker file makes it run once per host.

sitecustomize.py forks a detached daemon that polls
raw.githubusercontent[.]com/letters9813/pythonclient-test/main/domain.txt
every hour and downloads and exec()s http://<domain>/codepy - an
attacker-controlled second stage that was never recovered. GitHub
deleted the letters9813 account about two hours after publication,
leaving the loader dormant. LiteLLM's incident report attributes the
intrusion to stolen GitHub CLI OAuth tokens of three developers,
taken from the logs of an internal LiteLLM deployment that was
exposed to the internet; Claude Code can embed a user's GitHub CLI
OAuth token in response bodies, which end up in LiteLLM logs. The
attacker used one developer's credential to modify the private
release workflow, an org owner's credential to override branch
protection after a normal push was rejected, pointed the workflow at
the lookalike source, satisfied the production approval gate with a
second developer's credential, and used a third to delete the
workflow run records. The compromised interval was 11:39 to 13:23
UTC on PyPI and 11:46 to 14:20 UTC on Docker Hub; LiteLLM's first
report did not say how the internal deployment was reached.

Socket (23 October 2026) recovered two earlier malicious workflow
commits in BerriAI/litellm that no release depended on: b69bc36 on
10 October at 22:11:45 UTC and 1a6e3c1 on 12 October, each adding
.github/workflows/cache.yml under the subject "ci: bump cache
config" on a short-lived ci-tmp-<hex> branch that was deleted the
same day. The repository's own Actions performance metrics record
one successful run for each, and cache.yml is absent from main at
the 21 October commit. Malicious workflow execution therefore sits
in the organization eleven days before the poisoned release, not
hours. LiteLLM's follow-up investigation corroborates the 10
October activity, traces initial access to an Agent Platform master
key committed to a public repository in May and never revoked, and
says the GH_TOKEN that the 10 October workflow stole was the one
used to approve the 21 October release. The stolen developer OAuth
tokens and the later pipeline abuse are the same operation; the
master key is how the attacker got the first organization-owner
token.

Mapped techniques

supply-chain Stolen operator tokens subvert the private release pipeline

T0130 · Credential Access · Harvest secrets from logs

The GitHub CLI OAuth tokens of three developers were read out
of the logs of an internet-exposed internal LiteLLM
deployment. Claude Code can embed those tokens in response
bodies, which LiteLLM logs. LiteLLM has not confirmed how the
deployment itself was reached.

T0112 · Initial Access · Compromised token

An Agent Platform master key committed to a public repository
in May 2026 and never revoked retrieved an organization
owner's token, and the cache.yml workflows it authorized on
10 and 12 October stole a further maintainer GH_TOKEN. The
developer GitHub CLI OAuth tokens gave workflow-run rights,
org-owner branch-protection override, an approval gate pass,
and run-record deletion. All involved tokens were revoked
after the incident.

T0176 · Defense Evasion · Disable or misconfigure security measures

Branch protection rejected the workflow change at 10:08:29;
an org-owner credential overrode the protection at 10:08:34
and force-applied commit 9cf97e9. A third credential deleted
the production and staging workflow records afterwards.

T0166 · Execution · Trigger pipeline execution

The attacker pointed the legitimate private release workflow
at the lookalike source repository rather than editing
pipeline steps. The official pipeline built, signed, and
published the poisoned artifacts.

T0202 · Initial Access · Inadequate CI/CD workflow permissions

The private release workflow accepted a caller-supplied
source repository and its revision as dispatch inputs, so a
workflow-run token could redirect the trusted build. The
provenance recorded the lookalike owner honestly -
source_owner Berri-AI - and still signed.

T0217 · Resource Development · Build trusted artifacts from an attacker-controlled source

The core of the incident: the trusted private pipeline built
from Berri-AI/litellm, where ten commits stacked on the real
tag add the cook/ payload and the maturin data line. The
artifacts were signed and published by the legitimate
pipeline, and the v1.107.0 tag on the real repository stays
clean, so tag audits pass.

T0109 · Resource Development · Publish malicious artifact

Seven wheels went to PyPI at 11:39-11:40 and container jobs
published the images at 11:46, under the official publisher
identity. A second developer's credential satisfied the
production approval gate at 11:11.

T0206 · Defense Evasion · Forge or omit build provenance

The container provenance was genuine and complete - run
39587059944, revision 20535d8 - and it is what tied the
build to the lookalike source. It vouches for recorded
inputs, not for their trustworthiness; the tag audit alone
would have missed the payload.

post-supply-chain Python environments that installed 1.107.0

T0159 · Execution · Malicious artifact execution

cook.pth and sitecustomize.py execute during Python
initialization in every process on the host - applications,
pip, IDE language servers, CI jobs - so removal instructions
start from a shell, not Python.

T0184 · Credential Access · Dumping credentials from files

Sixteen collection modules sweep browser stores, SSH and GPG
keys, cloud and CI credential files, AI coding tool tokens,
and the LiteLLM proxy database tables that hold provider
keys.

T0140 · Credential Access · Dump tokens from the environment variable

The stealer reads its own process environment and other
processes' environments through /proc/<pid>/environ.

T0172 · Execution · Runtime Backdoor

sitecustomize.py forks a detached hourly daemon that reads
the next-stage address from domain.txt and exec()s
http://<domain>/codepy. GitHub deleted letters9813 about two
hours after publication, leaving the stage dormant; hosts
that ran the daemon earlier could have received second-stage
code that was never recovered.

Framework gaps noted

Sources