threat_intelligence1979 wordsRead on Huntaegis

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

LiteLLM attacked again: Malicious 1.107.0 wheels on PyPI and Docker Hub (October 2026) - View all articles Quincy Castro CISO Chainguard 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. 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.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 Look for the host markers. ls -la /tmp/.pc_once_* /tmp/remote_code_loader.lock /tmp/.pc_sq_* /tmp/.pc_e_* 2>/dev/null Find a process that holds the loader lock. lsof /tmp/remote_code_loader.lock 2>/dev/null List your LiteLLM images by digest and compare them with the digests above. docker images --digests | grep -i litellm 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 Isolate the host or container. Treat every credential and every file that the host could reach as compromised. 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. Stop the loader. Find and stop each detached Python process that holds /tmp/remote_code_loader.lock. 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. 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. Investigate any request to /codepy. That stage can run any code, so the stealer's behavior does not limit its effects. 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: 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. 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. 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 Related articles - security Announcing the Chainguard Compliance Dashboard - security Athena's disclosures begin - security The flood is coming and the pipes were already full - security Athena spotlight: Black Duck on the importance of flagging zero-days at scale - security How financial services companies can modernize their software supply chain - security Proven, not promised: Chainguard Containers achieves SLSA Build Level 3

How it works

Once you click Generate, Ollama reads this article and crafts 5 comprehension questions. Your answers are graded against the article content — general knowledge won't be enough. Score 70+ to count toward your certificate.

Questions are cached — you'll always get the same 5 for this article.