All articles

LiteLLM attacked again: Malicious 1.107.0 wheels on PyPI and Docker Hub (October 2026)

Quincy Castro CISO

TL;DR

  • What happened: On October 21, 2026, an attacker published LiteLLM 1.107.0 to PyPI with a credential stealer and a remote code loader inside all seven wheels. The official LiteLLM Docker images for the same version carry the same payload.

  • Affected artifacts: litellm==1.107.0 on PyPI, plus the ghcr.io/berriai/litellm, litellm-database, and litellm-non_root images and docker.io/litellm/litellm under the v1.107.0, latest, main-latest, and main-stable tags.

  • Scale: litellm had more than 100 million downloads on PyPI in the last month. More than 2,000 packages depend on it, so many teams install it without a direct reference.

  • Risk: Python runs the payload each time it starts, even when no code imports litellm. The stealer takes SSH keys, cloud credentials, CI/CD secrets, browser passwords, AI tool tokens, and the LLM provider keys in a LiteLLM proxy database.

  • Chainguard customers: Not affected. Chainguard uses multiple layers of defense to make sure malicious packages are unavailable for consumption. Chainguard malware scanning flagged 1.107.0 just 16 minutes after release. Chainguard also builds LiteLLM from upstream source, and the source does not contain the payload.

  • What to do: If you installed 1.107.0 or pulled an affected image, isolate the host. Remove the startup files before you run Python again, then rotate every credential the host could reach.

At 11:39 UTC on October 21, 2026, LiteLLM 1.107.0 appeared on PyPI. The source code on GitHub was clean, but all seven prebuilt wheels carried a credential stealer and an hourly remote code loader. This is the second compromise of LiteLLM in seven months. In March 2026, the TeamPCP group used a stolen PyPI token to publish versions 1.82.7 and 1.82.8.

This post covers what happened, why Chainguard customers were not affected, and what every other LiteLLM user must do now.

Affected packages

One PyPI release and the official Docker images built from it:

Package

Malicious version

Published (UTC)

Latest safe version

litellm (all seven wheels)

1.107.0

11:39:51, October 21

1.106.2

Image

Affected tags

ghcr.io/berriai/litellm

v1.107.0

ghcr.io/berriai/litellm-database

v1.107.0

ghcr.io/berriai/litellm-non_root

v1.107.0

docker.io/litellm/litellm

v1.107.0, latest, main-latest, main-stable

The wheels cover every supported platform: macOS (x86_64 and arm64), manylinux and musllinux (x86_64 and aarch64), and Windows. Versions 1.106.2, 1.107.0rc1, and 1.107.0rc2 are clean. PyPI has quarantined 1.107.0.

The source distribution (sdist) does not contain the payload files. Its pyproject.toml has one added line, data = "cook", that the v1.107.0 Git tag does not have. The Git tag points to an ordinary signed commit. Do not trust the latest tag on Docker Hub or GHCR. Check each image digest against the list below.

What the malware does

The added data = "cook" setting tells the maturin build tool to copy a cook/ folder into litellm-1.107.0.data/purelib/ in each wheel. During install, pip puts the files from that folder into the root of site-packages, next to every other package. The attacker used this path to drop three files: cook.pth, cook.py, and sitecustomize.py.

  • Startup trigger: cook.pth contains one line, import cook. Python processes .pth files at startup, so cook.py runs each time any Python process starts in that environment. This includes pip, IDE language servers, and scripts that never import litellm.

  • Credential theft: cook.py decodes a 61 KB program that calls itself privcollect. It collects SSH, GPG, and TLS private keys, shell histories, browser passwords and cookies, AWS, Azure, and Google Cloud credentials, Kubernetes configs and service account tokens, Docker secrets, and package registry tokens. It also takes CI/CD secrets from GitLab Runner, Jenkins, Vault, Terraform, and Packer, and configuration from more than 15 AI coding tools.

  • Cloud and process secrets: The program reads the environment of every readable process on Linux. It queries the AWS, Google Cloud, and Azure instance metadata services, and it tests stolen AWS credentials with GetCallerIdentity.

  • LiteLLM proxy database: The program connects through DATABASE_URL and dumps LiteLLM_CredentialsTable and LiteLLM_ProxyModelTable. One compromised proxy host can expose the keys for many upstream AI providers at once.

  • Exfiltration: The program encrypts the stolen data with AES-256-CBC, wraps the key with an RSA-4096 public key, and packs the result as a tar.gz archive. It sends the archive over plain HTTP to www.litellm[.]dev/healthcheck. This look-alike domain is not the real litellm.ai domain.

  • Remote code loader: sitecustomize.py forks a detached daemon on Linux and macOS. Each hour, the daemon reads a server address from a file in a GitHub repository. If the address resolves, the daemon runs the stealer again, then downloads and runs Python code from that server with no integrity check.

  • Status of the loader: The GitHub file was empty when researchers analyzed it, so the loader did not run code. Wiz reports that GitHub has taken the repository down. The local stealer does not depend on that file and runs on its own.

  • Platform scope: On Windows, the daemon fails because it needs os.fork(). The stealer still runs on Windows.

