# LiteLLM Got Poisoned (Again). Your AI Gateway Is Now the Juiciest Box in Your Stack

---

On October 21, someone pushed `litellm==1.107.0` to PyPI and Docker. It looked like a normal release. It wasn't. For about two hours on PyPI, and a bit longer on Docker Hub and GHCR, anyone who installed it or pulled `:latest` got a credential stealer that ran every time Python started.

If you're a MERN dev thinking "I don't write Python, not my problem", keep reading. A lot of us run LiteLLM as a little sidecar container next to our Express API so we can swap between OpenAI, Anthropic and Bedrock without touching app code. That sidecar holds every AI key you own. That's exactly why it got hit.

## What actually happened

Here's the short version, pieced together from the reports by [Socket](https://socket.dev/blog/litellm-compromise), [Help Net Security](https://www.helpnetsecurity.com/2026/10/22/litellm-compromised-supply-chain-attack-pypi-docker/) and [BleepingComputer](https://www.bleepingcomputer.com/news/security/litellm-release-pipeline-hacked-to-distribute-info-stealer-malware/):

- The attacker used the GitHub accounts of **three** LiteLLM engineers, apparently through stolen GitHub CLI OAuth tokens. One account changed the release pipeline and kicked off the release, a second one approved it, and a third deleted workflow records. So the two-person review was there, and it didn't help.
- LiteLLM thinks the tokens came out of logs on an internal deployment that was reachable from the public internet.
- The build ran from `Berri-AI/litellm`. The real repo is `BerriAI/litellm`. One hyphen.
- The wheels dropped three files into `site-packages`: `cook.pth`, `cook.py`, and `sitecustomize.py`.
- PyPI exposure was 11:39 to 13:23 UTC. The Docker tags `v1.107.0`, `latest`, `main-latest` and `main-stable` served the bad build from 11:46 to 14:20 UTC.
- According to Wiz, about 4.5 hours after data left a victim's machine, the attacker was already testing stolen AWS keys against Amazon Bedrock. That's LLMjacking: running up someone else's bill on expensive models.

This is LiteLLM's second supply chain incident this year. The March one ended with a 153GB archive of stolen data linked to roughly 2,500 organizations.

![](https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/4dfd17fc-4df5-405f-9b8f-cb7fbb943dc6.png align="center")

## The trick: you didn't even have to import it

This part is worth understanding because it changes how you think about "just installing" a package.

Python reads every `.pth` file in `site-packages` at interpreter startup. Most of them just add paths. But a line that starts with `import` gets executed. So `cook.pth` contained one line:

```python
import cook
```

That's it. Any `python` or `pip` command in that environment, including your test runner or a random `pip list`, would launch the collector in a background thread. `sitecustomize.py` is another file Python runs automatically, and that one started an hourly loop that polled a GitHub file for a domain to download and run more code from.

The collector went after a long list: AWS/GCP/Azure creds, Kubernetes and Docker configs, `.env` files, SSH and GPG keys, npm and PyPI tokens, browser cookies, shell history, and auth files for Claude Code, Codex, Gemini and Cursor. If `DATABASE_URL` was set, it also tried to read LiteLLM's own credentials table.

The Node world has the same problem with a different name: `postinstall` scripts. The difference is that `.pth` runs on *every* Python start, not just once at install time. Removing the package doesn't stop whatever already ran.

## Are you affected? Check without running Python

Socket's advice is to avoid running the affected environment's Python or pip while you investigate, because that triggers the payload again. So check with plain shell tools:

```bash
# Run from your project root. Works in bash and zsh (macOS + Linux). Never calls python or pip.

echo "== 1. litellm 1.107.0 or its payload installed? =="
for d in "$HOME" /usr/lib /usr/local/lib /opt /app /Library/Frameworks; do
  [ -d "$d" ] && find "$d" \( -name 'litellm-1.107.0.dist-info' -o -name 'cook.pth' \) -print 2>/dev/null
done | grep . || echo "nothing found"

echo "== 2. Malware marker files? =="
find /tmp/ -maxdepth 1 \( -name '.pc_once_*' -o -name 'remote_code_loader.lock' \) 2>/dev/null \
  | grep . || echo "none (this does NOT prove you're clean)"

echo "== 3. Bad Docker image pulled? =="
if command -v docker >/dev/null 2>&1; then
  docker images --digests 2>/dev/null \
    | grep -E '1bbcb1443bc9|bd1aeee419ea|56b74573eed6' || echo "no bad litellm image locally"
else
  echo "docker not installed, skipped"
fi

echo "== 4. Lockfiles / requirements pinning 1.107.0? =="
{
  grep -rn -A2 --include='*.lock' 'name = "litellm"' . 2>/dev/null | grep '1\.107\.0'
  grep -rnE --include='requirements*.txt' --include='pyproject.toml' 'litellm[^0-9]*1\.107\.0' . 2>/dev/null
} | grep . || echo "no lockfile references"
```

