LiteLLM Got Owned, and Your Dependencies Might Be Next
Earlier today, someone published malicious versions of litellm (versions 1.82.7 and 1.82.8) to PyPI. No corresponding release appeared on GitHub. The package was uploaded directly to PyPI, bypassing the normal release process, which points to a compromised maintainer account or token.
What the malware did
The payload was a .pth file named litellm_init.pth. Python executes .pth files automatically on interpreter startup, so the malware ran without any user interaction once the package was installed.
It operated in three stages:
Collection: Harvested SSH keys,
.envfiles, cloud credentials (AWS, GCP, Azure), Kubernetes configs, database passwords,.gitconfig, shell histories, and crypto wallet files. It also scraped environment variables and queried cloud metadata endpoints.Exfiltration: Encrypted collected data with a hardcoded 4096-bit RSA public key and AES-256-CBC, then POSTed it to
models.litellm.cloud, a domain unrelated to legitimate litellm infrastructure.Lateral movement: If Kubernetes credentials were present, the malware read all cluster secrets and attempted to create privileged pods on every node in
kube-system, installing persistent backdoors via systemd.
The malware had a bug: because .pth files trigger on every interpreter startup and the payload spawned child processes via subprocess.Popen, it created an exponential fork bomb that crashed machines.
The attack was discovered when the compromised package was pulled in as a transitive dependency by an MCP plugin running inside Cursor. A GitHub issue was filed and closed as “not planned,” suggesting the maintainer account was still compromised at that point.
This keeps happening
litellm gets roughly 95 million downloads per month. It sits in the dependency tree of countless AI applications. If your CI pulled a fresh install between 10:52 UTC and whenever PyPI yanked the versions, you may have been affected.
This follows a pattern. In 2022, the ctx package was compromised through an expired domain re-registration. In 2024, Ultralytics (80 million monthly downloads) was hit via GitHub Actions script injection. Earlier this year, the Shai-Hulud worm demonstrated cross-ecosystem token exfiltration from GitHub secrets.
The attacks are getting more sophisticated. The defenses need to keep up.
How to protect yourself
Bernát Gábor, who maintains virtualenv, tox, platformdirs, and filelock for PyPA, published Securing the Python Supply Chain earlier this month. It is the most practical guide available on this topic.
Gábor’s core argument is defense in depth: no single measure stops every attack, but layering protections means each failure mode is covered by something else. He organizes the work into phases based on effort.
Quick wins (a day or two)
Add Ruff security linting. Rules like S105 (hardcoded secrets), S301 (pickle deserialization), and S608 (SQL injection) catch common vulnerabilities in your own code before they ship.
Pin dependencies with hashes. uv pip compile --generate-hashes produces a lockfile where every artifact is verified by its hash at install time. This stops tampering in transit, in caches, and on mirrors. It would not have caught today’s litellm attack (the malicious version was a legitimate PyPI upload), but it narrows the attack surface.
Foundation work (about a week)
Run pip-audit in CI. It checks your dependency tree against the OSV database for known CVEs. Once litellm’s malicious versions are added to the advisory databases, pip-audit would flag them.
Generate SBOMs. A CycloneDX SBOM gives you a machine-readable inventory of every dependency in your builds. When the next advisory drops, you can answer “are we affected?” in seconds instead of hours.
Switch to Trusted Publishing. OIDC-based publishing replaces long-lived PyPI API tokens with short-lived, scoped credentials tied to your CI provider. If litellm had used Trusted Publishing, a stolen token from a different context could not have been used to upload packages.
Advanced measures
Delayed ingestion. If you run a corporate mirror, add a 7-day buffer before new versions become available internally. Most obvious malware gets caught by the community within days. Today’s litellm attack was caught within hours.
Time-based installation constraints. uv supports --exclude-newer to limit installations to packages published before a specific date. pip v26+ adds --uploaded-prior-to. Both give you a buffer against fresh compromises.
What would have helped today
Hash pinning alone would not have caught this, since the malicious package was a real PyPI upload with valid hashes. What would have helped:
- Lockfiles with pinned versions: If your lockfile pinned litellm to 1.82.6, you never pulled 1.82.7 or 1.82.8
- Delayed ingestion: A 7-day buffer would have kept the malicious versions off internal mirrors entirely
--exclude-newer: Constraining to packages published before today would have skipped the compromised uploads- Trusted Publishing on the litellm side: Would have prevented the unauthorized upload in the first place
Gábor’s guide covers all of this with implementation details, CI integration examples, and clear explanations of what each layer does and does not protect against. Read it: Securing the Python Supply Chain.