Indicators of compromise

If you installed the affected version or ran an affected image (see Affected packages above), treat your environment as compromised.

Network indicators:

  • www.litellm[.]dev (exfiltration, path /healthcheck)

  • raw.githubusercontent[.]com/letters9813/pythonclient-test/main/domain.txt (dead drop for the C2 address)

  • http://{domain}/codepy (second stage, any domain that the dead drop returns)

  • HTTP header X-Token: XWiA8gZNa7NcX2XRoAEb on outbound requests

Wheel hashes (SHA256):

File

SHA256

litellm-1.107.0-cp310-abi3-macosx_10_12_x86_64.whl

7598727f209d717041db37f7dca271d3a7d328a279890e0e1d55375729aeb86d

litellm-1.107.0-cp310-abi3-macosx_11_0_arm64.whl

0aaf7ba346d41ef2f40390436738b001180d5a1c03aa093f121078a9969d9a1a

litellm-1.107.0-cp310-abi3-manylinux_2_28_aarch64.whl

668169d35b87bf51324080390b1b1c5ab12bd335cfe5f1a99e3689abd5e02ae6

litellm-1.107.0-cp310-abi3-manylinux_2_28_x86_64.whl

462d0c9f67ca522d9e0e4acd245e90b36ce0d5353604a66bb7c43a1ec4a6fcf7

litellm-1.107.0-cp310-abi3-musllinux_1_2_aarch64.whl

c2e32d0ce2548aa662c18bf5001911a79fe5b30cf8357132275add02fdcde054

litellm-1.107.0-cp310-abi3-musllinux_1_2_x86_64.whl

1bbce8caa94f01c0aaa0b7ddae51553a8a32e1ce53d96d44fe583272aff7ad00

litellm-1.107.0-cp310-abi3-win_amd64.whl

12d4ccc5c6f8d35cf4f2cf233b7f3e418e863e657d81ff5f5fc9e210005b9484

Image digests:

Image

linux/amd64

linux/arm64

ghcr.io/berriai/litellm:v1.107.0 and docker.io/litellm/litellm

sha256:bd1aeee419ea36da9aa2b657288da4101887be468f020ac63d22ba6ed84a3ea5

sha256:56b74573eed6e4aafe4b50603dbe7853e0345f188c8e44a09f1098caf1f4bcdf

ghcr.io/berriai/litellm-database:v1.107.0

sha256:24581ddc30767f16ecb69d2b0517556f2cd7dea56971adb42a3d1beb97d69523

sha256:827dd893c74cf621a860a66a3a447a5c732533f087589ce905573e285c0a239c

ghcr.io/berriai/litellm-non_root:v1.107.0

sha256:645a67d810628252d63df85dcc8824c58bcc77cf8383f4d3e6321f0ccc3ce9e4

sha256:f75c8edae59b72b1ebfd617295833f465c85bb2fab9c1d60f21b496db86cb8da

Package-level indicators:

  • Startup files in the root of site-packages: cook.pth, cook.py, and sitecustomize.py

  • Wheel manifest entries in RECORD: three paths under litellm-1.107.0.data/purelib/

  • Build setting in the sdist pyproject.toml: data = "cook" under [tool.maturin]

Other artifacts:

  • Run-once marker: /tmp/.pc_once_72129118c59f5636c0bb. The stealer writes this file after a successful exfiltration. If the file exists, assume your secrets are gone. If it does not exist, the host is not proven clean.

  • Loader lock: /tmp/remote_code_loader.lock

  • Temporary files: /tmp/.pc_sq_* and /tmp/.pc_e_*

  • Environment variable: PRIVCOLLECT_ACTIVE=1

  • A detached Python process with/as its working directory

How to check if you're affected

