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
Or without Homebrew:
curl -fsSL https://raw.githubusercontent.com/garagon/aguara/main/install.sh | bash
aguara check
aguara check scans:
- Python site-packages directories (virtualenvs, system)
- uv, pip, and npx package caches
- Installed package versions against known compromised list
- Every
.pthfile for executable content - Persistence paths (
~/.config/sysmon/, systemd user services) - Which credential files exist on your system
If it finds something:
aguara clean
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
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
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
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
-
.pthfiles 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
- Aguara - detection engine + incident response
- oktsec - MCP security proxy
- litellm #24512
- Callum McMahon's writeup
- Full analysis
Top comments (0)