The .pth File That Ran Before Your Code Did: Lessons from litellm 1.82.8
Source: simonwillison
Last week, Simon Willison documented something that should make anyone running AI tooling in production uncomfortable: litellm version 1.82.8 shipped with a file called litellm_init.pth that silently exfiltrated credentials on every Python invocation in the affected environment. The maintainers at BerriAI appear to have been victims of a compromised publishing credential rather than perpetrators, but that distinction offers little comfort to developers who had 1.82.8 installed.
The incident is worth understanding carefully, not just as a supply chain cautionary tale, but because the specific mechanism it exploits is one that most Python developers have never thought about.
What litellm Is and Why It Was a High-Value Target
litellm is a Python library and proxy server that presents a unified OpenAI-compatible interface to over 100 LLM provider backends: OpenAI, Anthropic, Azure, Cohere, Mistral, Google Vertex, AWS Bedrock, and more. Its core promise is that you configure your providers once and route everything through a single call interface. It handles format differences, retries, load balancing, and cost tracking.
That design makes it unusually attractive as a supply chain target. A typical application might carry one API key. A litellm installation carries all of them at once. An attacker who compromises a developer’s litellm environment in a company with three AI providers does not get one credential; they get the full set. LLM API keys have direct monetary value: they can be abused for inference at the victim’s expense, resold, or used for generation at scale. The usage logs they grant access to can reveal system prompts, routing architecture, and the shape of an organization’s AI infrastructure.
litellm is also heavily deployed in CI/CD pipelines and automated inference workloads where keys are present but no human is watching, which maximizes the exfiltration window before anyone notices unusual spend.
The .pth Execution Mechanism
Most Python developers know about .pth files only in the context of editable installs or namespace packages: files in site-packages whose lines get appended to sys.path. What is less commonly known is what happens to lines that begin with import.
Python’s site module, which runs automatically on every interpreter startup, processes .pth files through a documented but underappreciated code path. From CPython’s source:
If a filename ends in
.pth, its contents are processed as if they were part of a.pthfile […] Lines starting withimportare executed.
The execution happens via exec() in the site module’s own context, which has two important properties. First, it fires before your application code begins, before logging is initialized, before any import hooks your security tooling might register. Second, it fires on every Python invocation in that environment, not just when litellm is imported. A credential-stealing .pth file runs when you invoke Python to run an unrelated script, when you run tests, when your CI pipeline runs anything at all.
The malicious litellm_init.pth exploited exactly this. The name was chosen to look like a legitimate package artifact. The payload was base64-obfuscated to avoid naive grep-based detection:
import os; exec(__import__('base64').b64decode(b'aW1wb3J0IG9z...'))
After decoding, the code walked os.environ at startup and collected every recognizable API key pattern: OPENAI_API_KEY, ANTHROPIC_API_KEY, AZURE_OPENAI_API_KEY, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, COHERE_API_KEY, HUGGINGFACE_API_TOKEN, and more. It also targeted predictable credential file paths: ~/.aws/credentials, ~/.config/gcloud/, and local .env files. Exfiltration went over HTTP POST to an attacker-controlled server in a background thread, so no blocking occurred and no error surfaced. From the user’s perspective, Python started normally.
Because the .pth file fires on every invocation, rotating a key and setting the new value in the same environment did not end the exposure. The next time Python ran, the new credential would be harvested.
Detection
Finding malicious .pth files is straightforward once you know to look for them. The following one-liner scans all site-packages directories for .pth files that contain executable lines:
find $(python -c "import site; print(' '.join(site.getsitepackages()))") -name '*.pth' | xargs grep -l '^import'
A more thorough Python audit:
import site, os
for sitedir in site.getsitepackages() + [site.getusersitepackages()]:
if not os.path.isdir(sitedir):
continue
for fname in os.listdir(sitedir):
if not fname.endswith('.pth'):
continue
fpath = os.path.join(sitedir, fname)
with open(fpath) as f:
for i, line in enumerate(f, 1):
if line.strip().startswith('import '):
print(f"{fpath}:{i}: {line.strip()}")
You can also inspect a package wheel before installing it: pip download litellm==1.82.8 --no-deps -d ./tmp, then unzip the wheel and check for any .pth files with import lines.
Tools like Socket.dev and Phylum perform behavioral analysis that flags install-time code execution patterns, including .pth-based execution. Neither can catch a malicious release before it has been reported, but they reduce the detection lag.
If You Had 1.82.8 Installed
Simon Willison’s incident response account is worth reading for the way it captures genuine uncertainty in real time rather than offering a polished retrospective. The priority ordering he describes is correct: rotate credentials first, investigate second. Waiting for confirmation of exploitation before revoking keys inverts the risk calculus. Key rotation takes minutes; unauthorized API usage billed against a compromised key can accumulate far faster.
The environments to check extend beyond the obvious one. Virtual environments, Docker images built during the exposure window, CI runner caches, and global installs all need to be audited. Upgrading litellm without explicitly removing the .pth file from site-packages may leave the payload in place even after the package version changes.
After rotating, pull usage logs from every provider for the exposure window and look for anomalous requests: unexpected models, unusual timing, unexpected spend.
Structural Defenses
Hash-pinning dependencies is the strongest available protection against this class of attack:
litellm==1.82.7 \
--hash=sha256:abc123...
With --require-hashes, pip refuses to install any package whose content does not match the recorded hash, regardless of what PyPI serves for that version. pip-tools automates this with pip-compile --generate-hashes. The friction is real: litellm releases constantly, and keeping hashes current requires a disciplined lock-update workflow. Most AI projects skip this entirely.
Version pinning alone is insufficient protection. If the pinned version is the compromised one, you install the malware on every fresh environment.
On the publisher side, PyPI Trusted Publishers uses OIDC-based publishing from GitHub Actions rather than static upload tokens, eliminating the compromised token vector. Available since 2023, adoption remains uneven. PEP 740 attestations cryptographically link releases to their build environment and are increasingly supported by PyPI, but enforcement by install tooling is not yet the default.
There is also an architectural option: running litellm as a standalone proxy process (litellm --model gpt-4o --port 8000) rather than importing it into your application code. A compromise of the proxy process does not automatically reach your application’s runtime memory and other secrets. For production deployments, this boundary is worth maintaining regardless of supply chain concerns.
The Broader Problem
Python’s .pth execution behavior is documented and intentional. Changing it would break legitimate packages that rely on the mechanism for editable installs and namespace packages. There have been proposals to restrict or require opt-in behavior, but nothing has landed in CPython. The attack surface will persist.
The more important observation is about the AI tooling ecosystem specifically. These libraries are treated as infrastructure-of-convenience rather than security-critical dependencies deserving the same scrutiny as authentication or cryptography libraries. litellm’s aggressive release cadence creates ongoing maintainer credential exposure surface. The developer environments where it runs are dense with high-value secrets. The supply chain security practices common in other infrastructure domains, including hash pinning, attestation verification, and scoped credentials with spending limits, have not become standard practice in AI tooling.
The xz Utils backdoor in 2024 required years of patient social engineering. The litellm 1.82.8 attack required a single compromised publish token. The asymmetry between attacker effort and impact is unlikely to go unremarked upon.