DEV Community

Ashraf
Ashraf

Posted on

LiteLLM Got Backdoored on PyPI. Again. Here's What Your AI Stack Should Do Differently

On October 21, 2026, litellm==1.107.0 landed on PyPI with a credential stealer and a remote-code backdoor baked in. PyPI pulled it about two hours later.

This is the second time LiteLLM has done this. In March, versions 1.82.7 and 1.82.8 shipped malware too. Same package, same class of payload, seven months apart.

If you run LLM gateways, agents, or anything that pip installs into a box holding provider API keys, read on.

TL;DR

  • Bad version: litellm==1.107.0 (plus Docker images published the same day).
  • Clean versions: 1.106.2, 1.107.0rc1, 1.107.0rc2, litellm-proxy-extras==0.4.110, litellm-enterprise==0.1.78.
  • The malware runs on every Python process start, not only when you import litellm.
  • It steals cloud creds, SSH keys, AI tool tokens, and the LiteLLM proxy DB's provider keys.
  • If it ran, rotate everything on that machine. Uninstalling is not enough.

Timeline (UTC, Oct 21)

Time Event
10:08 Attacker bypasses branch protection using stolen GitHub CLI credentials
11:39-11:40 Seven malicious wheels uploaded to PyPI
11:46 Compromised Docker images published
13:23 PyPI removes 1.107.0
14:20 Docker Hub / GHCR repointed to clean builds

Source: SafeDep's analysis. Exposure window on PyPI: roughly 1h45m. For a package with millions of daily downloads, that's plenty.

The part that should bother you: the build was legit

The attacker didn't typosquat. They didn't phish a PyPI token. Per SafeDep, they held GitHub CLI credentials for three LiteLLM developers, reportedly pulled from exposed internal deployment logs. With an org owner's token they:

  1. Overrode branch protection.
  2. Pointed the private release workflow at a lookalike repo.
  3. Used a second developer's credential to clear the approval gate.

Then LiteLLM's own CI built, signed, and published the artifacts. Everything downstream looked official, because it was.

Signatures and "published from the real pipeline" tell you the pipeline ran. They don't tell you the pipeline was running your maintainers' intent.

How the payload runs

One line added to pyproject.toml:

data = "cook"
Enter fullscreen mode Exit fullscreen mode

That made the build drop extra files into site-packages:

  • cook.pth: 12 bytes. Python processes .pth files at startup, and lines starting with import get executed. It imports cook.py.
  • sitecustomize.py: auto-imported by Python, forks a detached daemon.

Result: the stealer fires in any Python process on that interpreter. Your Jupyter kernel, your pytest run, pip itself. You never have to import litellm.

If this sounds familiar, it should. March's 1.82.8 used a litellm_init.pth the same way. A .pth file is one of the best persistence tricks in Python and almost nobody audits them.

What it steals

