# LiteLLM 1.107.0 was malware: your Azure AI gateway audit

LiteLLM 1.107.0 shipped on 21 October 2026 with a credential stealer and remote-code loader that ran on every Python start, harvesting Azure, Kubernetes, SSH and AI-tool credentials plus the proxy database of provider keys. A two-hour audit for Azure teams running LiteLLM in front of Azure OpenAI: find the digests, hunt the indicators, rotate in the right order, and decide whether the gateway belongs in API Management.

- Published: 2026-10-22 · Category: Security & Compliance · Tags: LiteLLM, Supply Chain Security, Azure OpenAI, Azure API Management, AI Gateway, Azure Container Apps, AKS, NIS2, Cyber Resilience Act, Managed Identity
- Author: Technspire AB, Stockholm (https://technspire.com)
- Canonical: https://technspire.com/en/blog/litellm-1-107-0-was-malware-your-azure-ai-gateway-audit

At 11:39 UTC on 21 October 2026, seven wheels for litellm 1.107.0 landed on PyPI. Every one of them carried a credential stealer and a remote-code loader. The package stayed installable for 104 minutes. The matching container images, including the floating latest tag on Docker Hub and GHCR, stayed pullable for about two and a half hours. LiteLLM is the open-source proxy a large share of Azure teams put in front of Azure OpenAI and Microsoft Foundry to get one OpenAI-compatible endpoint, per-team keys and spend tracking. If that describes your stack, the question this morning is not whether to patch. It is whether a Python process started on any host that pulled the release, and if so, which of your Azure credentials left the building.

## What actually shipped

The source repository was clean. The attacker modified the release workflow so the build pulled from a lookalike repository, Berri-AI/litellm instead of BerriAI/litellm, and the wheels that came out contained three extra files in site-packages: cook.pth, cook.py and sitecustomize.py. Python processes .pth files at interpreter start, so the payload ran every time any Python process started on the host, whether or not it ever imported litellm. Datadog's write-up spells out the consequence that matters for your clean-up: deleting the litellm package directory does not remove these files.

Stage one, in cook.py, is a 61 KB stealer. Socket's analysis lists what it collects: AWS, Azure and Google Cloud credentials, including instance metadata tokens; Kubernetes, Docker, Vault and Terraform secrets; SSH, GPG and TLS private keys; Git, npm and PyPI tokens; .env files and shell history; Chromium and Firefox password and cookie databases; and the saved sessions of AI coding tools, naming the Claude credentials file, the Codex auth file, Gemini OAuth credentials and Cursor's MCP configuration. It also queries the LiteLLM proxy database for stored upstream provider keys. Everything is packed, encrypted with AES-256 and an RSA-4096 public key, and posted to www.litellm[.]dev/healthcheck, a typosquat of the project's own domain.

Stage two, in sitecustomize.py, forks a detached daemon on Linux and macOS, holds a lock at /tmp/remote_code_loader.lock, and once an hour reads a domain from a text file in a GitHub repository, then downloads Python from that domain's /codepy path and runs it. The pointer file was emptied at 13:01 UTC, so the second stage never fired for most victims. The first stage needed no second stage to do its damage.

| Artefact | Exposure window (UTC, 21 Oct) | Status now |
| --- | --- | --- |
| PyPI litellm==1.107.0 (7 wheels) | 11:39 to 13:23 | Removed |
| Docker Hub v1.107.0, latest, main-latest, main-stable | 11:46 to 14:20 | Removed or repointed to clean digests |
| ghcr.io/berriai/litellm, litellm-database, litellm-non_root (same tags) | 11:46 to 13:51 | Made unavailable |
| Enterprise private registry release | Not affected | Separate embargoed build |

Detection was fast. AISLE flagged the first wheel 3 minutes and 14 seconds after upload, Socket at 7 minutes, Chainguard at 16. None of that helps a CI runner or a Kubernetes node that resolved latest during the window, which is why the audit below starts from "what pulled it" rather than "is it still there".

## How the attacker got the keys to the release

The LiteLLM team's incident report is unusually candid, and the chain it describes should worry anyone who logs LLM traffic. BerriAI ran an internal LiteLLM deployment that was exposed to the public internet. An "unexpected, innocuously named user account" turned up in it. The attacker, the report says, got into that deployment and pulled GitHub CLI OAuth tokens out of its logs. Why were GitHub tokens in an LLM proxy's logs at all? Because, after Anthropic's investigation, the team observed that Claude Code "will embed a user's GitHub CLI OAuth token into a response body, and in situations where response bodies are logged, these tokens may appear in LiteLLM logs."

With three developers' tokens in hand, the attacker tried to change the workflow at 10:08 UTC, was rejected by branch protection, and five seconds later overrode it with a token belonging to an organisation owner. One account triggered the release run, another approved the production gate, a third deleted the workflow logs. From first push to malicious wheels on PyPI: 91 minutes.

This is the second LiteLLM supply-chain compromise this year. In March 2026 the TeamPCP group used a stolen PyPI publish token, harvested five days earlier when LiteLLM's own CI ran a backdoored Trivy scanner, to ship versions 1.82.7 and 1.82.8 with a credential stealer. March was a stolen publish token with no matching source; October was a hijacked build pipeline with clean source. Only one of those is caught by "check the source code".

## Who is exposed on Azure

Three populations, in rough order of blast radius:

- **LiteLLM proxy as your AI gateway.** A container on Azure Container Apps, AKS or App Service in front of Azure OpenAI or Foundry, with a Postgres database holding per-team virtual keys and the real upstream keys. If the tag was latest or main-stable and a scale-out, restart or redeploy happened between 11:46 and 14:20 UTC, the node pulled the bad digest. The stealer targets that database, so every provider key the proxy knew is burned.
- **litellm as a library.** Any unpinned requirement resolved during the window on a laptop, an Azure DevOps or GitHub Actions runner, or a container build. The runner's identity is what leaks: service connection secrets, Azure CLI session files, Key Vault secrets materialised as environment variables.
- **Developer machines.** A pip install to try the new release pulled the Claude, Codex, Gemini and Cursor sessions, browser passwords, SSH keys and the Azure CLI token cache.

Socket's triage note is blunt: starting Python or pip on an affected host "to inspect or repair the installation can execute the payload again." Inspect with the shell, not with Python.

## The two-hour audit

Work in this order. Containment first, evidence second, rotation third. Do not reinstall over the top.

### 1. Find every place 1.107.0 could have landed

Start with the registries you control, because image digests are the one piece of evidence that does not depend on a possibly-compromised host. Azure Container Registry keeps the manifest digest of everything it imported or cached, so compare against the known-bad Docker Hub digest, sha256:1bbcb1443bc962e45f5aad53fe3d2c30600e0379715fb6c664cdd3ac126c339e, and list which tags resolved during the window.

```
# ACR: which litellm images do we hold, and what digests?
az acr repository show-manifests -n <registry> --repository litellm/litellm \
  --orderby time_desc --query "[].{digest:digest,tags:tags,created:createdTime}" -o table

# Container Apps / AKS: what image did each revision or pod actually run?
az containerapp revision list -n <app> -g <rg> \
  --query "[].{rev:name,created:properties.createdTime,image:properties.template.containers[0].image}" -o table
kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"\t"}{.metadata.name}{"\t"}{.status.containerStatuses[*].imageID}{"\n"}{end}' | grep -i litellm

# Build logs: any pip resolution of 1.107.0 between 11:39 and 13:23 UTC?
# (Azure DevOps / GitHub Actions: search job logs for "litellm-1.107.0")
```

### 2. Hunt the on-host indicators with the shell

```
# Any of these present means the stealer ran or could run
find / -path '*site-packages*' \( -name 'cook.pth' -o -name 'cook.py' -o -name 'sitecustomize.py' \
  -o -name 'litellm-1.107.0.dist-info' \) 2>/dev/null
ls -la /tmp/remote_code_loader.lock /tmp/.pc_once_72129118c59f5636c0bb 2>/dev/null

# The second-stage daemon, if it is still alive
pgrep -af python | grep -v litellm-proxy
```

Treat a hit on any of these as confirmed execution. Note that sitecustomize.py is a legitimate Python mechanism and some images ship one deliberately, so read it before you draw conclusions, but read it with cat, not with Python.

### 3. Check egress for the exfiltration path

If your gateway sits behind Azure Firewall, a NAT gateway with flow logs, or a Container Apps environment with network logging, query for the two destinations that matter: the typosquat domain www.litellm[.]dev with a /healthcheck path, and raw.githubusercontent.com with the letters9813/pythonclient-test path. A hit on the first means data left. A hit on the second means the loader was alive and polling.

```
// Azure Firewall application rule logs (Log Analytics)
AZFWApplicationRule
| where TimeGenerated between (datetime(2026-10-21 11:30) .. now())
| where Fqdn has "litellm.dev" or Url has "letters9813/pythonclient-test"
| project TimeGenerated, SourceIp, Fqdn, Url, Action
```

### 4. Rebuild, do not repair

Chainguard's advice is the right one: isolate the host or container, remove the startup files with shell tools before any Python runs again, and then throw the environment away. Rebuild from a pinned, clean version. The LiteLLM team's guidance is to pin to 1.106.2 or any explicit version published before the incident. For images, pin by digest from now on, not by tag. A floating latest tag is exactly the surface this attack was designed for.

### 5. Rotate everything the process could read

If the host had outbound internet and Python started, assume exfiltration. On Azure that means, in priority order:

- **Azure OpenAI and Foundry keys** on every resource the proxy routed to. Regenerate both keys. Then move the proxy to managed identity so there is no long-lived key to steal next time; LiteLLM supports Entra ID token auth to Azure OpenAI.
- **Managed identity scope.** The stealer also reads instance metadata tokens, and a stolen one stays valid until it expires, typically up to 24 hours. Review what the gateway's identity could reach: Key Vault, storage, Cognitive Services resources. Tighten the role assignments while you rotate.
- **Every secret in Key Vault the process had GET on.** Not just the ones it used. Use the Key Vault audit log to see what was read on 21 October and rotate those first.
- **The LiteLLM proxy database.** Rotate the Postgres password, then every upstream provider key stored in it: Anthropic, OpenAI, Google, Mistral, whatever the config mapped. Rotate the virtual keys issued to teams as well; they are bearer tokens for your spend.
- **CI identities.** Service principal secrets on Azure DevOps service connections, GitHub Actions secrets, PyPI and npm publish tokens held by the runner. If you use workload identity federation there is no long-lived secret to rotate, which is the strongest argument for it you will read this week.
- **Developer credentials.** GitHub CLI tokens (the exact credential that started this incident), SSH keys, the Azure CLI token cache, and the Claude, Codex, Gemini and Cursor session files. Clear browser saved passwords and force a re-login.

Then watch for use. Wiz saw the attacker validating stolen AWS credentials with sts:GetCallerIdentity and enumerating IAM. The Azure equivalent is a service principal sign-in from an unfamiliar IP followed by role enumeration, so watch Entra ID sign-in logs and Azure OpenAI token usage in Azure Monitor for the next two weeks.

## The lesson that outlives the incident: your gateway logs are a credential store

Strip the attack down and the root cause is mundane. An LLM proxy logged response bodies. A coding agent put a GitHub OAuth token in a response body. The proxy was reachable from the internet. That combination gave an attacker release rights to a package with tens of millions of monthly downloads, and nothing in the chain is specific to BerriAI. Ask the same three questions of your own gateway:

- **Do you log prompts and completions?** Most teams do, for debugging, cost attribution or AI Act Article 12 logging. Those logs contain whatever your agents paste into context: tokens from gh auth, connection strings from .env files, Key Vault secrets echoed by a tool call. Treat the log store as you treat Key Vault: restricted access, short retention, redaction at write time.
- **Is any path reachable without authentication?** Symantec published research this week on an autonomous "Skynet" agent that scans the internet for exposed OpenAI-compatible endpoints, hijacks them for inference, and runs up to 100,000 steps without a human. Its honeypot logged more than 127,000 requests from tens of thousands of addresses. The mitigation list opens with "require authentication on all LLM API paths including health checks."
- **Can the gateway reach the internet?** It needs Azure OpenAI and perhaps two or three third-party providers. It does not need raw.githubusercontent.com. An egress allowlist on the Container Apps environment or an Azure Firewall rule would have cut both stages of this payload off at the network.

## Decision guide: keep LiteLLM, or move the gateway into Azure API Management

Two compromises in seven months is a reasonable trigger for a build-versus-buy review. Azure API Management's AI gateway is not a separate product; it is a set of policies on the ordinary APIM gateway. Microsoft Learn lists what it covers today: OpenAI Chat Completions and Responses APIs, the Anthropic Messages API in the v2 tiers, Google Vertex AI, remote MCP servers and A2A agent APIs, plus a preview unified model API that exposes several providers through one OpenAI-compatible endpoint.

| Concern | LiteLLM proxy (self-hosted) | APIM AI gateway |
| --- | --- | --- |
| Supply-chain surface | You pull a wheel or image from a public registry on every release. Two compromises in 2026. | Managed service; no artefact for you to pull. Your exposure is policy XML you author. |
| Auth to Azure OpenAI | Keys in the proxy database by default; Entra ID token auth is available but you configure it. | Managed identity by default in the Foundry import wizard; no key to store. |
| Multi-provider routing | Hundreds of providers, mature, the main reason teams chose it. | OpenAI, Anthropic (v2 tiers), Vertex, Bedrock passthrough; unified model API is preview. |
| Token limits and quotas | Per virtual key, in the proxy. | llm-token-limit policy per subscription, IP or any expression; prompt tokens pre-counted at the gateway. |
| Caching | Redis-backed exact and semantic cache. | Semantic cache via Azure Managed Redis with llm-semantic-cache-store and lookup policies. |
| Logging | Callbacks to Langfuse, OTel and others; response-body logging is the thing that just burned BerriAI. | Prompts and completions to Azure Monitor; llm-emit-token-metric to Application Insights with custom dimensions. |
| MCP and agent governance | Pass-through; governance is your code. | Expose REST as MCP, govern existing MCP servers, register agents in the Foundry control plane. |
| Cost | Compute plus Postgres plus Redis, and the engineering hours for days like today. | APIM tier pricing; the v2 tiers are the practical floor for Anthropic routing. Price it for your region before deciding. |

The recommendation splits on one question. If your traffic is Azure OpenAI and Foundry models, with at most Anthropic on the side, move the gateway to APIM and let managed identity remove the stored-key problem. You stop pulling a release artefact that has now been poisoned twice. If you depend on LiteLLM's long tail of providers or its routing across non-Azure clouds, keep it but change how you run it:

- Use the enterprise release from the private registry, which the team confirms was not affected, or build from source in your own pipeline as Chainguard does.
- Pin by digest in Container Apps and AKS. Review the pin on your schedule, not the registry's.
- Front the public registries with a private feed and a cooling-off period of at least 24 hours. Three scanners flagged this release inside 16 minutes and PyPI pulled it inside two hours; a one-day delay turns it into a non-event.
- Managed identity to Azure OpenAI, egress allowlist, read-only root filesystem, non-root image, no public ingress. Put APIM or Front Door in front with authentication on every path.
- Stop logging response bodies to anything an internet-facing account can read.

## The Swedish and EU angle

For organisations in Sweden this is not only an engineering clean-up. Three clocks may already be running.

**Cybersäkerhetslagen and NIS2.** Sweden's NIS2 transposition, SFS 2025:1506, has been in force since 15 January 2026 and covers roughly 8,000 organisations across 18 sectors. A significant incident requires an early warning within 24 hours of becoming aware of it, a notification within 72 hours, and a final report within 30 days. If your LiteLLM host ran the payload and held credentials to systems in scope, the confirmed-execution finding from step 2 above is the moment you became aware. Write that timestamp down.

**GDPR Article 33.** The stealer took browser cookie databases, shell history and the proxy's logs, and those logs contain prompts. If prompts contained personal data, which in a customer-service or HR assistant they almost certainly did, the 72-hour notification clock to IMY starts at awareness. Your data protection officer needs the step 3 egress result, not a vague "we are investigating".

**The Cyber Resilience Act.** If you ship a product with digital elements that embeds LiteLLM, Article 14 has applied since 11 September 2026. An actively exploited vulnerability in your product must be reported through ENISA's Single Reporting Platform within 24 hours, with a fuller notification at 72 hours. A malicious dependency that executed inside your product counts. Swedish ISVs who assumed the CRA was a December 2027 problem should reread the reporting article this week.

There is also a procurement point. Public-sector buyers running upphandling for AI platforms ask how the supplier governs third-party components. "We pull latest from Docker Hub" was a weak answer in September. After two LiteLLM compromises in one year it is a disqualifying one.

## What to do today

- List every ACR digest, Container Apps revision and AKS pod that ran a litellm image created on 21 October, and every CI log that resolved 1.107.0.
- Hunt for cook.pth, cook.py and an unexpected sitecustomize.py with find, never with pip or python.
- Query egress logs for litellm[.]dev and the letters9813 GitHub path since 11:30 UTC on 21 October.
- Rebuild from 1.106.2 or an enterprise build, pinned by digest. Rotate Azure OpenAI keys, Key Vault secrets the identity could read, the proxy database and every provider key in it, CI secrets and developer GitHub tokens.
- Decide, this sprint, whether the gateway belongs in APIM with managed identity. For Azure-centric traffic the answer is yes.
- Open the NIS2, GDPR and CRA reporting questions with legal before the 24-hour window closes, not after.

## Sources

- [LiteLLM: Contained: Oct 21, 2026 LiteLLM release v1.107.0 compromised (incident report)](https://docs.litellm.ai/blog/security-incident-oct-21-2026)
- [Datadog Security Labs: LiteLLM supply chain compromise, what you need to know](https://securitylabs.datadoghq.com/articles/litellm-compromised-pypi-docker/)
- [Chainguard: LiteLLM attacked again, malicious 1.107.0 wheels on PyPI and Docker Hub](https://www.chainguard.dev/unchained/litellm-attacked-again-malicious-1-107-0-wheels-on-pypi-and-docker-hub-october-2026)
- [Socket: LiteLLM PyPI package and Docker images compromised to steal cloud and AI credentials](https://socket.dev/blog/litellm-compromise)
- [AISLE: Compromised LiteLLM package found 3 minutes after release](https://aisle.com/blog/litellm-11070-on-pypi-shipped-a-credential-stealer)
- [Wiz: LiteLLM compromised, steals credentials](https://www.wiz.io/blog/litellm-compromised-steals-credentials)
- [Datadog Security Labs: the March 2026 LiteLLM compromise and the TeamPCP campaign](https://securitylabs.datadoghq.com/articles/litellm-compromised-pypi-teampcp-supply-chain-campaign/)
- [Symantec: Autonomous LLMjacking, the Skynet agent hijacking exposed AI models](https://www.security.com/threat-intelligence/ai-llmjacking-skynet)
- [Microsoft Learn: AI gateway capabilities in Azure API Management](https://learn.microsoft.com/en-us/azure/api-management/genai-gateway-capabilities)
- [Jones Day: EU Cyber Resilience Act 24-hour reporting duties start 11 September 2026](https://www.jonesday.com/en/insights/2026/07/eu-cyber-resilience-act-24hour-reporting-duties-start-september-11-2026)
- [Gallagher: Stärkta krav på cybersäkerhet, detta är NIS2 (cybersäkerhetslagen)](https://www.ajg.com/se/news-and-insights/starkta-krav-pa-cybersakerhet-detta-ar-nis2/)

---

Technspire AB builds AI agents, Azure OpenAI solutions, and production web platforms for Swedish and EU enterprises. Book a call: https://calendly.com/technspire · hello@technspire.com · More articles: https://technspire.com/en/blog · Site overview for agents: https://technspire.com/llms.txt
