49,000

downloads of the malicious version, per Wiz citing ClickPy

3

stolen developer credentials used to override, approve and erase

4.5 hours

from exfiltration to the first use of stolen AWS credentials, per Wiz

1.106.2

the version LiteLLM says to pin while it investigates

Every release control passed the poisoned LiteLLM release

LiteLLM's release pipeline did exactly what it was built to do on 21 Oct 2026, and that is the problem. Every control that stood between an attacker and the PyPI page checked an identity, and the attacker held the identities.

After its March compromise, LiteLLM hardened its release process the way the supply chain guidance says to. Releases moved to a private repository. Publishing went through Trusted Publishing, so there was no long-lived PyPI token to steal. Docker release tags were made immutable and the images were signed. Publishing was tied to an account locked to one laptop inside the office. On 21 Oct the attacker went through all of it, and the result came out of the far end looking more official than a hand-pushed release ever could: built by the real workflow, approved, signed and published by the trusted identity.

The incident report lists three stolen developer credentials. One overrode branch protection seconds after it rejected the change. A second satisfied the production approval gate. A third deleted the workflow records afterwards. The source the pipeline built from was not LiteLLM's at all. It came from Berri-AI/litellm, a lookalike of the real BerriAI/litellm that differs by one hyphen.

So the lesson is narrower than "supply chain is hard". Signing, provenance and trusted publishing answer the question "did our pipeline produce this?" They do not answer "is this the code we reviewed?" When the people who can run and approve the pipeline are the ones compromised, the first answer is yes and the second is no, and only the second one protects your users.

The second lesson is about where the tokens came from. LiteLLM believes they were lifted from the logs of its own LiteLLM deployment, which was reachable from the internet. An AI gateway sits in the path of every request a coding agent makes. Whatever those requests carry, its logs can keep.

What happened to LiteLLM 1.107.0 on 21 Oct 2026

LiteLLM is the open source Python library and proxy that puts one API in front of many LLM providers. Datadog Security Labs puts it at about 23 million weekly downloads. Organizations run it as a gateway that holds the provider keys for a whole team, which is why its compromise reaches further than most packages.

The LiteLLM incident report says the release pipeline "was compromised and pushed v1.107.0 to the docker registry and PyPi", and that the version "contained compiled malware which steals credentials and loads remote-code execution tools." All seven 1.107.0 wheels on PyPI were malicious, live from 11:39:51 to 13:23 UTC. The Docker tags v1.107.0, latest, main-latest and main-stable on Docker Hub and GHCR pointed at the compromised image index from 11:46:56 UTC. LiteLLM says the GHCR tags were unavailable by 13:51 and the Docker Hub tags were removed or repointed at clean images by 14:20 UTC.

The outside timings differ slightly. Wiz and Datadog put the PyPI quarantine at 13:20 UTC and the Docker removal at about 13:53. We use LiteLLM's own figures, which give the longer window, because the longer window is the safer one to scope against.

The LiteLLM 1.107.0 compromise Critical
  • Package //

    litellm 1.107.0, all seven wheels, PyPI
  • PyPI window //

    11:39:51 to 13:23 UTC, 21 Oct 2026, per LiteLLM
  • Images //

    litellm, litellm-database and litellm-non_root on Docker Hub and GHCR
  • Floating tags //

    v1.107.0, latest, main-latest, main-stable, from 11:46:56 UTC to 14:20 UTC at the latest
  • Built from //

    Berri-AI/litellm, a lookalike repository; the official v1.107.0 tag is clean
  • Pin to //

    1.106.2, per LiteLLM
  • CVE //

    none; this is a malicious release, not a flaw in the code

Wiz reports that the malicious version received approximately 49,000 downloads, citing ClickPy data. That is a PyPI download count. It does not include pulls of the latest image, which is how many teams run the proxy.

LiteLLM's enterprise customers who pull from its private registry were not affected. The report also says that registry is distributing an embargoed release for a separate vulnerability whose disclosure is scheduled for 27 Oct. Nothing public ties that vulnerability to this incident.

This is the second compromise of LiteLLM this year. In March, malicious versions 1.82.7 and 1.82.8 reached PyPI during the TeamPCP campaign we covered in our breakdown of that cascade. Datadog notes that no connection between the two incidents has been established, and we do not assume one.

