DEV Community

Cover image for The litellm supply chain attack: how MCP servers got compromised and how to check if you're affected
Gus
Gus

Posted on

The litellm supply chain attack: how MCP servers got compromised and how to check if you're affected

On March 24, 2026, litellm versions 1.82.7 and 1.82.8 were published to PyPI with malicious code. 97 million monthly downloads. No corresponding GitHub tag or release. The maintainer account was likely fully compromised.

The vector

Not setup.py. Not import hooks. A .pth file.

Python executes .pth files on every interpreter startup when the package is installed. No import needed. Just pip install litellm and every Python process on your machine runs the payload.

The attack was found by accident. The .pth uses subprocess.Popen to spawn a new Python process, but since .pth triggers on every interpreter startup, the subprocess re-triggers itself. Fork bomb. Callum McMahon was using an MCP plugin in Cursor that pulled litellm as a transitive dependency. The fork bomb consumed all RAM and crashed the machine. Without that bug, it could have run for weeks.

How it spread through MCP

MCP clients like Cursor, Claude Desktop, and VS Code launch MCP servers with package executors like uvx and npx. These auto-download the latest version on every run. No lockfile. No hash verification.

McMahon's MCP server had an unpinned litellm dependency. When Cursor auto-loaded the server, uvx pulled litellm 1.82.8 from PyPI. The malware was live for less than an hour. His machine was compromised within minutes.

Most MCP server READMEs show uvx run package without @version. This is a category-wide problem, not just litellm.

What the malware does

Collection. Reads ~/.ssh/id_rsa, ~/.aws/credentials, ~/.kube/config, .env, .gitconfig, .bash_history, crypto wallet files, .npmrc, .pypirc. Dumps os.environ. Queries cloud metadata endpoints (169.254.169.254).

Exfiltration. Encrypts everything with a hardcoded 4096-bit RSA public key (AES-256-CBC), bundles into a tar archive, POSTs to models.litellm.cloud (attacker-controlled).

Lateral movement. If a Kubernetes service account token exists: reads all secrets across all namespaces, creates privileged alpine:latest pods on every node in kube-system, mounts host filesystem, installs persistent backdoor at ~/.config/sysmon/sysmon.py with a systemd user service.

Check if you're affected

Install Aguara and run two commands:

brew install garagon/tap/aguara
aguara check
Enter fullscreen mode Exit fullscreen mode

Or without Homebrew:

curl -fsSL https://raw.githubusercontent.com/garagon/aguara/main/install.sh | bash
aguara check
Enter fullscreen mode Exit fullscreen mode

aguara check scans:

  • Python site-packages directories (virtualenvs, system)
  • uv, pip, and npx package caches
  • Installed package versions against known compromised list
  • Every .pth file for executable content
  • Persistence paths (~/.config/sysmon/, systemd user services)
  • Which credential files exist on your system

If it finds something:

aguara clean
Enter fullscreen mode Exit fullscreen mode

Shows what it found, asks for confirmation, quarantines files to /tmp/aguara-quarantine/, uninstalls the package, purges caches.

Manual check (no install)

pip show litellm | grep Version

find $(python -c "import site; print(site.getsitepackages()[0])") \
  -name "litellm_init.pth"

ls ~/.config/sysmon/sysmon.py
ls ~/.config/systemd/user/sysmon.service

# Kubernetes
kubectl get pods -n kube-system | grep node-setup
Enter fullscreen mode Exit fullscreen mode

If any return results: uninstall litellm, delete the files, remove ~/.config/sysmon/, and rotate every credential on that machine.

Preventing this

Pin your versions

# Bad: pulls whatever PyPI serves right now
uvx run litellm-proxy

# Good: locked to specific version
uvx run litellm-proxy@1.82.6
Enter fullscreen mode Exit fullscreen mode

Same for npx. Same for pip (use == pins and --require-hashes).

Scan MCP server directories before running them

aguara scan /path/to/mcp-server/ --severity high
Enter fullscreen mode Exit fullscreen mode

10 rules (SC-EX category) detect the code patterns from this attack in Python source:

Rule What it detects
SC-EX-001 Python code reading credential files via open() or pathlib
SC-EX-002 File contents being base64/AES encoded before transmission
SC-EX-003 Bulk os.environ access combined with HTTP POST
SC-EX-004 .pth files with executable content
SC-EX-005 Cloud metadata endpoint access in Python
SC-EX-006 Kubernetes secrets API access or privileged pod creation
SC-EX-007 Systemd/cron persistence installation
SC-EX-008 Hardcoded RSA/AES key material
SC-EX-009 Tar/zip creation combined with HTTP POST
SC-EX-010 .pth file presence (review flag)

MCPCFG_012 flags uvx and uv run MCP servers without version pins. MCPCFG_013 flags pip install without --require-hashes.

Restrict network access at runtime

If you run MCP servers through oktsec (MCP security proxy), egress_sandbox: true forces subprocesses to route HTTP through the proxy. Even if a dependency is compromised, exfiltration to unauthorized domains is blocked.

dep_check: true hashes dependency manifests on startup and warns when they change between runs.

What this doesn't cover

  • .pth files execute at interpreter startup, before any runtime scanner. You have to scan before running.
  • Egress sandbox catches HTTP/HTTPS. Raw TCP bypasses it.
  • The SC-EX rules detect patterns from this specific attack. Different techniques need new rules.
  • Obfuscated Python won't match regex patterns.

Links

Top comments (0)