deniz.in

Markets

Weather

Loading weather

· via Hacker News – Front Page (native)

Malicious LiteLLM 1.107.0 on PyPI ran credential stealer in every Python process

The LiteLLM 1.107.0 release on PyPI installed a credential stealer and an hourly backdoor that ran in any Python process on an infected host, according to SafeDep. PyPI has since removed the release.

Malicious LiteLLM 1.107.0 on PyPI ran credential stealer in every Python process

The 1.107.0 release of LiteLLM, published to PyPI on October 21, 2026, shipped a credential stealer and a remote-code-execution backdoor, according to an analysis by SafeDep. All seven wheels for Linux, macOS and Windows installed the payload as three files in site-packages, and the malicious code ran whenever any Python process started on the host — including processes that never imported litellm. PyPI has since removed the release, and the upstream GitHub repository never contained the implant.

What the malicious code did

Installing litellm 1.107.0 dropped three files into site-packages, two of which Python executes automatically at interpreter startup. cook.pth held a single line, "import cook", which loaded cook.py, the stealer. sitecustomize.py, a module Python imports at startup by design, spawned the backdoor daemon.

SafeDep found that cook.py wraps roughly 61 KB of Python inside a layer of exec(zlib.decompress(base64...)). Once running, it collected SSH keys, AWS, Azure and Google Cloud credentials, Kubernetes and Docker configs, browser passwords and history, GPG keys and environment variables — including those of other processes, read through /proc. It also harvested CI/CD secrets such as GitLab runner configs, Jenkins credentials and Vault tokens, plus auth tokens from AI developer tools including Claude Code, Cursor, Copilot, Gemini CLI and Codex. Most damaging for LiteLLM deployments, it read DATABASE_URL and dumped the proxy database tables that hold LLM provider API keys.

The results were encrypted with a random AES-256-CBC key, itself wrapped using RSA-OAEP, packed into a tar.gz and posted to www.litellm[.]dev/healthcheck. A marker file under /tmp limited collection to once per host.

The sitecustomize daemon took an exclusive lock so only one copy ran per machine, detached itself with fork() and setsid(), and then, every hour, read a server address from a file in an attacker-controlled GitHub repository, re-ran its own copy of the stealer (with no once-per-host limit), and downloaded and executed a payload named codepy from that server. Because the address lived in an editable GitHub file, infected hosts could be repointed at will.

How the implant entered the release

The only visible difference in the source distribution was a single added line in the maturin build section of pyproject.toml — data = "cook" — which copies a cook/ folder into the wheel, where pip then installs it into site-packages. The sdist itself omitted the cook/ folder, hiding the actual payload. No public commit in the BerriAI/litellm repository contains that line or the folder.

The release also did not match its git tag. According to SafeDep, the v1.107.0 tag points to a commit whose pyproject.toml declares version 1.108.0; the shipped sdist instead matches a later commit on main plus five edited files and the cook/ folder. One of those edits pinned the companion packages to the same values as the previous release candidate, so a routine dependency diff against rc2 showed nothing unusual.

Built by the project's own pipeline

Forensic details suggest the wheels came out of BerriAI's normal release pipeline rather than an attacker's own machine. They were built with maturin 1.9.4, the same version as legitimate releases, carry correct RECORD entries with no post-build edits, and their zip timestamps show separate native builds for macOS (11:20 UTC), Linux (11:21) and Windows (11:37). In the Windows wheel, all 2,804 .py files — the three implant files included — have CRLF line endings, a pattern produced by a git checkout on Windows with line-ending conversion.

Yet none of the 539 public GitHub Actions runs between 09:30 and 12:30 UTC built or published the release, and PyPI holds no provenance attestations for it. SafeDep assesses, with moderate to high confidence, that the project's private release pipeline built these wheels from attacker-modified git source — plausibly via a compromised maintainer or bot credential, a private branch or mirror, or manipulation of the reference the pipeline builds from. A stolen PyPI token used from attacker machines fits the evidence less well; only BerriAI's pipeline logs can settle the question.

Around 13:40 UTC, roughly two hours after the upload, the release vanished from PyPI and the attacker's GitHub account was deleted. The daemon now finds no server address and does nothing, though hosts infected earlier could already have received second-stage code. SafeDep could not recover the server address.

Why it matters

The incident is a serious supply-chain compromise with three uncomfortable lessons. First, the .pth and sitecustomize techniques break out of the package's trust boundary: one bad wheel on a CI runner or developer laptop exposes every secret any Python process on the host can see, not just LiteLLM's. Second, the target list — cloud credentials, CI/CD secrets, AI tool tokens and the LLM provider keys in LiteLLM's proxy database — shows attackers treat AI gateways as prime credential stores. Third, the attack defeats common review practices: the dependency diff against the previous release candidate was clean, and without provenance attestations or public build logs, tampering inside a private pipeline is nearly invisible. SafeDep confirms versions 1.106.2, 1.107.0rc1 and 1.107.0rc2 are clean, so anyone who pulled 1.107.0 should remove it and check for the indicators of compromise SafeDep published.

  • #supply-chain
  • #security
  • #pypi
  • #python
  • #litellm

Related posts