How the attacker got through branch protection, approval and signing

The incident report's timeline is unusually precise, and it reads like a list of controls doing their jobs.

LiteLLM's normal release cuts a release candidate branch and tag, qualifies it through integration tests, creates artifacts in a private repository from the qualified commit, tests those artifacts again, and publishes them through Trusted Publishing without rebuilding. On the Monday before the attack, LiteLLM had temporarily widened release permissions so two engineers could publish daily bugfix backports for customers.

At 10:08:29 UTC GitHub branch protection rejected an unauthorized change to the private release workflow, because it had not passed the required pull request and status checks. At 10:08:34 a privileged developer credential overrode that protection and applied the change. LiteLLM says one of the stolen tokens belonged to one of its two GitHub organization owners, and that the change let the releaser point at a different source repository. The modified workflow then ran an official staging build from the lookalike repository, which completed successfully at 11:08:09. At 11:11:18 a second developer credential satisfied the production approval gate. The wheels reached PyPI at 11:39. At 11:53:49 a third credential deleted the production and staging workflow records, which LiteLLM says limited the logs available for its investigation.

Figure 01

Four gates, four passes

  1. 01

    Tokens //

    GitHub CLI tokens taken from an exposed gateway's logs

  2. 02

    Override //

    An owner token lifts branch protection

  3. 03

    Redirect //

    The release builds from a lookalike repository

  4. 04

    Approve //

    A second stolen credential clears the gate

  5. 05

    Publish //

    Signed wheels and images ship as official

  6. 06

    Erase //

    A third credential deletes the run records

Result //

A trusted release built from untrusted source.

No gate failed. Each one checked a person or a workflow, and the attacker held both.

The public issue opened when the release was spotted shows how little had to change. The sdist's build configuration carried one extra line that was not in the tagged source, and that line told the build tool to include the payload files in every wheel. The files carried the same timestamps and metadata as the legitimate ones, because the real build produced them. Nothing about the wheels looked hand-made.

The staging build deserves a second look. A staging run that builds whatever the workflow points at, and passes, is evidence that the pipeline works, not evidence that the source is the reviewed source.

Why an AI gateway's logs held the keys to its own release

The incident report's account of the entry point is the part every team running an AI gateway should read twice.

LiteLLM says it has "observed specific situations in which Claude Code will embed a user's GitHub CLI OAuth token into a response body", and that where response bodies are logged, "these tokens may appear in LiteLLM logs." It found "an unexpected, innocuously named user account" in its internal LiteLLM deployment, which was exposed to the public internet. Its working theory is that the attacker got into that deployment, read the GitHub CLI tokens out of its logs, and used them against the release workflow.

LiteLLM hedges that and so do we. It has not confirmed how the attacker reached the internal deployment, and says only that it currently suspects "a recently patched vulnerability". It does not say which one. LiteLLM's proxy has had other vulnerabilities this year, including the pre-authentication SQL injection we covered in our analysis of CVE-2026-42208, but nothing public connects that flaw to this incident.

The structural point stands without the details. A gateway in front of coding agents sees the prompts, the tool output and the responses of every developer who uses it. Coding agents run shell commands, read files and call CLIs that hold tokens. When the gateway logs full request and response bodies for spend tracking or debugging, the log store becomes a quiet copy of whatever credentials passed through. In this case the deployment holding it faced the internet.

When we test an AI deployment we treat the LLM gateway as a credential vault, because it holds every upstream provider key for the organization. This incident adds a second reason. Its logs can hold the developers' own tokens, and those tokens can reach the build system.

What the payload steals, and why uninstalling does not remove it

Each malicious wheel drops three files into the root of site-packages: cook.pth, cook.py and sitecustomize.py. Both entry points are standard Python behaviour. The Python documentation says lines in a .pth file that start with import "are executed", and that the site module also imports a module named sitecustomize; both happen automatically at interpreter startup unless Python runs with -S.

Datadog spells out the consequence: "Any Python process in the environment runs the malware, even if LiteLLM is never imported." Deleting the litellm package directory does not remove the files, and running the environment's own python or pip runs the malware again. On a CI runner or a shared build image, that means every later job that starts Python in that environment.

