Skip to content
guide

LiteLLM got backdoored: audit your AI toolchain this week

| 8 min read
Server rack representing supply chain infrastructure and credential exposure
Photo by Taylor Vick on Unsplash
Dark terminal with code streaming across the screen, representing supply chain compromise
Every pip install you ran last month is a trust decision you already made. Time to re-check them. Photo by Markus Spiske on Unsplash

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, and known_hosts
  • ~/.aws/credentials and ~/.aws/config profiles
  • ~/.azure/ tokens and refresh credentials
  • GCP application_default_credentials.json and service-account JSON files
  • ~/.docker/config.json including 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 login with session duration capped at 8 to 12 hours. Delete ~/.aws/credentials. Everything should go through ~/.aws/sso cache that expires on its own.
  • GCP: gcloud auth application-default login with --lifetime=43200. Revoke long-lived service-account JSONs sitting on laptops; use workload identity federation from CI.
  • Azure: az login with 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 run or vault 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=none is 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/check to 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.