LiteLLM Hijacked Again: Malicious 1.107.0 Wheels Steal Credentials and Load Remote Code
The JFrog Security Research team identified malicious code in litellm 1.107.0, a widely used Python interface and proxy for large language model providers. The payload wheels are a functional copy of the legitimate project with three additional files that execute at Python startup, steal a broad set of credentials and secrets, and provide an hourly remote-code execution channel. The payload can run even when an application never imports litellm .
The official litellm/litellm:1.107.0 Docker image is also affected. Systems that pulled the mutable latest , main-latest , or main-stable tags while they referenced 1.107.0 should be treated as exposed as well. Docker Hub has since removed the affected image and all affected tags.
This is the second documented hijacking of LiteLLM in 2026. The new compromise is especially instructive because the published source distribution does not contain the three payload files found in the wheel. Reviewers who inspect only source code, a Git tag, or an sdist can therefore miss malicious behavior delivered in a platform-specific binary artifact.
The open source community is actively following the new incident in LiteLLM issue #47826 .
PyPI has since removed the compromised 1.107.0 release.
All seven PyPI wheels for litellm 1.107.0 contain the payload, across macOS, Linux, musl Linux, and Windows targets. The source tarball lacks cook.pth , cook.py , and sitecustomize.py , while its pyproject.toml contains an added data = "cook" setting that is absent from the corresponding Git tag. Our local copy of the source distribution likewise contains this setting but not the three malicious payload files.
The payload entries ZIP timestamps and metadata with legitimate wheel files, indicating that they were included during normal wheel construction rather than appended through manual ZIP repacking. This narrows the likely point of injection, but it does not reveal who altered the build tree, how that alteration occurred, or how the artifacts were published. The exact build or publishing compromise remains unknown.
LiteLLM was already hijacked
In March 2026, JFrog Security Research documented LiteLLM's first compromise . Its pipeline ran a malicious, unpinned Trivy 0.69.4 release, exposing CI/CD and publishing credentials.
The stolen credentials were used to publish backdoored litellm versions 1.82.7 and 1.82.8, which harvested credentials, spread across Kubernetes nodes, and installed a persistent backdoor. JFrog tracks those releases under XRAY-955589.
The new 1.107.0 compromise uses different payloads and infrastructure. No evidence currently links it to TeamPCP or the earlier access path, so the incidents should be treated as separate campaigns.
The source archive and built wheel tell different stories
Its RECORD manifest includes three files under litellm-1.107.0.data/purelib/ :
The data = "cook" setting directs Maturin to copy a build-time cook/ directory into the wheel's data area. The wheel build therefore had access to files that were not present in the published source archive.
Python wheel installers place files from this purelib directory into the environment's site-packages root. The corresponding 1.107.0 sdist contains none of these payload files. A comparison of the legitimate library tree found no other injected code: aside from expected wheel-specific artifacts such as the compiled Rust bridge and packaging differences, the rest of the wheel appears consistent with the legitimate release.
This is wheel-level or build-artifact-level injection. The observable evidence is consistent with injection during or after wheel construction, while the matching metadata favors inclusion during the ordinary build process over later manual repacking. It does not prove that a particular CI system, maintainer machine, PyPI account, or publishing credential was compromised.
The distinction matters operationally. Source review can show that the main litellm code is legitimate while still missing executable files added to a built artifact. Package consumers and security tools must verify the exact wheel they install, including its RECORD contents and startup hooks, rather than treating an sdist or repository tag as a complete representation of every published artifact.
At the time of writing, a read-only retrieval of domain.txt , the dead-drop file used by the remote loader, returned HTTP 200 with an empty plain-text body of exactly zero bytes and no redirect. No domain was present or contacted. This made the loader dormant at observation time, but changing the file can re-arm it. This time-sensitive result does not neutralize the wheel: the local cook.py credential stealer executes independently of domain.txt and remains active.
Python startup hooks trigger the compromise
The malicious wheel uses two Python startup mechanisms. The first file, cook.pth , contains only:
Python's site module processes .pth files during normal interpreter initialization and executes lines beginning with import . As a result, cook.py runs during normal Python startup in the affected environment, regardless of whether the application imports litellm .
The second mechanism is sitecustomize.py . After processing site paths, Python automatically attempts to import a module named sitecustomize . The injected module uses this behavior to start a detached loader. Together, these hooks turn normal Python invocations in the environment, including administrative use of pip , into potential retriggers.
The sitecustomize.py daemon path cannot complete on Windows because the code relies on os.fork() . It fails silently there, but cook.pth still imports cook.py , so Windows installations remain exposed to the credential stealer.
cook.py launches the privcollect credential stealer
The two-line cook.py file contains a Base64-encoded, zlib-compressed Python program passed directly to exec() . Static decoding, without executing the sample, produces approximately 61 KB of plain Python source. The decoded program identifies itself as privcollect .
The heavily shortened wrapper below preserves only a prefix of the malicious encoded data:
The cook.py copy starts the collector in a daemon thread and uses /tmp/.pc_once_72129118c59f5636c0bb as a run-once marker. The marker is created only after successful exfiltration, so its absence does not establish that a host is clean. The payload also sets PRIVCOLLECT_ACTIVE=1 to limit duplicate execution within inherited process environments.
What the stealer collects
The collector is designed for developer workstations, CI/CD workers, cloud workloads, containers, and LiteLLM proxy deployments. It targets:
SSH files, GPG private keys, TLS private keys, shell histories, database client histories, and key files such as .pem , .key , .p12 , and .pfx
Chromium-family passwords, cookies, web data, and history, plus Firefox login databases, key databases, and browsing history
AWS, Azure, and Google Cloud credentials; Kubernetes configurations and service-account tokens; Docker and Swarm secrets; GitHub CLI, rclone , and .netrc credentials
CI/CD and infrastructure-as-code secrets from GitLab Runner, Jenkins, Vault, Terraform, and Packer
Package manager and source-control credentials from .git-credentials , .npmrc , .pypirc , pip, Yarn, Maven, Gradle, Composer, RubyGems, and Cargo
AI development credentials and configurations for Cursor, Claude, Codex, Gemini, GitHub Copilot, Hugging Face, Kiro, Windsurf, Zed, Continue, aider, goose, opencode, Qwen, Cline, Roo, and Cody
Environment variables from the current process and every readable /proc/ /environ file on Linux
Cloud instance credentials from AWS IMDS and ECS task metadata, Google Cloud metadata, and Azure IMDS
LiteLLM proxy data reached through DATABASE_URL , including LiteLLM_CredentialsTable and LiteLLM_ProxyModelTable
This shortened excerpt from the decoded source shows selected collection modules:
On Linux, the environment-variable collector also reads other processes' environments:
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
