The LiteLLM 1.107.0 compromise started with a token an agent platform was holding

Check your hosts first. Then check which GitHub token your agents are using to open PRs.

Published . On October 21, a malicious litellm==1.107.0 was published to PyPI, Docker Hub, and GHCR. It installs a credential stealer and an hourly remote-code loader that run every time Python starts. LiteLLM's incident post says the package was live on PyPI from 11:39 to 13:23 UTC, and that the v1.107.0, latest, main-latest, and main-stable Docker tags served the bad build from 11:46 to 14:20 UTC. The clean version to go back to is 1.106.2.

LiteLLM is the gateway a lot of teams put between their coding agents and model providers. BleepingComputer puts it at about 92 million PyPI downloads a month. It is also the project's second compromise this year: version 1.82.8 was poisoned in March through a vulnerability in the Trivy scanner. If you run LiteLLM, work through the next two sections today. Then read how the attackers got in, because the first domino was an AI agent platform holding a GitHub token it should never have had.

What the poisoned release does

The technical report on issue #47826, credited to Upwind's researchers, found three extra files in every 1.107.0 wheel, installed into the root of site-packages:

  • cook.pth, 12 bytes containing import cook. Python runs .pth imports at every interpreter start, so the payload fires on any Python process in that environment, including pip.
  • cook.py, a stealer that collects SSH keys, shell history, cloud and CI credentials (AWS, Azure, gcloud, kube, Docker, gh, npm, PyPI), browser cookies, and the environment of every readable process. If DATABASE_URL is set, it also dumps the LiteLLM credentials and model tables, which is where a proxy keeps your upstream provider keys. The data is encrypted before it's sent, so a packet capture won't tell you what left.
  • sitecustomize.py, which forks a detached daemon that checks a GitHub dead drop every hour and runs whatever code the attacker points it to. The report says the drop has been empty since October 20, but one commit would re-arm it.

The malicious builds came from a commit in a lookalike organization, Berri-AI/litellm, one hyphen off the real BerriAI/litellm. LiteLLM's incident post and the issue report both say the payload isn't in the v1.107.0 source tag on GitHub. Only the built packages were poisoned, which is why reviewing the source would have shown you nothing.

Check whether you were hit, without starting Python

The usual first move, pip show litellm, is a bad idea here. pip is a Python process, so in an infected environment it runs the payload again. Search with the shell instead:

# payload files and the dist-info for the bad version
find / -path '*site-packages*' \( -name cook.pth -o -name cook.py \
  -o -name 'litellm-1.107.0.dist-info' \) 2>/dev/null

# host markers from the report (the first appears only after a successful exfil)
ls -la /tmp/.pc_once_72129118c59f5636c0bb /tmp/remote_code_loader.lock 2>/dev/null

# the compromised Docker Hub digest
docker image ls --digests | grep 1bbcb1443bc9

No hits isn't proof. LiteLLM warns that a clean result doesn't rule out compromise, because containers and tmp cleanup remove markers. Define exposure by what ran, not by when or how it was installed. Any environment that installed 1.107.0 or ran one of the compromised images is affected. That includes an explicit litellm==1.107.0 pin, a wheel served later from a pip cache or internal mirror, and an image pulled that afternoon and still running today. The registry windows above tell you when fresh downloads were possible. They don't clear anything that copied the artifact during that time. LiteLLM says enterprise customers on its private registry were not affected.

If you were exposed

The steps from LiteLLM and the issue report, in order:

  • Delete cook.pth, cook.py, sitecustomize.py, and their .pyc files from every affected site-packages, using the shell.
  • Kill the detached Python process holding /tmp/remote_code_loader.lock.
  • Rebuild the venv or image from scratch with litellm==1.106.2. Reinstalling over the top is the weaker option. Purge 1.107.0 from pip caches and internal mirrors too, so nothing reinstalls it.
  • If the host had outbound internet, assume exfiltration succeeded, and rotate everything it could reach: the instance or task IAM role, every provider key stored in the proxy database, LiteLLM master and virtual keys, the DATABASE_URL password, and any secret in any process's environment.
  • On AWS, look in CloudTrail for GetCallerIdentity calls from the instance role around install time. The stealer uses that call to validate cloud credentials before sending them.

How they got in: an agent platform's GitHub token