SafeDep lists 16 collection modules. Highlights:

  • Cloud: AWS, Azure, GCP configs, Kubernetes service tokens, Docker config, plus IMDS creds from 169.254.169.254
  • Keys: ~/.ssh/*, GPG, TLS private keys, .pem / .key / .pfx
  • CI/CD: GitLab runner config.toml, Jenkins initialAdminPassword, Terraform creds, .vault-token
  • AI tooling: Claude Code, Cursor, Copilot, Gemini CLI, Hugging Face, Codex settings
  • LiteLLM proxy: direct DB dump of LiteLLM_CredentialsTable and LiteLLM_ProxyModelTable, i.e. every upstream provider key you gave the gateway
  • Everything else: env vars, shell history, browser-saved passwords

Data is encrypted with a random AES-256-CBC key, wrapped with an embedded RSA-4096 key, and POSTed to www.litellm[.]dev/healthcheck. Note that domain: it looks like it belongs to the project. Your egress allowlist might wave it through.

The backdoor is the real problem

The stealer is a smash-and-grab. The backdoor is why rotation matters.

sitecustomize.py daemonizes and, every hour:

  1. Reads a domain from a file in an attacker-controlled GitHub repo.
  2. Re-runs the stealer (no once-per-host guard, so it re-exfils).
  3. Downloads http://<domain>/codepy and exec()s it.

Because the target domain is read from GitHub, the attacker could repoint the whole fleet without publishing a new package. The GitHub account was deleted at 13:40 UTC, which kills that control channel, but any host that already ran the second stage could have received arbitrary code.

Am I hit? Check in 60 seconds

# Version
pip show litellm | grep -i '^version'

# Persistence files, in every env you have
find / \( -name 'cook.pth' -o -name 'cook.py' \) -path '*site-packages*' 2>/dev/null
find / -name sitecustomize.py -path '*site-packages*' 2>/dev/null

# Marker files
ls -la /tmp/.pc_once_72129118c59f5636c0bb /tmp/remote_code_loader.lock 2>/dev/null

# Daemon: detached, parent PID 1, cwd /
ps -eo pid,ppid,cmd | awk '$2==1 && /python/'
Enter fullscreen mode Exit fullscreen mode

Also grep egress/DNS logs for litellm.dev and the header X-Token: XWiA8gZNa7NcX2XRoAEb. Treat any Python process talking to 169.254.169.254 that shouldn't be as suspect.

Containers: check your image build dates. Anything built on Oct 21 between 11:46 and 14:20 UTC from the official tags needs a rebuild and a hard look.

If you were hit

  1. Isolate the host. Don't just pip uninstall.
  2. Rotate: cloud keys, SSH keys, Kubernetes tokens, every LLM provider key (OpenAI, Anthropic, Gemini, etc.), CI secrets, Vault tokens, Hugging Face, and the tokens for your coding agents.
  3. Audit cloud access logs (CloudTrail, GCP audit, Azure activity) from the install time forward.
  4. Rebuild from clean, don't clean in place.

What to change before the next one

This will happen again. It already has. Practical hardening, in order of payoff:

1. Pin with hashes. Floating litellm>=1.x is how you pull a bad release within minutes of publish.

uv pip compile requirements.in --generate-hashes -o requirements.txt
uv pip install --require-hashes -r requirements.txt
Enter fullscreen mode Exit fullscreen mode

2. Add a cooldown. Most malicious releases are caught within hours. Don't install anything newer than a day or two.

# pyproject.toml (uv)
[tool.uv]
exclude-newer = "2026-10-20T00:00:00Z"
Enter fullscreen mode Exit fullscreen mode

3. Stop putting every key on one box. A gateway holding all your provider keys is a single jackpot. Scope keys per service, set spend caps, and keep the LLM proxy off machines with SSH keys and cloud admin creds.

4. Lock down egress. Your LLM gateway needs to talk to a handful of provider domains. Allowlist those. litellm.dev/healthcheck shouldn't have gotten out.

5. Audit .pth and sitecustomize.py. Add this to CI and image scans:

python - <<'EOF'
import site, pathlib
for d in site.getsitepackages():
    for p in pathlib.Path(d).glob("*.pth"):
        t = p.read_text(errors="ignore")
        if any(l.startswith(("import ", "import\t")) for l in t.splitlines()):
            print("EXEC .pth:", p)
    sc = pathlib.Path(d, "sitecustomize.py")
    if sc.exists():
        print("sitecustomize:", sc)
EOF
Enter fullscreen mode Exit fullscreen mode

Some legit packages ship executable .pth files, so this is a review list, not an alarm.

6. Protect your maintainers' tokens (if you publish packages). Org-owner tokens that can override branch protection are the crown jewels. Use short-lived credentials, require hardware-key 2FA, never log tokens, and make release workflows immutable from a single credential. PyPI Trusted Publishing helps, but as this attack shows, it only moves the trust to your repo's permissions.

My take

The industry keeps treating AI infrastructure like a pile of convenient wrappers. It's not. A LiteLLM install is a credential vault with a Python API on top. Attackers noticed first.

Two incidents in seven months means this is the baseline now, not an anomaly. The fix isn't "trust LiteLLM less." The fix is architecture that assumes any dependency can ship malware on any given Tuesday: pinned hashes, cooldowns, scoped keys, locked egress.

Go check your lockfiles. Then go check your site-packages.


Sources: SafeDep: Compromised LiteLLM 1.107.0 · FutureSearch: minute-by-minute response to the March attack · Sonatype on the March compromise · Hacker News discussion

Top comments (0)