LiteLLM compromise: when your logs hold the keys

$ cat blog/litellm-supply-chain-tokens-in-logs.md
✓ incident
✓ supply chain
✓ CI/CD
✓ secrets
6 min read
$ 

On October 21, 2026, The Hacker News reported that LiteLLM, the popular LLM gateway library and proxy, had been compromised in a supply chain attack: a malicious version 1.107.0 was published to PyPI and to Docker images. A day later, the maintainers said they suspected the attacker got in using GitHub tokens that had ended up in LiteLLM's own logs. See the first report and the follow-up.

Two lessons stand out for engineering teams: logs that capture full request and response bodies can quietly collect credentials, and a release pipeline is only as strong as the single most powerful token that can change it.

What happened

According to the reports, the malicious LiteLLM 1.107.0 was published on PyPI and on Docker Hub and GHCR, under the tags v1.107.0, latest, main-latest and main-stable.

The package included a file named cook.pth containing import cook. Python processes .pth files at startup, so the payload ran whenever the Python interpreter started, even before LiteLLM was imported. The stealer collected cloud tokens, developer account data, private keys, credential JSON files for AI developer tools, Chromium and Firefox data, environment variables, and LiteLLM proxy saved credentials and model configuration. It packed them into a GZIP TAR and POSTed it to litellm[.]dev/healthcheck. A backdoor daemon, per StepSecurity, "checks a GitHub dead drop every hour and runs whatever Python the attacker serves", fetching <domain>/codepy.

How the attacker got release access, as reported:

  • They used the GitHub accounts of three engineers, via what appear to be stolen GitHub CLI OAuth tokens. One account triggered the release run, and another approved it.
  • LiteLLM said Claude Code can embed a user's GitHub CLI OAuth token in a response body, and when response bodies are logged, those tokens "may appear in LiteLLM logs".
  • The maintainers found an "unexpected, innocuously named user account" in an internal LiteLLM deployment that was exposed to the public internet, and suspect the attacker pulled tokens from its logs. How that deployment became public is unclear; they suspect a recently patched vulnerability.
  • With an organization owner token, the attacker disabled branch protections and force-pushed, then ran the workflow against a lookalike repository, "Berri-AI/litellm", so the artifacts were signed and published.
  • On October 19, release permissions had been temporarily expanded for two engineers.
  • About 4.5 hours after the exfiltration, the attacker validated AWS instance role credentials taken from compromised CI runners and Kubernetes nodes. Wiz called this "consistent with LLMjacking".

LiteLLM's response: tokens revoked, users removed from the organization, trusted publishing links removed, releases moved to a private repository, Docker tags made immutable, and images signed.

Why it worked

Several ordinary decisions lined up. None of them is unusual on its own, which is why this is worth reading closely.

Logs became a credential store

Debug logging of full HTTP request and response bodies is convenient: when something breaks, everything is there. But a gateway sits between many clients and many providers, and bodies can carry tokens, keys and personal data. Once a token is in a log, it lives as long as the log does, in every system the log is shipped to, readable by anyone with log access. A deployment exposed to the internet turns that into an open credential store.

Here is an illustrative example, not LiteLLM's code:

# Risky: the whole upstream body goes to the log
logger.info("upstream response: %s", resp.text)
# Safer: log what you need to debug, and redact anything secret-shaped
SAFE_FIELDS = ("id", "model", "status", "usage")

def summarize(body: dict) -> dict:
    return {k: body.get(k) for k in SAFE_FIELDS if k in body}

logger.info("upstream response status=%s body=%s",
            resp.status_code, redact(summarize(resp.json())))

Log an allowlist of fields, not a denylist. If you truly need full bodies for a short investigation, put them behind a flag that expires, store them separately with tight access, and run a redactor over them.

A release path one token could open

Branch protection and required reviews are meant to stop one person, or one stolen credential, from shipping code alone. Here, per the reports, an org owner token was used to disable those protections and force-push, and two accounts covered the trigger-and-approve steps. Protections that the same token can turn off are a speed bump, not a control. Release permissions that were temporarily expanded also widen the set of accounts worth stealing.

Pin what you install

As JFrog's Guy Korolevski put it, "a clean source repository tells you little about the artifact you actually install." Floating tags like latest and unpinned version ranges pick up whatever is published next. Pinning by hash or digest means a new, malicious upload doesn't reach you until you deliberately choose it. Our post on slopsquatting covers another way untrusted packages get in.