Compare any image digests against the IOCs Socket published. If you find anything, the playbook is not "upgrade and move on":

1. Isolate the host or rebuild the container from a clean, pinned image (`1.106.2` is the version researchers point to).
2. Rotate **every** secret that machine could read, from a different machine. Provider keys, cloud keys, GitHub tokens, the Mongo connection string, all of it.
3. Check your cloud audit logs, especially Bedrock usage and IAM calls, for activity you don't recognize.

## Harden the gateway in your MERN stack

Here's the setup I see most often, and how I'd change it.

**Pin by digest, not tag.** `:latest` was literally one of the poisoned tags. A tag can be moved; a digest can't.

```yaml
# docker-compose.yml
services:
  api:
    build: ./server
    environment:
      LLM_GATEWAY_URL: http://llm-gateway:4000
      LLM_GATEWAY_KEY: ${LITELLM_VIRTUAL_KEY_API}   # scoped key, not a provider key
    networks: [internal, public]

  llm-gateway:
    # Pin to a reviewed version AND its digest
    image: ghcr.io/berriai/litellm:v1.106.2@sha256:<digest-you-verified>
    env_file: ./secrets/gateway.env                  # provider keys live ONLY here
    networks: [internal, egress]
    read_only: true
    tmpfs: [/tmp]

networks:
  internal: { internal: true }
  public: {}
  egress: {}   # put an egress proxy/allowlist in front of this in prod
```

**Keep provider keys out of your Express app.** Your Node server should talk to the gateway with a scoped virtual key that has a budget and a model allowlist. If that key leaks, the blast radius is capped.

```js
// server/lib/llm.js
export async function chat(messages, { model = "gpt-fast" } = {}) {
  const res = await fetch(`${process.env.LLM_GATEWAY_URL}/v1/chat/completions`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.LLM_GATEWAY_KEY}`,
    },
    body: JSON.stringify({ model, messages }),
    signal: AbortSignal.timeout(30_000),
  });
  if (!res.ok) throw new Error(`LLM gateway ${res.status}`);
  return res.json();
}
```

**Lock egress.** The stealer phoned home to a lookalike domain over plain HTTP. If your gateway container can only reach `api.openai.com`, `api.anthropic.com` and your Bedrock endpoint, that upload fails. This one control would have neutered most of the damage.

**Don't log response bodies with tokens in them.** LiteLLM specifically called out that GitHub CLI OAuth tokens can end up in response bodies, and that's how they think the tokens leaked. Redact `Authorization` headers and anything shaped like a token in your Express logger too.

![](https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/616ddec5-ed77-4d14-a352-ea68aa3fcad5.png align="center")

## Add a cooldown before new versions land

The malicious version was on PyPI for under two hours. A simple rule like "don't install anything published in the last 24 hours" would have skipped it entirely. Big shops have done this for years with internal mirrors; you can get most of the benefit with config.

```yaml
# pnpm-workspace.yaml  (pnpm 10.16+, value in minutes)
minimumReleaseAge: 1440
```

```json
// renovate.json — applies to npm, PyPI and Docker updates
{
  "minimumReleaseAge": "3 days",
  "pinDigests": true
}
```

```bash
# Python side: hash-pinned installs fail if a file changes under you
pip install --require-hashes -r requirements.txt

# or with uv, refuse anything newer than a known-good date
uv pip install -r requirements.txt --exclude-newer 2026-10-20T00:00:00Z
```

And in CI, `npm ci --ignore-scripts` for jobs that don't need native builds. Fewer things running at install time means fewer chances for this kind of surprise.

## The bigger lesson

AI gateways are attractive targets for the same reason they're useful: one box, every key. The attacker here didn't care about your app. They wanted your Bedrock quota and your cloud credentials, and they were using them within hours.

So treat your gateway like you'd treat your payment service. Pin it, isolate it, restrict where it can talk, give your app scoped keys instead of root keys, and slow down how fast new versions reach production.

Go check your `docker-compose.yml` right now. If you see `litellm:latest` in there, you know what to do first.
