LiteLLM 1.107.0 PyPI Supply-Chain Compromise: How a Malicious Python Package Stole Credentials at Every Startup

PyPI

On 21 October 2026 a malicious version of the popular LiteLLM package appeared on the Python Package Index. Version 1.107.0 stayed live for roughly two hours. In that short window it was downloaded approximately 49,000 times. The payload did not need anyone to import LiteLLM. Simply starting a Python interpreter in an affected environment was enough to launch a credential stealer and, on Unix systems, a remote-code loader.

LiteLLM is widely used as a proxy and unified interface to dozens of large-language-model providers. That position means it frequently holds API keys, database connection strings, and cloud credentials. The compromise therefore reached developer laptops, continuous-integration agents, test containers, and production proxies. This was the second supply-chain incident involving LiteLLM in 2026. Enterprise customers who pulled from LiteLLM’s private registry were not affected.

Organizations that treat LiteLLM as a central gateway often store the API keys for OpenAI, Anthropic, Google, Azure OpenAI, and other providers in one place. A single compromised host can therefore expose a broad set of upstream credentials. The same package is also installed on developer workstations for local experimentation and in CI pipelines that test LLM applications. That dual use—production proxy and developer tool—widened the set of environments that could be affected by a single malicious release.

The incident is a textbook example of how Python’s own startup mechanisms can be turned into persistence and theft tools, and how a carefully prepared release pipeline can still be subverted when developer tokens are stolen.

What Happened: Timeline and Attack Chain

All times below are UTC on 21 October 2026 unless noted.

At approximately 10:08 an attacker used stolen GitHub CLI OAuth tokens belonging to three LiteLLM engineers. One token belonged to a GitHub organization owner and was used to override branch-protection rules that had rejected an earlier unauthorized change. The attacker then pointed LiteLLM’s private release workflow at a lookalike repository named Berri-AI/litellm (one hyphen different from the legitimate BerriAI/litellm).

A staging run began at 10:46 and completed successfully. The production release pipeline started shortly afterward. A second developer’s credential satisfied the production approval gate. Between 11:39:51 and 11:40:13 seven malicious wheels for litellm==1.107.0 were uploaded to PyPI. Docker images carrying the same payload were published minutes later under the tags v1.107.0, latest, main-latest, and main-stable on both Docker Hub and GitHub Container Registry.

PyPI quarantined and removed the package by 13:23. The affected Docker tags were cleaned or repointed by 14:20. The official source tag for v1.107.0 on GitHub never contained the malicious files; the poisoned artifacts were built from the attacker-controlled repository.

LiteLLM’s later investigation (published 23 October) traced the initial token theft further back. A configuration file committed to a public repository on 27 May had contained the master key of an internal LiteLLM Agent Platform deployment. That key allowed the attacker to read logs that contained GitHub CLI tokens. The tokens were later used to modify the release workflow. Microsoft Security Research independently confirmed the use of stolen developer credentials to redirect the pipeline to the lookalike repository.

The short exposure window limited the absolute number of installs, but 49,000 downloads of a package that sees roughly 23 million weekly downloads is still significant. Socket’s scanners flagged the malware within seven minutes of publication. Multiple independent researchers (SafeDep, JFrog, Wiz, Datadog, Microsoft) published technical analyses the same day or the day after.

How the Malware Works

The malicious wheels added three files that pip placed directly into the root of site-packages:

  • cook.pth (12 bytes)
  • cook.py (approximately 22 KB)
  • sitecustomize.py (approximately 25 KB)

Python processes every .pth file in site-packages at startup. Any line that begins with import is executed. The single line in cook.pth was:

import cook

That statement loads cook.py. The file wraps a large base64- and zlib-compressed payload that, once decoded, identifies itself as PRIVCOLLECT. The stealer runs in a background thread and collects a wide range of credentials before encrypting and exfiltrating them.

A second, independent startup hook is Python’s built-in import of a module named sitecustomize if it exists in site-packages. The attacker’s sitecustomize.py performs the same collection and, on Linux and macOS, forks a detached daemon. Every hour the daemon reads a file from a GitHub repository (letters9813/pythonclient-test/main/domain.txt). If that file contains a domain name, the daemon downloads Python code from http://<domain>/codepy and executes it. The dead-drop file was empty during the observed window, so no second-stage payload was confirmed in the wild. The mechanism itself remained available until the GitHub account was taken down.

Both hooks fire on every Python process start, including pip, IDE language servers, test runners, and scripts that never import litellm. Deleting only the litellm package directory leaves the three files in place.

What the stealer collects

