← Blog
LiteLLMSupply Chain AttackPyPISecrets Rotation

LiteLLM 1.107.0 PyPI Compromise: How to Check, Clean and Rotate

LiteLLM 1.107.0 (PyPI, Docker) shipped a stealer that runs on every Python start. Who is exposed, how to check without re-triggering it, what to rotate.

Zero Hunt Research··10 min read

Published by Zero Hunt, an autonomous AI red team on an on-premise appliance running private AI: automated penetration testing for networks and infrastructure, black-box or gray-box, with a human approving every step that matters.

For about two hours on 21 October 2026, pip install litellm and the LiteLLM Docker latest tag delivered a credential stealer and an hourly remote-code loader. The malicious 1.107.0 was built by LiteLLM's own release pipeline, signed, and shipped with valid provenance, but its source was a lookalike repository, Berri-AI/litellm, one hyphen away from the real BerriAI/litellm. Wiz counts about 49,000 downloads of the bad version and saw the attacker testing stolen AWS credentials four and a half hours later. This page is the runbook: who is exposed (including through other frameworks), how to check without setting the payload off again, the cleanup order the vendor gives, and what to rotate first.

Developing story — first published 22:27 CEST (20:27 UTC), October 22, 2026. Updated as LiteLLM and the researchers publish more.

At a glance

Package / affected versions litellm==1.107.0 on PyPI (7 wheels) · Docker Hub and GHCR v1.107.0, latest, main-latest, main-stable (also litellm-database, litellm-non_root)
Exposure window PyPI 2026-10-21 11:39–13:23 UTC · Docker tags 11:46–14:20 UTC
Safe version 1.106.2 (latest on PyPI as of 2026-10-22; no 1.107.1 has been published)
Payload cook.pth, cook.py, sitecustomize.py in site-packages: stealer on every Python start, hourly loader holding /tmp/remote_code_loader.lock
Exploited in the wild Yes: stolen AWS role credentials validated 16:20–17:01 UTC on 21 Oct (Wiz)
Downloads About 49,000 of 1.107.0 (Wiz, from ClickPy data)
Official advisory LiteLLM incident report

What happened: a real pipeline, a fake source

LiteLLM is the Python library and proxy that many teams put between their code and OpenAI, Anthropic, Bedrock and other model APIs; Socket puts it at about 23 million downloads a week. That position is what made it worth attacking: a LiteLLM host holds the provider keys of everything behind it.