LiteLLM published the attack chain on October 23, and says it supersedes earlier status updates. Here's the sequence:

  • May 27: a config file committed to the public LiteLLM-Labs/lite-harness repository contained the master key to a deployment of the LiteLLM Agent Platform. The commit was force-pushed over and the key was flagged for revocation. In LiteLLM's words, “key revocation did not occur.”
  • October 10: the attacker used that master key against the Agent Platform API. A single request, GET /api/v1/managed_agents/agents?limit=100, “returned the configuration of every managed agent,” including the GitHub CLI OAuth token of one of LiteLLM's two GitHub organization owners.
  • Next: with that token, the attacker ran throwaway workflows on LiteLLM repositories that posted CI secrets to a webhook.site endpoint. Two of those secrets, GH_TOKEN and RELEASE_PLEASE_TOKEN, held the personal GitHub CLI tokens of two more maintainers.
  • October 21: branch protection rejected the malicious workflow change, and the owner's token pushed it through anyway by overriding the rules. Release approval had been relaxed earlier that week so that any two engineers could trigger and approve a release, and the attacker now held tokens for three. A third account deleted the workflow run records afterward.

LiteLLM describes the Agent Platform as “an experimental project for self-hosted cloud agents to file PRs to GitHub repositories.” To do that it was given a token that, per the post, “was not associated with a service account and was not scoped down.” That's the part that applies to you. Every agent that opens PRs on your behalf, whether it's a self-hosted harness, a cloud coding agent, or a CI bot, needs some GitHub credential. The easy option is your own gh token, and if you're an org owner, that token can disable branch protection.

LiteLLM's first incident update, on October 21, suspected the tokens came from an internal deployment's logs, and said it had seen Claude Code embed a user's GitHub CLI token in a response body that could end up in those logs. The October 23 post says the request logs show the first token “came from a different service,” the Agent Platform API described above. So the logging theory wasn't the way in. We cover it separately below because the underlying risk is still real.

What to change in your own setup

Give agents their own scoped GitHub identity. An agent that files PRs needs write access to a few repositories, not an owner's reach across the org. Use a GitHub App installation token or a fine-grained token limited to the repos and permissions the agent needs, owned by a service account. Never use a maintainer's gh auth token. Then an agent platform's config leaking costs you one revocable credential that can't touch branch protection.

Make secrets write-only in agent configs. The endpoint that listed every agent's configuration also returned the secret inside it. If you run an agent platform or build one, store credentials so the API can use them but never return them. If you're evaluating a hosted one, ask how it stores the GitHub token you give it.

A force-push is not a revocation. Once a key has been in a public commit, assume it was scraped, whatever happened to the commit afterward. The only fix is rotating the key, and in this case that step was missed for nearly five months.

Keep personal tokens out of CI secrets. Two of the three tokens came from repository secrets that held maintainers' own CLI tokens. Anyone who can run a workflow can usually read secrets available to it. Use the workflow's built-in GITHUB_TOKEN or an App token wherever possible. For the tokens that do live on laptops, GitHub CLI's new expiring tokens (gh auth login --expiring, covered in our OpenCode Console breach piece) limit how long a stolen one works.

Pin to artifacts you've vetted, not just to something fixed. Pinning litellm==1.107.0 or the bad image digest would have installed the malware just as reliably as latest, since the version-specific tag was poisoned too. What pinning buys you is control over when a new artifact enters your systems. Combine version-and-hash pins (pip install --require-hashes, or a lock file from uv or Poetry) and digest-pinned images with a rule that new releases wait a day or two before anyone adopts them. A delay of hours would have skipped this release entirely. Our CI security guide covers the rest of the pipeline side.

The logging lesson, even though it wasn't the way in

A coding agent sends everything it reads back to the model: file contents, command output, error messages. If it runs gh auth status --show-token while debugging an auth failure, the token is in the next request. A gateway that stores request and response bodies is storing those secrets next to the prompts. That applies to any agent behind any gateway, not just to Claude Code or LiteLLM.

In LiteLLM, the spend logs don't keep request and response content by default. Someone has to set store_prompts_in_spend_logs: true under general_settings or turn it on under Logs → Settings in the UI, which overrides the config file. Logging callbacks such as Langfuse and OpenTelemetry are separate, and the logging docs show how to keep message bodies out of them:

litellm_settings:
  turn_off_message_logging: True   # spend and metadata still tracked

callback_settings:
  otel:
    message_logging: False

Claude Code's gateway docs list “audit logging: log every model request for compliance” as a reason to run a gateway at all. If you need full bodies, keep them, but protect the log store like a secrets vault.

Verdict

If any environment of yours installed 1.107.0 or ran one of the compromised images, treat it as compromised and rotate, however it got there. If none did, you still have homework. The attackers didn't break LiteLLM's hardened release pipeline. They walked in with an org owner's personal token that an agent platform had been handed for opening PRs, exposed by a key nobody revoked. Look at which GitHub credential each of your agents uses, and whether it could disable your branch protection.

We haven't run the malicious package or tested LiteLLM's Agent Platform. This piece is based on LiteLLM's two incident posts, the public issue report, and the coverage linked below. LiteLLM says it will share process changes next week, and we'll update this page when it does.

Sources