LiteLLM got backdoored: audit your AI toolchain this week
In March 2026, a threat actor calling itself TeamPCP published a tampered release of
litellm, the Python library that wraps 100+ LLM providers behind a single
OpenAI-compatible interface. The release shipped an infostealer wired into the post-install path.
Anyone who ran pip install litellm during the window handed over SSH private keys,
AWS credentials, Azure tokens, GCP service-account JSONs, and Docker config.json
files to an attacker-controlled endpoint. The Hacker News, Ardan Labs, and Security Boulevard
each confirmed the attribution and the exfiltrated artifact list.
LiteLLM is not obscure. It sits inside agent frameworks, inside CI pipelines that route eval traffic across providers, inside Jupyter notebooks on a data scientist's laptop that also hold long-lived cloud credentials. A single compromised release gave the attacker a wallet of provider keys, a set of cloud roles, and the SSH material to move laterally into every repo those keys could reach.
You can't prevent a maintainer's PyPI token from being stolen. You can shrink your blast radius so that when the next AI library gets hit; and it will; one laptop does not turn into a cross-cloud incident. Here are five checks worth running this week across the whole AI toolchain: LiteLLM, LangChain, LlamaIndex, Ollama wrappers, Claude SDKs, MCP clients, and every agent framework you have in production.
What TeamPCP actually took
The payload ran inside setup.py and a post-install hook. On import or install it
read the following from the developer's home directory and CI runner filesystem:
~/.ssh/id_rsa,id_ed25519, andknown_hosts~/.aws/credentialsand~/.aws/configprofiles~/.azure/tokens and refresh credentials- GCP
application_default_credentials.jsonand service-account JSON files ~/.docker/config.jsonincluding registry auth tokens- Environment variables starting with
AWS_,GCP_,OPENAI_,ANTHROPIC_,GITHUB_
The exfil channel was a plain HTTPS POST to an attacker domain that rotated weekly. Takedown happened within days, but that window was enough; credentials that leaked cannot be unleaked. On to the checks.
Check 1: pin exact versions, not ranges
The ^1.14.0 and ~=1.48 patterns in requirements.txt,
pyproject.toml, and package.json let any new minor or patch release
roll into a clean environment overnight. That is how a three-hour malicious window becomes a
month-long exposure; your lockfile never got regenerated but your CI cache pulled the bad
version on its next cold build.
Pin exact versions. Pin by hash when the ecosystem supports it.
# requirements.txt: exact versions with sha256 hashes
# Generated by: pip-compile --generate-hashes requirements.in
litellm==1.48.2 \
--hash=sha256:a1b2c3d4e5f6071829304e5a6b7c8d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8 \
--hash=sha256:112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00
openai==1.51.0 \
--hash=sha256:fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210
anthropic==0.39.0 \
--hash=sha256:0011223344556677889900112233445566778899001122334455667788990011
# Install with hash enforcement
# pip install --require-hashes -r requirements.txt
With --require-hashes, pip refuses to install anything whose SHA-256 does not match
what you generated at lock time. A compromised release with the same version string but a
different tarball hash fails the install. Pair pip-compile --generate-hashes with a
CI job that regenerates the hash file only on explicit dependency updates, never on a plain
install.
Poetry users get the same safety net via the lockfile. The gotcha is the caret syntax in
pyproject.toml; replace it with exact pins:
# pyproject.toml: exact pins, no caret
[tool.poetry.dependencies]
python = "^3.12"
litellm = "1.48.2" # not ^1.48.2
openai = "1.51.0" # not ^1.51.0
anthropic = "0.39.0" # not ^0.39.0
[tool.poetry.group.dev.dependencies]
pytest = "8.3.3"
# Lock once, verify on every install:
# poetry lock --no-update
# poetry install --sync --no-interaction
# poetry check --lock poetry install --sync removes any package that is not in the lockfile, which stops
a stray dependency from hiding between deploys. For npm-side AI tooling, use
npm ci (not npm install) on every CI run and commit
package-lock.json.
Check 2: isolate credentials from dev shells
A dev shell that has ~/.aws/credentials with a long-lived access key is a loaded
gun pointed at your cloud bill and your customer data. LiteLLM did not need those credentials to
do its job; they sat on disk because the developer once ran aws configure and
never cleaned up.
Move credentials off disk. Load them on demand, scope them short, and revoke the persistent layer:
- AWS:
aws sso loginwith session duration capped at 8 to 12 hours. Delete~/.aws/credentials. Everything should go through~/.aws/ssocache that expires on its own. - GCP:
gcloud auth application-default loginwith--lifetime=43200. Revoke long-lived service-account JSONs sitting on laptops; use workload identity federation from CI. - Azure:
az loginwith conditional access that forces MFA; kill stored refresh tokens older than 12 hours. - API keys: keep them in 1Password, Vault, or Doppler. Inject at process start
via
op runorvault read. Never export them into~/.zshrc.
If TeamPCP had found only expired STS session tokens, the blast radius would stop at a few hours of access. The incident happened because long-lived keys sat plaintext on disk, ready to be read.
Check 3: scan every install with a sandbox
Before you trust a new AI library or an upgrade to an existing one, install it somewhere it cannot reach your credentials. A throwaway Docker container with dropped capabilities and network isolation tells you whether the install-time code does anything suspicious:
# Install + test a new AI library inside a throwaway container
# with zero network egress after the install step.
# Step 1: install in an isolated container with egress only to PyPI
docker run --rm -it \
--network bridge \
--read-only \
--tmpfs /tmp \
--cap-drop=ALL \
-v "$PWD/wheels:/wheels:ro" \
python:3.12-slim \
bash -c "pip install --require-hashes -r /wheels/requirements.txt && python -c 'import litellm; print(litellm.__version__)'"
# Step 2: run your actual import smoke test with NO network
docker run --rm -it \
--network=none \
--read-only \
--tmpfs /tmp \
-v "$PWD:/app:ro" \
python:3.12-slim \
bash -c "cd /app && python -c 'from litellm import completion; print(\"import ok\")'"
# If the import tries to phone home, you see the exception.
# If the postinstall already ran on step 1, see Check 5 for egress monitoring.
The two-step pattern matters. Step one needs network access to reach PyPI, so a post-install
script could still exfiltrate. Run step one on a clean VM with no secrets and a DNS egress rule
that logs every outbound hostname. Step two runs with --network=none; if your
library tries to phone home at import time, the import fails and you know.
Better: use a disposable microVM like Firecracker or orb on macOS for the same
shape without Docker's shared kernel. For JavaScript, Deno's permission model gives you the
same isolation without a container: deno run --allow-net=api.openai.com,api.anthropic.com
agent.ts.
Check 4: verify breach exposure before rotating
If your keys sat on a machine that touched a bad release, rotate them. No debate. But also check whether the identities tied to those keys already show up in known dumps; a laptop that ran a compromised LiteLLM release last month might be a laptop whose owner reused passwords from a 2019 Collection1 dump. Both problems need fixing, and Check 4 tells you which accounts need the most urgent attention.
Botoi's /v1/breach/check endpoint wraps the same data sources most enterprise IAM
tools pay per-seat for, and it costs nothing on the free tier:
curl -X POST https://api.botoi.com/v1/breach/check \
-H "Content-Type: application/json" \
-d '{"email": "ci-bot@acme-corp.com"}' {
"data": {
"email": "ci-bot@acme-corp.com",
"found": true,
"breach_count": 3,
"password_exposed": true,
"breaches": [
{
"name": "Collection1",
"date": "2019-01-07",
"data_classes": ["email_addresses", "passwords"]
},
{
"name": "LinkedIn",
"date": "2012-05-05",
"data_classes": ["email_addresses", "passwords"]
}
],
"recommendation": "rotate_password_and_enable_mfa"
}
}
Walk every service-account email, every machine-user email, and every human dev whose laptop
touched the window. Any email with password_exposed: true needs a rotation and a
forced MFA reset before you move on. Pipe the output into your incident response runbook; one
curl per identity, ten minutes for a 50-person engineering org.
Check 5: watch your outbound egress
Post-install malware has to phone home somehow. Most developers cannot remember the last time they looked at what their laptop is actually talking to; that is the gap TeamPCP counted on.
On a suspect machine, a single command tells you what every Python and Node process is connected to:
# Spot unexpected outbound sockets from any python or node process.
# Run on the suspect machine; pipe to a file if you want to diff later.
# macOS / Linux with lsof
sudo lsof -i -P -n | grep -E "python|node" | grep -v "127.0.0.1\|::1"
# Linux with ss (faster, no lsof)
ss -tunp 2>/dev/null | grep -E "python|node|uv|poetry"
# What you want to see: connections to pypi.org, registry.npmjs.org,
# your own API endpoints, and nothing else.
# What you don't want: IPs on :8000, :4444, :1337, or any bare IP with
# no matching hostname. At the fleet level, push every developer laptop and CI runner through a Zero Trust egress allowlist. Cloudflare WARP, Tailscale ACLs, and Little Snitch all give you the same shape: list the hostnames you actually need, block the rest, alert on the blocks.
# Cloudflare Zero Trust: egress allowlist for a dev device posture check.
# Apply to the "Developer laptops" group; block everything else.
- name: allow-package-registries
action: allow
traffic:
domains:
- pypi.org
- files.pythonhosted.org
- registry.npmjs.org
- objects.githubusercontent.com
- name: allow-ai-providers
action: allow
traffic:
domains:
- api.openai.com
- api.anthropic.com
- api.botoi.com
- generativelanguage.googleapis.com
- name: allow-cloud-control-planes
action: allow
traffic:
domains:
- "*.amazonaws.com"
- "*.googleapis.com"
- login.microsoftonline.com
- name: block-everything-else
action: block
traffic:
destinations: ["0.0.0.0/0", "::/0"]
notify: slack-security-alerts An allowlist this tight feels harsh the first week. By week two, the only alerts you see are real: a library trying to reach a destination it has no business reaching. That is exactly the signal TeamPCP's infostealer would have tripped on install.
The five checks at a glance
| Check | Effort | Blast-radius reduction |
|---|---|---|
| Pin exact versions with hashes | 1 hour for a single repo; 1 day for a monorepo | Stops silent rollout of a compromised release |
| Move credentials to short-lived sessions | 2 to 4 hours per developer, one-time | Caps exfil to 8 to 12 hours of access instead of months |
| Sandbox every install | 15 minutes per new library evaluation | Install-time code cannot see real secrets or prod network |
| Breach-check identities tied to exposed keys | 10 minutes for a 50-person org | Prioritizes rotations and MFA resets by known exposure |
| Enforce egress allowlist on dev and CI | 1 day to configure, continuous to maintain | Blocks phone-home channels that exfil stolen data |
Key takeaways
- Pin exact versions, not ranges. A caret or tilde in your lockfile is a promise that tomorrow's release is safe. TeamPCP broke that promise.
- Cloud credentials do not belong in dev shells. Short-lived SSO sessions turn a stolen credential into a rotation event, not a breach.
- Sandbox installs before you trust them. Docker with dropped caps and
--network=noneis enough to catch install-time malware for the cost of 15 minutes. - Rotate plus breach-check. If a machine touched a bad release, rotate every
identity it saw and run each email through
/v1/breach/checkto find the ones that need urgent MFA resets. - Egress allowlists catch phone-home. Zero Trust rules on laptops and CI turn a silent exfiltration into a loud, logged, blockable event.
Botoi gives you HTTP endpoints for breach checking, hash verification, SSL inspection, HTTP header auditing, and npm metadata lookup; the primitives you need to audit a supply-chain incident without installing yet another library. One API key, 5 req/min on the free tier, no install hooks. Browse the interactive docs or wire the MCP server into Claude Code or Cursor to call the same endpoints from your editor while you work through the checklist.
Frequently asked questions
- Which LiteLLM versions were affected and how do I confirm I am clean?
- The malicious releases surfaced in March 2026 and sat on PyPI long enough to land in CI caches and developer laptops worldwide. Run pip show litellm to print your installed version, then cross-reference the version and upload timestamp against the LiteLLM security advisory and PyPI release history. If a machine ran pip install litellm during the window, treat it as compromised: rotate every cloud key that sat in that shell, redeploy from a clean image, and wipe ~/.aws, ~/.config/gcloud, and ~/.ssh tokens that pre-date the rotation.
- Was this PyPI fault or LiteLLM fault?
- Both, and that is the lesson. LiteLLM is a legitimate library with a real maintainer; the attacker rode a compromised release channel, not a typosquat. PyPI still does not require signed releases or mandatory 2FA on tokens for every project, so a stolen upload credential turns into shipped malware in minutes. Package signing via Sigstore and maintainer-scoped tokens would have cut the attack off at upload.
- Do I need to rotate every AWS key or only the ones touched on affected machines?
- Rotate all of them. Attackers pivot through STS, assume-role chains, and cross-account trust policies the moment they have a single long-lived key. If the laptop or CI runner that saw the bad release had any IAM credentials in memory or on disk, treat the entire principal graph reachable from that identity as hot. Same logic for GCP service accounts and Azure service principals.
- Does Bun or Deno change this for JavaScript AI tooling?
- A little, not a lot. Deno runs code with explicit permissions (--allow-net, --allow-env), so a library that suddenly tries to read ~/.aws/credentials gets denied unless you granted filesystem access. Bun has a --frozen-lockfile flag and does not run install scripts by default in recent releases. Both are improvements over npm defaults, but neither stops a library from exfiltrating data once your application code hands it the credentials at runtime.
- What makes AI-toolchain supply chain risk different from regular npm or PyPI risk?
- AI libraries sit unusually close to secrets. A LLM wrapper like LiteLLM needs API keys for 10+ providers in one env file. A LangChain agent reads AWS credentials so it can call S3 as a tool. A Claude SDK touches GITHUB_TOKEN because you asked it to open PRs. The blast radius per install is higher than a typical utility library; one compromised release gets you a wallet of provider keys plus cloud credentials plus source-code access in one shot.
Try this API
Breach Check API — interactive playground and code examples
More guide posts
Start building with botoi
150+ API endpoints for lookup, text processing, image generation, and developer utilities. Free tier, no credit card.