According to LiteLLM's incident report, the release pipeline had been hardened after a compromise in March 2026 (versions 1.82.7 and 1.82.8): private release repository, PyPI Trusted Publishing, immutable Docker tags, signed images. It did not help, because the attacker did not need to bypass the pipeline. They ran it.

  • The attacker held GitHub CLI OAuth tokens for three LiteLLM engineers. LiteLLM says they came from logs of an internal deployment exposed to the internet, where an unexpected user account had appeared; the logged response bodies could contain the tokens (the report says an AI coding assistant can include a user's gh token in a response body). The initial entry point, possibly a recently patched vulnerability, is not confirmed.
  • At 10:08 UTC an organization owner's token overrode branch protection on the private release workflow. At 10:46 a run started from Berri-AI/litellm, where the attacker had stacked ten commits on top of the clean tag; a second engineer's credential passed the production approval gate at 11:11.
  • At 11:39 the seven wheels reached PyPI; at 11:46 the images were pushed. At 11:53 a third credential deleted the workflow run records.

The v1.107.0 tag in GitHub is clean, which is why a code review of the tag would miss everything. StepSecurity found the change in the build configuration (data = "cook" under [tool.maturin] in pyproject.toml), and the SLSA provenance on the images names the hyphenated repository as the source. The provenance was valid. It was simply describing the wrong repository, the same lesson as the Shai-Hulud wave that shipped signed packages in May.

The question to ask of a signed release is not "is the signature valid?" but "does the provenance name the repository I expected?" Here, it did not, and nobody was checking.

What the payload does

From the analyses by Datadog Security Labs, StepSecurity and Aikido:

  • cook.pth contains import cook. Python executes .pth files in site-packages at every interpreter start, so the stealer runs for any Python process in that environment, including pip itself, even if no code ever imports LiteLLM.
  • cook.py collects cloud credentials (AWS, GCP, Azure, including instance metadata and role credentials), Kubernetes, Docker, Vault and Terraform secrets, SSH, GPG and TLS keys, Git, npm and PyPI tokens, .env files, shell and database history, the environment of every readable process, browser passwords and cookies, the configs of more than 20 AI coding tools, and, when DATABASE_URL is set, the LiteLLM proxy tables holding upstream provider keys. It encrypts the archive (AES-256, key wrapped with RSA-4096) and POSTs it to http://www.litellm.dev/healthcheck. It writes /tmp/.pc_once_72129118c59f5636c0bb only after a successful upload, so a blocked upload is retried on every start.
  • sitecustomize.py forks a detached daemon on Linux and macOS that, every hour, reads a hostname from a GitHub file (letters9813/pythonclient-test/main/domain.txt) and runs whatever http://<host>/codepy returns. The file held only test values while it was up, so no second stage was observed, and the account has been deleted. On Windows the loader fails, but the stealer still runs.

Wiz saw what happened next: between 16:20 and 17:01 UTC, AWS instance-role credentials taken from CI runners and Kubernetes nodes were tested from 168.100.8[.]57 and 165.227.201[.]8, with sts:GetCallerIdentity followed by bedrock:ListFoundationModels, the opening moves of LLMjacking, and in a few environments by IAM and CloudTrail discovery calls.

Who is exposed

You are exposed if anything in your estate installed or ran 1.107.0 in the window, and that is wider than "we use LiteLLM":

Path Exposed if
Direct install pip install litellm / uv add litellm without a pin, between 11:39 and 13:23 UTC
LiteLLM proxy in Docker / Kubernetes Pulled latest, main-latest, main-stable or v1.107.0 between 11:46 and 14:20 UTC (a restart with imagePullPolicy: Always counts)
Frameworks with an open-ended LiteLLM range Fresh install or lock refresh in the window. Current PyPI metadata: DSPy requires litellm>=1.65.8 (core dependency); CrewAI [litellm], smolagents [litellm] and Google ADK [extensions]/[all] extras are open-ended too
Frameworks that pin an exact version Not exposed through that path: aider-chat pins litellm==1.81.10, OpenHands 1.84.1; mem0ai caps <1.98.0
CI runners, notebooks, dev laptops Any job or kernel that resolved dependencies in the window; ephemeral runners still leaked whatever secrets they held
Internal mirrors and caches Artifactory, Nexus, devpi or a pip cache that kept the 1.107.0 wheel can serve it again after PyPI removed it

LiteLLM says its enterprise customers on the private registry were not affected, and that all earlier versions are safe.

Remediation

1. Am I affected? Check without starting Python

On a host that may be infected, every python or pip command runs the stealer again. Check from the shell first:

# payload files and the 1.107.0 metadata in any environment
find / \( -name cook.pth -o -name cook.py -o -name 'litellm-1.107.0.dist-info' \) -path '*-packages*' 2>/dev/null
find / -name sitecustomize.py -path '*-packages*' -exec sha256sum {} + 2>/dev/null | grep 34bef04fe88ac9d0
# markers: stealer has uploaded / loader is (or was) running
ls -la /tmp/.pc_once_72129118c59f5636c0bb /tmp/remote_code_loader.lock 2>/dev/null
lsof /tmp/remote_code_loader.lock 2>/dev/null

Some distributions ship a legitimate sitecustomize.py outside site-packages; compare against the hash above rather than deleting on sight. Finding the files or markers confirms exposure; not finding them proves nothing, because containers and tmp cleanup erase markers.

Then the inventory, which needs no infected interpreter:

# requirements files in your repositories
grep -rnE 'litellm(\[[^]]*\])?==1\.107\.0' --include='requirements*.txt' .
# uv.lock / poetry.lock / Pipfile.lock keep the version on a following line
grep -rn -A3 --include='uv.lock' --include='poetry.lock' --include='Pipfile.lock' -E '"litellm"' . | grep '1\.107\.0'
# images on a host and running pods
docker images --digests | grep -i litellm
kubectl get pods -A -o jsonpath='{range .items[*]}{range .status.containerStatuses[*]}{.imageID}{"\n"}{end}{end}' | grep -i 1bbcb1443bc962e45f

The compromised image index is sha256:1bbcb1443bc962e45f5aad53fe3d2c30600e0379715fb6c664cdd3ac126c339e; the seven wheel hashes are in the Datadog and Aikido reports. If you keep an SBOM or a dependency inventory, query it for [email protected].

Network evidence settles the question: any DNS lookup of litellm.dev, any POST to /healthcheck with an X-Token header, any request to the GitHub dead-drop path above or to a /codepy path. A /codepy request means attacker code ran.

2. Clean up, in the vendor's order

  1. Kill the process holding /tmp/remote_code_loader.lock.
  2. From the shell, delete cook.pth, cook.py, sitecustomize.py and their __pycache__/cook*.pyc and sitecustomize*.pyc from every affected site-packages. Uninstalling litellm does not remove them.
  3. Do not pip install the fix into that environment: rebuild the virtualenv or image from scratch with litellm==1.106.2 (or the v1.106.2 image pinned by digest).
  4. Purge cached 1.107.0 wheels and images from internal mirrors, registries and pip caches.
  5. Block litellm.dev at DNS and egress.

3. Rotate, from a clean machine, in this order

If 1.107.0 ever ran on a host with outbound internet access, LiteLLM's guidance is to assume the upload succeeded.

  1. Cloud roles and keys reachable from the host: instance and task roles, CI deployment roles, static access keys. These were the first thing used.
  2. LiteLLM proxy secrets: every provider key in the proxy database and model configs, the master key, the salt key, virtual keys, and the DATABASE_URL password.
  3. LLM provider and AI tool tokens (OpenAI, Anthropic, Bedrock, Gemini, Copilot, Cursor and similar), which are directly billable.
  4. Source and registry tokens: GitHub/GitLab (including gh CLI OAuth tokens), npm, PyPI, container registries. A stolen publish token is how the next package gets poisoned.
  5. Kubernetes, Vault, Terraform and CI secrets, SSH and GPG keys.
  6. Developer machines: browser sessions and saved passwords, since the stealer read them.

Then hunt for use: in CloudTrail, GetCallerIdentity and ListFoundationModels from your roles after 11:39 UTC on 21 October, any call from 168.100.8.57 or 165.227.201.8, and role credentials used from outside your own networks. A credential is only "rotated" once the old one fails.

4. Prevent the next one

  • Pin with hashes. pip install --require-hashes -r requirements.txt, or a committed uv.lock / poetry.lock. A new version should arrive through a reviewed lock update, not a fresh pip install.
  • Add a cooldown. Most poisoned releases are caught within hours; resolving only versions older than a few days (for example uv --exclude-newer) would have skipped this one entirely.
  • Check what the provenance names. Signatures and attestations were valid here; the source repository was wrong. Verify the repository, not just the signature.
  • Mirror and allowlist. Pull packages through an internal mirror that can block a version the moment it is flagged.
  • Restrict CI egress to the registries and endpoints your jobs need: the stealer had to reach litellm.dev to upload anything.
  • Treat developer CLI tokens as production secrets. Short-lived, scoped, never in logs; do not log response bodies of AI tools.
  • For your own packages: Trusted Publishing instead of long-lived tokens, protected release tags, required reviewers on the release environment, and workflow changes that an owner token alone cannot push. The IronWorm npm case shows the same pipeline abuse.

The pattern behind it

This is not an isolated event. LiteLLM itself was hit in March; on 23 September, according to Safedep, the MemoryOS package on PyPI and a MemTensor npm plugin shipped a worm that stole publish tokens from the projects' own release jobs and pushed loader workflows into every repository the stolen credentials could reach. In each case the registry served exactly what the project's legitimate pipeline uploaded. The control point has moved from "is this package from the right maintainer?" to "did the right repository, at the right commit, produce this artifact?", and very few consumers check that. For the rest of this October cluster, see our Black October verified incident tracker; for the earlier LiteLLM vulnerability, our analysis of CVE-2026-42271.

What is not known yet

  • How the attacker first got into LiteLLM's internal deployment (LiteLLM suspects a recently patched vulnerability).
  • Whether the GitHub dead drop ever served a real second stage to anyone before the account was deleted.
  • How many of the ~49,000 downloads were real installs rather than mirrors and scanners, and how many uploads succeeded.

After the cleanup: proving nothing is still reachable

Rotating keys answers "what did they take?". The harder question for the security team is "what could someone holding those credentials still reach, and did the rotation actually close it?". That is a testing question, and it is where an autonomous AI red team running on-premise helps: Zero Hunt's Credential and Pivot agents work from the access an attacker would have, show which systems the exposed accounts and hosts can still reach, and re-run the same tests after rotation, with a human approving every step that matters. It runs on its own private AI models inside your perimeter, so no provider API key sits on the appliance for the next stealer to take, and your findings and credentials do not leave your network. If you are choosing a platform for this, our buyer's guide to on-premise pentest platforms lists the questions to ask.

Is this exploitable in your environment?

Zero Hunt answers that on your own network: an autonomous AI red team on an on-premise appliance, running on private AI, black-box or gray-box, with a human approving every step that matters. Proof of what is exploitable, the fix, and signed evidence — no data leaves your perimeter.