Dev.to Security 🔐 Cybersecurity 👁 0 📖 5 min read

A Popular AI Package Was Backdoored on PyPI, and It Ran in Every Python Process on Your Machine

On October 21, 2026, version 1.107.0 of LiteLLM landed on PyPI with a credential stealer and a remote code execution backdoor baked in. All seven wheels - Linux, macOS, and Windows - installed them as three files in site

A Popular AI Package Was Backdoored on PyPI, and It Ran in Every Python Process on Your Machine

LiteLLM PyPI compromise banner

On October 21, 2026, version 1.107.0 of LiteLLM landed on PyPI with a credential stealer and a remote code execution backdoor baked in. All seven wheels - Linux, macOS, and Windows - installed them as three files in site-packages. Both payloads ran whenever any Python process started on the host, even one that never imported litellm. The upstream GitHub source never contained them. PyPI yanked the release by 13:23 UTC the same day. By then it had been live long enough to do real damage, and the Docker images had already spread it further. LiteLLM confirmed the compromise in their incident report, which lists remediation steps and confirms the GitHub source was clean. The gap between publication and removal was roughly two and a half hours. That's a long time for a package sitting in the dependency tree of thousands of AI applications.

BerriAI litellm GitHub repository

What actually got installed

Installing litellm==1.107.0 dropped three files into site-packages. Two of them ran on every Python startup, and that's what makes this worse than a typical malicious package.

  1. cook.pth contained a single line: import cook. Python executes every import line in every .pth file in site-packages at startup, so the stealer ran in your application, in pip, in IDE language servers, and in CI jobs. cook.py collected SSH keys, cloud credentials, CI/CD secrets, AI tool tokens, browser passwords, and the LLM provider keys stored in a LiteLLM proxy database. It encrypted the data and sent it to www.litellm[.]dev.
  2. sitecustomize.py started a background daemon. Every hour, the daemon read a server address from a GitHub file, ran the stealer again, and downloaded and executed code from that server.

The sdist left out the cook/ folder, so the only visible change in the source distribution was one extra line in pyproject.toml. No public commit in BerriAI/litellm contains data = "cook" or a cook/ folder. The wheel's RECORD listed the three added files with their sizes: cook.pth at 12 bytes, cook.py at 22,252 bytes, sitecustomize.py at 25,509 bytes.

The .pth mechanism is the quiet part. Most developers never look at site-packages, and .pth files are loaded by the interpreter before any application code runs. That means the stealer had access to the full process environment - including anything in memory - before the host application could object. The sitecustomize.py trick is older but equally effective: Python imports sitecustomize automatically if it exists on the path, so the daemon started without any explicit import statement anywhere. Together, the two files gave the attacker both immediate exfiltration and a persistent hourly callback, with no cooperation from the host application and no visible change to the package's public API.

How the implant got into the release

The pyproject.toml in the 1.107.0 sdist had one injected line in the maturin build section:

[tool.maturin]
...
python-source = "."
data = "cook"                     # <-- injected
python-packages = ["litellm_cli"] # legitimate, present upstream

The maturin data = "cook" setting copied a cook/ folder into litellm-1.107.0.data/purelib/ inside the wheel. Pip then installed that folder into site-packages. The sdist leaves out the cook/ folder, so this single line is the only visible change in the source distribution. A reviewer looking at the sdist would see one extra configuration line and nothing else.

The reason this works is that maturin's data setting is a normal, documented feature. It exists so Rust-based Python packages can bundle non-Python files into the wheel. The attacker didn't invent a new attack primitive. They used a legitimate build tool in a way the tool was designed to allow, and they hid the payload one directory up in the sdist where no one looks.

How the attacker published it

This is the part that should keep you up at night more than the payload. The attacker held the GitHub CLI credentials of three developers. LiteLLM believes the attacker took them from the logs of an internal LiteLLM deployment that was exposed to the internet. With an organization owner's token, the attacker bypassed branch protection and pointed the private release workflow at a lookalike repository, Berri-AI/litellm. The attacker then passed the approval gate with a second developer's credential. The official pipeline built, signed, and published the artifacts. Every check that was supposed to catch this ran on attacker-controlled input.

The lesson isn't that LiteLLM was careless. The lesson is that a release pipeline is only as strong as the credentials that gate it, and those credentials were sitting in logs of a service that should never have been reachable from the internet. If your CI logs contain tokens, and your CI is reachable from outside, you have the same problem with a different name on it. The attacker didn't need to break into LiteLLM's source. They needed to break into LiteLLM's process, and the process was the build pipeline itself.

Who was affected

Versions 1.106.2, 1.107.0rc1, and 1.107.0rc2 are clean. The companion packages that 1.107.0 pins - litellm-proxy-extras==0.4.110 and litellm-enterprise==0.1.78 - are also clean. The Docker images v1.107.0, latest, main-latest, and main-stable on Docker Hub and GHCR carried the malicious code between 11:46 and 14:20 UTC on October 21. If you pulled any of those tags in that window, treat the host as compromised.

PyPI package registry

What to check right now

If you installed litellm 1.107.0, or pulled one of the affected Docker tags, do these in order:

  1. Uninstall and downgrade: pip uninstall litellm, then pip install litellm==1.106.2.
  2. Look for the three files in site-packages: cook.pth, cook.py, sitecustomize.py. Delete them if present.
  3. Rotate every credential that was reachable from that host: SSH keys, cloud provider keys, CI/CD secrets, AI tool tokens, browser passwords, and LLM provider keys. Assume the stealer exfiltrated all of them.
  4. Check whether any Python process on the host started between the install and the uninstall. If yes, the daemon had an hour to run.
  5. Audit your logs for outbound connections to www.litellm[.]dev and for hourly GitHub file reads.

Why this matters beyond LiteLLM

The attack didn't exploit a code vulnerability. It exploited the trust we place in the release pipeline. The .pth file trick isn't new, but running it in every Python process on the host - including ones that never touch the package - is a deliberate choice that maximizes blast radius. The hourly daemon makes it persistent. The credential theft covers the keys that actually matter in 2026: cloud access, CI/CD, and LLM provider keys.

Supply-chain attacks hit other ecosystems the same week

For teams running AI tooling in production, the lesson is uncomfortable. The packages you depend on for LLM routing are also the packages with the widest access to your secrets. Pin versions. Verify hashes. Mirror your dependencies internally. Treat a new release of a high-privilege package the way you'd treat a new binary on your production host. Add a lockfile review to your release checklist, and require that any change to a package with credential access goes through a second pair of eyes before it ships. The cost of that review is minutes. The cost of skipping it is a full credential rotation across every host that touched the package.

Have you audited your Python dependencies this quarter, or are you still trusting pip to be right?

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.