Wiz names the stealer privcollect and says it covers 16 modules. Among them:

  • browser credentials and cookies from Chrome, Firefox, Edge and Safari
  • AWS credentials from the ~/.aws directory, environment variables and EC2 and ECS instance metadata, plus GCP service account keys and Azure CLI tokens
  • API keys for AI providers
  • SSH and GPG keys and shell history
  • on a LiteLLM proxy with a database, the stored upstream provider credentials, read straight from the proxy's credentials table

The haul is encrypted and posted to www.litellm[.]dev, a lookalike domain. The sitecustomize.py loader polls a file in a GitHub repository for a domain to fetch a second stage from. Wiz says that file never held more than test values and GitHub took the repository down, so no second stage was seen. The loader is still a backdoor waiting for an address.

What happened next is the strongest argument for rotating quickly. Wiz observed abuse of stolen credentials starting approximately 4.5 hours after exfiltration. Between 16:20 and 17:01 UTC the attacker validated AWS instance role credentials taken from compromised CI runners and Kubernetes nodes, calling sts:GetCallerIdentity and then Bedrock model listing calls, which Wiz describes as consistent with LLMjacking. In a small number of environments the attacker also enumerated IAM and ran general discovery, including DescribeTrails.

Figure 02

LiteLLM 1.107.0 payload files written to site-packages

Sigma

title: LiteLLM 1.107.0 payload files in a Python environment
status: experimental
description: Startup hooks dropped by the malicious litellm 1.107.0 wheels and images.
logsource:
  category: file_event
  product: linux
detection:
  hook_files:
    TargetFilename|contains: 'site-packages/'
    TargetFilename|endswith:
      - '/cook.pth'
      - '/cook.py'
  run_once_marker:
    TargetFilename|startswith: '/tmp/.pc_once_'
  loader_lock:
    TargetFilename: '/tmp/remote_code_loader.lock'
  condition: hook_files or run_once_marker or loader_lock
falsepositives:
  - Unlikely; no legitimate package ships cook.pth
level: high
A starting point to tune against your own telemetry, not a production rule. sitecustomize.py is left out because legitimate tooling writes it; check it by hand. Adapt it for macOS and Windows endpoints that ran Python.

Figure 03

Stolen AWS role credentials checked from outside

Sigma

title: AWS role credentials validated from LiteLLM 1.107.0 attacker addresses
status: experimental
description: Credential checks and Bedrock listing reported by Wiz after the LiteLLM 1.107.0 exfiltration.
logsource:
  product: aws
  service: cloudtrail
detection:
  attacker_ip:
    sourceIPAddress:
      - '168.100.8.57'
      - '165.227.201.8'
  bedrock_listing:
    eventSource: 'bedrock.amazonaws.com'
    eventName: 'ListFoundationModels'
    userIdentity.type: 'AssumedRole'
  condition: attacker_ip or bedrock_listing
falsepositives:
  - Roles that use Bedrock legitimately; filter them by role and by your own address ranges
level: medium
A starting point to tune against your own telemetry, not a production rule. The addresses are the two Wiz published; the attacker can change them.

Who is affected by LiteLLM 1.107.0, how to check, and what to rotate

You are affected if you installed an unpinned litellm from PyPI between 11:39 and 13:23 UTC on 21 Oct, or ran the v1.107.0, latest, main-latest or main-stable image between 11:46 and 14:20 UTC. The public issue puts it more bluntly for Docker users: any pull of those floating tags from about 11:48 UTC got the stealer.

The places to look are wider than the servers that run the proxy:

  • CI runners and build images that install litellm without a pinned version, including jobs that pull it in as a dependency of something else
  • developer laptops, where the stealer's browser and SSH modules matter most
  • Kubernetes deployments and compose files that reference latest or main-stable
  • notebooks and scheduled jobs that build a fresh environment on each run

On each candidate, pip show litellm tells you the version, and the LiteLLM image index digest beginning sha256:1bbcb1443bc9 identifies the compromised container. LiteLLM says to search for litellm-1.107.0.dist-info, cook.pth and sitecustomize.py, and warns that finding one confirms exposure while finding none proves nothing. Wiz adds the /tmp/.pc_once_ marker file and the PRIVCOLLECT_ACTIVE=1 environment variable.