Warning: do not run Python or pip inside an environment that you suspect. Each Python start runs the payload again. Use shell tools for every check below.

  1. Search for the startup files. find / -path "*site-packages/cook.pth" -o -path "*site-packages/cook.py" -o -path "*site-packages/sitecustomize.py" 2>/dev/null Some environments have a legitimate sitecustomize.py. Inspect any match before you delete it.

  2. Search your lockfiles and requirements files for the affected version. grep -rn "1.107.0" requirements*.txt poetry.lock uv.lock Pipfile.lock pyproject.toml 2>/dev/null | grep -i litellm

  3. Look for the host markers. ls -la /tmp/.pc_once_* /tmp/remote_code_loader.lock /tmp/.pc_sq_* /tmp/.pc_e_* 2>/dev/null

  4. Find a process that holds the loader lock. lsof /tmp/remote_code_loader.lock 2>/dev/null

  5. List your LiteLLM images by digest and compare them with the digests above. docker images --digests | grep -i litellm

  6. Search DNS and proxy logs for www.litellm.dev and for the path letters9813/pythonclient-test.

A lockfile match alone needs investigation, even if your application never imports litellm. The trigger is on Python startup rather than the import.

Immediate steps if you're affected

  1. Isolate the host or container. Treat every credential and every file that the host could reach as compromised.

  2. Remove the startup files before you start Python again. Delete cook.pth, cook.py, sitecustomize.py, their files in __pycache__, and the /tmp markers with shell tools. A pip uninstall starts Python and runs the payload again.

  3. Stop the loader. Find and stop each detached Python process that holds /tmp/remote_code_loader.lock.

  4. Block egress to www.litellm[.]dev, the dead drop URL, and any domain that the dead drop returned. Keep your DNS, proxy, endpoint, and cloud audit logs.

  5. Rotate exposed credentials: SSH and GPG keys, TLS private keys, cloud and Kubernetes credentials, package publish tokens, source control tokens, AI tool tokens, browser-saved passwords, DATABASE_URL, LiteLLM master and virtual keys, and every provider key in LiteLLM_CredentialsTable.

  6. Investigate any request to /codepy. That stage can run any code, so the stealer's behavior does not limit its effects.

  7. Rebuild virtual environments, hosts, and images from verified artifacts. Pin litellm==1.106.2 in the new environment. Pin Docker images by digest, not by the latest tag.

Why Chainguard customers were protected

Chainguard Libraries customers were not exposed to this attack. Chainguard scans every new package version for malicious behavior before it can be served from Chainguard Repository. No customer pulled litellm 1.107.0

Chainguard Libraries apply multiple layers of protection. Two of them stopped this attack.

  • First, Chainguard malware scanning flagged litellm 1.107.0 at 11:55 UTC on October 21, 2026, 16 minutes after the first wheel reached PyPI. That block kept the PyPI release out of Chainguard Repository, preventing users from pulling it.

  • Second, once a new version is scanned for malware, Chainguard builds LiteLLM from its source in the Chainguard Factory. The attacker put the payload into the prebuilt wheels on PyPI, and the v1.107.0 Git tag does not contain it. A wheel that Chainguard builds from that source does not get cook.pth, cook.py, or sitecustomize.py.

Chainguard Containers customers were not affected either.

  • The Chainguard LiteLLM image builds from source in the Chainguard Factory. It does not use the upstream BerriAI image or the PyPI wheels.

  • Our Python builds fetch dependencies through Chainguard Libraries and not straight from PyPI, adding another layer of protection.

Customers who configure cooldown policy get another governance layer. A cooldown keeps a new release out of reach until scanners and researchers have time to examine it.

This is the second time this year that LiteLLM has shipped malware to PyPI. It is also the second time that Chainguard customers had nothing to clean up. In March, the malicious versions had no verifiable source code, so Chainguard did not build them.

The bigger picture

This is the second documented compromise of LiteLLM in 2026. Researchers have not linked it to TeamPCP or to the March access path.

Takeaways:

  1. The attacker did not change the source. The Git tag points to a signed commit, and the sdist has no payload files. A team that reviews only source code, a Git tag, or an sdist misses this attack. The wheel is the artifact that runs, so the wheel is the artifact that you must trust.

  2. The payload targets AI infrastructure. It goes after the LiteLLM proxy database, which can hold keys for many AI providers in one table. It also collects configuration from more than 15 AI coding tools. One compromised AI gateway can expose the model access of a full organization.

  3. The same package can fall to two different attackers in one year. LiteLLM rotated its maintainer accounts after March. The new attack still reached PyPI and Docker Hub through a path that nobody has explained yet. A package's clean history does not predict a clean next release.

Want to learn more about Chainguard?

Attacks like today’s continue a trend of supply chain attacks on language dependencies. Chainguard Libraries lets your team turn off live access to PyPI, npm, Maven Central, Go Proxy, and NuGet and use secure-by-default endpoints instead. Our dependency catalog applies multiple layers of security controls: we build the newest and most popular packages from verified source, run malware and greyware scanning, and apply cooldowns. A package becomes available to your team only after it clears all of them.

Learn more about Chainguard Libraries for Python, or get in touch with our team.

Share this article

Start building with Chainguard