attack, as reported where OpenRouting checks 1 GitHub CLI tokens logged in bodies✓ Secrets & data exposure agent 2 Internet-exposed deployment's logs readruntime, not a repo scan 3 Owner token disables branch protectionorg settings, not code 4 Release workflow publishes 1.107.0✓ CI/CD pipeline agent 5 Floating tags pull the bad release✓ Supply chain agent 6 cook.pth steals credentialsno SCA scanning yet
The attack as reported, and where an OpenRouting check applies. Illustrative.

What to do now

If you use LiteLLM:

  1. Remove 1.107.0 from every environment, including Docker images pulled from the affected tags.
  2. Isolate hosts where it ran, and hunt for cook.pth, cook.py and sitecustomize.py in Python environments.
  3. Block the attacker's domain at your egress.
  4. Follow the guidance quoted in the report: "If 1.107.0 ran anywhere in your environment, rotate every reachable credential and rebuild." That includes cloud and CI credentials, not just LLM provider keys.
  5. Pin 1.106.2 by digest until you've reviewed a newer, signed release.

For your own systems:

  1. Search for logging of full request or response bodies, especially in proxies, gateways and webhook handlers, and switch to allowlisted fields plus redaction.
  2. Never expose internal admin or proxy deployments to the internet; put them behind authentication and a private network.
  3. Make release workflows require approvals that the triggering account can't satisfy, and limit who holds organization owner rights.
  4. Pin Python dependencies with hashes, Docker images by digest, and third-party GitHub Actions by commit SHA.
  5. Give CI runners and cluster nodes the narrowest cloud role they need, so stolen instance credentials can do less.

How OpenRouting helps

OpenRouting can't tell you a package is malicious, but it reviews the parts of your own repository that made this kind of attack worse:

  • The secrets & data exposure agent reviews findings where sensitive data leaks through logs, such as full bodies, headers or tokens passed to a logger, and judges whether the leak is real.
  • Semgrep rules flag hardcoded secrets in your files and, for GitHub Actions workflows, risky patterns such as untrusted input in run: steps.
  • The CI/CD pipeline agent reviews workflow findings with their context: who can trigger a release, which secrets a job can see, and what permissions it holds.
  • The supply chain agent reviews findings about how dependencies are declared, such as floating tags, unpinned versions or third-party actions not pinned by SHA.
  • Secret values are redacted before any AI call; triage runs on Claude, via Amazon Bedrock. PR review applies the same checks to the files a pull request changes.

What it won't catch

  • OpenRouting doesn't do dependency or SCA scanning yet. It won't tell you that a version like 1.107.0 is malicious, and it doesn't inspect installed packages or pulled images.
  • It reads workflow files, not your GitHub organization settings, so it can't see that branch protection was disabled or who holds owner rights.
  • It doesn't read your runtime logs, so tokens that have already been logged won't show up in a scan.
  • Findings come from a scan you run, manually or as a PR review.

Sources

Questions

What happened in the LiteLLM supply chain attack?

A malicious LiteLLM 1.107.0 was published to PyPI and to Docker images on Docker Hub and GHCR. It included a cook.pth file that ran a stealer whenever the Python interpreter started, collecting cloud tokens, private keys, environment variables and other credentials.

How did the attacker get release access to LiteLLM?

The maintainers suspect the attacker pulled GitHub CLI OAuth tokens from the logs of an internal LiteLLM deployment exposed to the public internet; how that deployment became public is unclear. With an organization owner token, the attacker disabled branch protections, force-pushed and ran the release workflow.

What should I do if LiteLLM 1.107.0 ran in my environment?

Remove 1.107.0 everywhere, including Docker images pulled from the affected tags, isolate hosts where it ran and hunt for cook.pth, cook.py and sitecustomize.py. Rotate every reachable credential, including cloud and CI credentials, rebuild, and pin 1.106.2 by digest until you've reviewed a newer, signed release.

Can OpenRouting tell me a package version is malicious?

No. OpenRouting doesn't do dependency or SCA scanning yet, so it won't flag a version like 1.107.0. It reviews your own repository for full-body logging, risky workflow patterns and unpinned dependencies.

Keep reading