Order matters when you clean up. LiteLLM's guidance is not to start with pip install, because any Python start runs the payload again. Delete the payload files and their cached .pyc copies from every affected site-packages from the shell, kill the detached Python process holding /tmp/remote_code_loader.lock, then reinstall pinned to litellm 1.106.2. Rebuilding the environment or image from scratch is better.

Then rotate, on the assumption that exfiltration succeeded on any host with outbound internet access. LiteLLM's list:

  • the instance or task IAM role the host ran with
  • every provider key stored in the LiteLLM proxy database, plus the master and virtual keys
  • the DATABASE_URL password
  • any secret in the environment of any process on the host

From the payload's own reach, add cloud CLI credentials, SSH and GPG keys, Git, npm and PyPI tokens on developer machines, and browser sessions to anything sensitive. Then search CloudTrail for GetCallerIdentity from the affected roles around install time, and for those credentials used from addresses outside your own infrastructure. If your trails do not log the regions and accounts the role could reach, that hunt has a hole in it. Our free atarus-cloud scanner, on our open source page, checks AWS IAM admin access and CloudTrail logging gaps, the two things this hunt depends on.

What this means for release pipelines, including yours

LiteLLM's March changes were the right changes, and they still mattered here. Immutable tags meant the attacker could not quietly overwrite an old version. Trusted Publishing meant there was no standing PyPI token to steal; PyPI's own documentation says the tokens it mints are valid for only 15 minutes. What none of them could stop was a pipeline run by people who were not who their credentials said.

Three questions follow for any team that ships software.

Can one person switch off the protection that stopped them? The override came five seconds after the rejection. Organization owner rights over branch protection, and over the workflow files that define a release, belong behind a second person and an alert, not one token. A bypass of branch protection on a release workflow should page someone.

Does the release check where its source came from, against something the attacker cannot edit? The workflow could be pointed at a different repository. A release job that builds only from a fixed repository and a tag that already passed review, and refuses anything else, would have stopped this build at checkout. Build provenance helps only if a consumer or a gate compares the recorded source against what was expected.

Do your approval gates check a person, or a token? LiteLLM's production gate was satisfied by a credential. An approval that a stolen GitHub CLI token can grant is a speed bump. Approvals for production releases should need a fresh interactive sign-in with a phishing resistant factor, and two approvals from different people should not be grantable by credentials stored on the same kind of machine.

For consumers the lesson is older and still ignored. Floating tags such as latest and main-stable, and unpinned installs in CI, turn a two hour window at the publisher into an exposure on every system that happened to build in it. Pinning to digests and hashes, and waiting a few days before taking a new release into production, would have kept those environments out of this one.

On a sanctioned engagement, this is the path we walk. We look for developer tokens in places that are not secret stores: agent and gateway logs, CI caches, shared build images. Then we test whether one of those tokens can change what the release pipeline builds and still produce a signed, approved artifact. The question is not whether the pipeline has controls. It is whether any single stolen credential can switch them off.

What to do today

  1. Find every environment that installed litellm 1.107.0 or pulled latest, main-latest or main-stable on 21 Oct. Include CI runners, build images, laptops and Kubernetes. Treat each as compromised.

  2. Clean without starting Python, then rebuild pinned to 1.106.2. Delete cook.pth, cook.py, sitecustomize.py and their .pyc files, stop the process holding /tmp/remote_code_loader.lock, and rebuild from scratch where you can.

  3. Rotate everything those hosts could reach. Instance and task roles, every provider key in the LiteLLM database, master and virtual keys, the database password, and developer tokens. Then hunt CloudTrail with the Sigma rule above.

  4. Stop logging full request and response bodies on your AI gateway, or treat those logs as secrets. Take the gateway off the public internet unless it has to be there, and review its user accounts for any you did not create.

  5. Pin releases by digest or hash, and stop deploying floating tags. Hold new versions of high-privilege packages for a few days before production.

  6. In your own release pipeline, alert on any branch protection bypass and fix the source. Build only from the expected repository and a reviewed tag, and make production approvals need a fresh phishing resistant sign-in.