SafeDep, Microsoft, and Datadog analyses list the collection modules. They include:

  • Cloud credentials: AWS (~/.aws), Azure CLI tokens, Google Cloud application-default credentials, Kubernetes service-account tokens, Docker config, instance-metadata service credentials.
  • Developer and AI-tool secrets: GitHub, GitLab, npm, and PyPI tokens; configuration and auth files for Claude, Cursor, Copilot, Gemini CLI, Codex, and other AI coding assistants; VS Code MCP settings.
  • Browser data: Chromium-family saved passwords (decrypted where possible) and cookies; Firefox logins.json and key4.db.
  • SSH, GPG, and TLS private keys; shell and database-client histories; environment variables of the current and other processes (via /proc on Linux).
  • LiteLLM proxy database: when a DATABASE_URL is present, the stealer uses Prisma to query the tables that store upstream provider API keys.

The breadth of collection is deliberate. By targeting both traditional developer credentials and the newer generation of AI coding-tool tokens, the stealer maximizes the chance of obtaining something valuable on any given host. Instance-metadata retrieval is especially relevant in cloud environments: a container or EC2 instance that starts Python after the malicious package is installed can automatically obtain temporary credentials for the attached IAM role and exfiltrate them.

The collected JSON is encrypted with a fresh AES-256-CBC key. That key is wrapped with an embedded RSA-4096 public key using OAEP. The result is packaged as a tar.gz containing data.bin, key.bin, and a plaintext hdr.json, then POSTed over plain HTTP to http://www.litellm[.]dev/healthcheck with a fixed X-Token header. After a successful 200 response the stealer writes the marker file /tmp/.pc_once_72129118c59f5636c0bb.

The encryption means a network capture cannot be decrypted without the attacker’s private key. The use of a look-alike domain (litellm.dev instead of litellm.ai) is a classic technique that can bypass casual inspection of outbound traffic. The stealer also sets the environment variable PRIVCOLLECT_ACTIVE=1 so that child processes do not immediately re-collect, reducing noise while still ensuring the initial process performs the theft.

A minimal illustration of the two startup mechanisms (clean, educational versions only):

# Example of a .pth file that Python will execute at startup
# cook.pth
import cook
# Minimal sitecustomize.py skeleton (the real file was far more complex)
# This runs automatically if present in site-packages
import os
import time

def background_check():
    while True:
        # In the real malware this read a GitHub raw file for a domain
        # then downloaded and exec()'d code from that domain
        time.sleep(3600)

# Real malware forked and detached; this is only a teaching sketch
if os.name != "nt":
    # fork/setsid logic omitted for brevity
    pass

These snippets show why the hooks are powerful: they require no application-level import and survive as long as the files remain in site-packages.

Impact and Who Was Affected

Anyone who ran pip install litellm (or an unpinned install that resolved to 1.107.0) between 11:39 and 13:23 UTC, or who pulled one of the four compromised Docker tags between 11:46 and 14:20 UTC, should treat the environment as compromised. The last known clean release is 1.106.2.

Wiz reported roughly 49,000 downloads of the malicious version. Because the payload activates on any Python start, the risk extends beyond systems that actually use LiteLLM as a gateway. A developer who installed the package to experiment, a CI job that pulled the latest tag, or a container that started a Python process after the image was pulled could all have executed the stealer.

LiteLLM stated that its private enterprise registry was not affected. Customers who exclusively used that channel and never pulled the public 1.107.0 artifacts are safe. The official GitHub source tag was also clean, so anyone who built from the tagged source rather than the published wheels avoided the payload.

This was the second LiteLLM supply-chain compromise of 2026. Datadog linked an earlier incident (versions 1.82.7 and 1.82.8) to TeamPCP activity. No connection between the two incidents has been established.

The blast radius is especially relevant for teams that use LiteLLM in production as a centralized proxy. Those deployments often hold the API keys for every model provider the organization uses. A single compromised proxy host could therefore expose a large set of upstream credentials.

Detection and Indicators of Compromise

The strongest and safest signals are the three files themselves. Because starting Python re-triggers the payload, detection should be performed with shell tools, not with python or pip.

Safe search (run from a clean shell):

find / -path "*/site-packages/cook.pth" -o \
       -path "*/site-packages/cook.py" -o \
       -path "*/site-packages/sitecustomize.py" 2>/dev/null

Presence of any of those three files is confirmation of exposure. Absence does not prove cleanliness if temporary files or container layers have already been cleaned.

Other host indicators:

  • /tmp/.pc_once_72129118c59f5636c0bb (written after a successful exfiltration)
  • /tmp/remote_code_loader.lock (held by the Unix daemon)
  • Environment variable PRIVCOLLECT_ACTIVE=1
  • Detached Python process with parent PID 1, own session, and working directory /

Network indicators:

  • Connections to www.litellm[.]dev or the path /healthcheck
  • HTTP header X-Token: XWiA8gZNa7NcX2XRoAEb
  • Requests to raw.githubusercontent.com/letters9813/pythonclient-test/main/domain.txt
  • Requests to 169.254.169.254 (AWS instance metadata) originating from Python processes

Container image index digest of the compromised images:

sha256:1bbcb1443bc962e45f5aad53fe3d2c30600e0379715fb6c664cdd3ac126c339e

Microsoft published Defender signatures including Trojan:PyPI/Stealer.SA and Trojan:PyPI/LiteLlmSupplyChain.A. Multiple vendors published SHA-256 hashes for the seven wheels and the three payload files; those hashes differ slightly on Windows because of line-ending differences.

Immediate Response Checklist

If you confirm or strongly suspect exposure, follow this order. Do not start Python or pip inside the suspect environment.

  1. From a clean shell, delete cook.pth, cook.py, sitecustomize.py and any matching __pycache__ files from every affected site-packages directory.
  2. Kill any process holding /tmp/remote_code_loader.lock.
  3. Rebuild the virtual environment or container image from scratch, pinning to litellm==1.106.2 (or an earlier verified clean version). Prefer digest pinning for Docker images.
  4. Assume every secret the host could read has been stolen. From a different, clean machine rotate:
    • All cloud IAM roles and instance profiles that the host could assume
    • Every LLM provider API key stored in the LiteLLM proxy database
    • The LiteLLM master key and any virtual keys
    • The DATABASE_URL password
    • SSH keys, cloud CLI tokens, AI-tool credentials, and any other secrets present in the environment or browser profiles
  5. Review cloud audit logs (CloudTrail, Azure Activity Log, GCP Audit Logs) for anomalous use of the affected identities, especially GetCallerIdentity or equivalent calls around the installation time, followed by activity from unexpected source IPs.
  6. Check for the marker file and lock file; their presence confirms the stealer ran, but their absence proves nothing.

Rebuilding from a known-good base is safer than trying to surgically clean an existing environment. Containers and ephemeral CI runners are easier to replace than long-lived developer workstations or production hosts.

Lessons for Python and AI Supply Chains

Python’s .pth files and the sitecustomize import are legitimate extension points. They also give any package that can write to site-packages the ability to run code on every subsequent interpreter start. That property turns a one-time install into persistent access that survives even if the original package is later uninstalled. In practice this means a malicious package can continue to operate long after an administrator believes it has been removed, provided the three files remain.

Several practical controls reduce the chance of a similar incident:

  • Pin versions and, where possible, verify package hashes or use a lock file that records exact artifacts. Tools such as pip-tools, uv, or Poetry lock files make this easier.
  • Treat mutable tags such as latest as unsafe for production. Prefer immutable tags or digests. The compromised latest and main-stable tags illustrate the risk of floating references.
  • Restrict who can modify release workflows and require multi-party approval that cannot be satisfied by a single stolen token. LiteLLM had already moved releases to a private repository and added approval gates after an earlier incident; the attacker still succeeded by compromising multiple independent tokens.
  • Monitor for unexpected files appearing in site-packages, especially new .pth files or a sitecustomize.py that was not previously present. Simple file-integrity checks or endpoint detection rules can catch these artifacts.
  • Keep developer tokens short-lived and avoid logging them. The LiteLLM investigation noted that Claude Code responses had, in some cases, embedded GitHub CLI tokens that later appeared in logs of an internal deployment.
  • Prefer private registries or vendored dependencies for high-value internal packages when the public supply chain cannot be fully trusted. Organizations that already maintain an internal PyPI mirror can add an allow-list or hash-pinning step before packages reach developer machines.

The earlier 2026 LiteLLM incident and this one both involved the release process rather than a vulnerability in the application code itself. That pattern is common in modern supply-chain attacks: the source that developers review is clean; the published artifact is not. Reviewing the GitHub tag would not have revealed the malicious files, because they were injected only during the build of the wheels and container images.

For teams building AI applications the practical takeaway is straightforward. Treat every dependency that can hold or access credentials as high-value. Pin it, verify it, and have a ready checklist for the day a popular package is compromised. The window in this case was short, but the combination of automatic startup execution and broad credential collection means that any environment that ran Python after the install must be considered exposed until proven otherwise.

Conclusion and Next Steps

The LiteLLM 1.107.0 compromise lasted only a few hours and was caught quickly by both the project and independent researchers. The combination of Python startup hooks, a broad credential collector, and a still-armed remote-code loader nevertheless created a high-impact window for anyone who installed the package or pulled the affected images.

Teams that use LiteLLM, or any similarly popular Python package in the AI tooling ecosystem, should treat the incident as a prompt to review pinning practices, rotate credentials if exposure is possible, and add detection for unexpected files in Python environments. The last clean version at the time of writing is 1.106.2; the malicious 1.107.0 artifacts have been removed from PyPI, Docker Hub, and GHCR.

If you maintain packages that other people depend on, the incident is also a reminder that release pipelines are high-value targets. Stolen developer tokens remain one of the most effective ways to turn a trusted build system against its own users.

Leave a Reply