Contents
ToggleExecutive Summary
On October 21, 2026 at 11:39 UTC, a malicious litellm==1.107.0 was uploaded to PyPI across all seven platform wheels. The same payload shipped in the official GHCR and Docker Hub images under v1.107.0, latest, main-latest and main-stable. PyPI removed the release by 13:23 UTC and BerriAI finished repointing Docker Hub tags at 14:20 UTC. Wheels installed three files into the site-packages root: cook.pth, cook.py and sitecustomize.py. Python executes both hook types at interpreter start, so the stealer ran on any Python process in the environment, pip included, whether or not the code ever imported LiteLLM.
The payload, self-named privcollect, runs 16 collection modules covering browsers, SSH and GPG keys, cloud credentials and instance metadata, CI secrets, every readable process environment, more than 20 AI coding tools, and the LiteLLM proxy’s own LiteLLM_CredentialsTable. A second hook forks a detached daemon that polls a GitHub dead drop hourly for a second-stage host. Wiz Research observed the stolen AWS role credentials being validated about 4.5 hours after exfiltration, followed by Bedrock model enumeration consistent with LLMjacking.
This is the second LiteLLM compromise this year. In March, TeamPCP pushed 1.82.7 and 1.82.8 with a PyPI token harvested by a poisoned Trivy action. The October entry point is different and more uncomfortable for anyone running an AI gateway: BerriAI states the attacker obtained GitHub CLI OAuth tokens from the logs of an exposed internal LiteLLM deployment, then used them to override branch protection, trigger the release workflow, approve the production gate, and delete the workflow records afterward. The gateway that brokers your model keys was also logging your developers’ GitHub tokens. No CVE has been assigned and none is expected for the package compromise itself.

TL;DR for Engineering Teams
What it is: Malicious litellm==1.107.0 (all 7 wheels) and LiteLLM Docker images carrying a .pth plus sitecustomize.py credential stealer and hourly remote code loader. No CVE. Attribution unconfirmed.
Where it bites: Any environment that installed 1.107.0 between 11:39 and 13:23 UTC on October 21, or ran ghcr.io/berriai/litellm* / docker.io/litellm/litellm from v1.107.0, latest, main-latest or main-stable between 11:46 and 14:20 UTC. Unpinned litellm>= constraints and floating image tags are the main exposure paths.
Why it matters: Execution needs no import. Proxy hosts with DATABASE_URL set lose every upstream provider key stored in the proxy DB. Stolen AWS role credentials were validated within ~4.5 hours (Wiz).
Patch status: Malicious artifacts removed from PyPI, GHCR and Docker Hub. Last clean release: 1.106.2. Release candidates 1.107.0rc1 and rc2 are clean.
Immediate action: Do not run pip first. Delete the three payload files, kill the process holding /tmp/remote_code_loader.lock, rebuild on litellm==1.106.2 (images pinned by digest), rotate every credential the host could reach, block litellm[.]dev.
Vulnerability Overview
| Field | Value |
|---|---|
| Vendor | BerriAI (LiteLLM) |
| Product | litellm (PyPI), LiteLLM Proxy images (GHCR, Docker Hub) |
| Vulnerability Type | Supply chain compromise: malicious release via hijacked CI/CD release workflow |
| CWE | CWE-506 (Embedded Malicious Code); CWE-532 (Sensitive Information in Log File) for the upstream token exposure |
| CVSS Score | Not scored (no CVE) |
| CVE | Not assigned |
| Patch Available | Yes: pin 1.106.2; 1.107.0 removed |
| Active Exploitation | Confirmed (credential validation and AWS discovery observed by Wiz) |
| Attribution | Unattributed (see lineage section) |
| Exposure Window | PyPI 11:39 to 13:23 UTC; Docker 11:46 to 14:20 UTC, October 21, 2026 |
| Exfiltration | hxxp://www.litellm[.]dev/healthcheck (plain HTTP POST) |
Technical Anatomy
Root Cause
Two failures stacked. Upstream, GitHub CLI OAuth tokens belonging to LiteLLM engineers ended up in request/response logs of an internal LiteLLM deployment that was reachable from the internet. In its incident report, BerriAI states it observed specific situations in which Claude Code embeds a user’s GitHub CLI OAuth token into a response body. Where response bodies are logged, those tokens can land in LiteLLM logs. BerriAI also found an unexpected, innocuously named user account in that deployment and suspects the attacker got in through a recently patched vulnerability but has not confirmed it.
Downstream, the release pipeline trusted those tokens fully. One account could override branch protection on the private release workflow and point the build source at an arbitrary repository; a second account could satisfy the production approval gate; a third could delete the run records. Two-person approval was in place and still failed, because both persons were the attacker.
Exploit Path
- Entry point: The attacker harvests three engineers’ GitHub CLI OAuth tokens from the exposed internal LiteLLM deployment’s logs, registers litellm[.]dev on October 13, and stages ten commits on a hyphenated lookalike repository, Berri-AI/litellm, on October 21 between 08:56 and 10:46 UTC.
- Vulnerability trigger: At 10:08 UTC branch protection rejects a change to the release workflow; five seconds later a privileged developer credential overrides it. The modified workflow builds from Berri-AI/litellm@20535d8f, a second account approves production at 11:11 UTC, and maturin’s data = “cook” directive copies the payload into litellm-1.107.0.data/purelib/ of every wheel. A final commit adds COPY cook ./cook to the Dockerfiles.
- Execution impact: pip installs the .data/purelib contents into the site-packages root. On the next interpreter start, cook.pth (12 bytes, import cook) loads the stealer and sitecustomize.py forks the loader daemon. Collected secrets are encrypted and posted to www.litellm[.]dev/healthcheck.
The v1.107.0 git tag (commit 66c8f5b1a9) is clean. Source review and tag-based provenance checks would not have caught this; SLSA provenance on the images did record the hyphenated source repository, which is how Lidor Machluf traced it in BerriAI/litellm#47826.

Payload Flow
pip install litellm==1.107.0 | docker run litellm:latest (Oct 21)
|
v
site-packages/ { cook.pth, cook.py, sitecustomize.py }
|
[any python start]
| |
cook.pth -> import cook sitecustomize.py (Linux/macOS)
| |
privcollect (daemon thread) flock /tmp/remote_code_loader.lock
PRIVCOLLECT_ACTIVE=1 fork + setsid, cwd=/
16 modules incl. db_litellm |
| every 3600s: GET raw.githubusercontent.com/
AES-256-CBC + RSA-4096 OAEP letters9813/pythonclient-test/main/domain.txt
| |
POST hxxp://www.litellm[.]dev/ if host: GET hxxp://<host>/codepy -> exec()
healthcheck (X-Token)
|
HTTP 200 -> /tmp/.pc_once_72129118c59f5636c0bb
What privcollect Takes
| Module group | Targets |
|---|---|
| Browsers | Chromium-family login, cookie and web-data DBs (passwords decrypted in place on Linux/Windows); Firefox logins.json and key4.db; top 300 history URLs per profile |
| Keys | ~/.ssh/*, ~/.gnupg/ private keys, .pem/.key/.pfx/.p12/.pk8/.ppk under home, TLS keys from /etc/ssl/private, nginx, Apache, Let’s Encrypt |
| Cloud | AWS, Azure, gcloud credential files; AWS IMDSv2 (v1 fallback), ECS task creds, GCP SA tokens, Azure IMDS; AWS creds pre-validated with sts:GetCallerIdentity |
| Developer and registry | ~/.kube/config, Docker config, gh CLI tokens, .git-credentials, .netrc, .npmrc, .pypirc, Maven, Gradle, Composer, RubyGems, Cargo, .env files two levels deep |
| CI and infra | GitLab Runner config, Jenkins master key, Vault tokens, Terraform/Packer creds, K8s SA tokens, /run/secrets |
| Process environments | Current env plus /proc/<pid>/environ for every readable process |
| AI tooling | Claude Code, Claude Desktop, Cursor, Windsurf, Kiro, Zed, Copilot, VS Code MCP settings, Cline, Roo, Cody, Gemini CLI, Codex auth.json, Continue, Aider, Goose, Ollama, Amazon Q, Hugging Face token and others |
| LiteLLM proxy DB | Via the proxy’s own Prisma client when DATABASE_URL is set: LiteLLM_CredentialsTable and LiteLLM_ProxyModelTable (litellm_params holds upstream API keys) |
The /proc/<pid>/environ sweep is what makes CI runner and gateway hosts worse than laptops. Secrets injected into sibling processes are captured, not just the environment of the Python step that triggered the hook.
The run-once marker is only written after the exfil server returns HTTP 200. While exfiltration fails (egress blocked, DNS sinkholed), the stealer re-runs on every Python start. On Windows the /tmp marker path usually does not resolve, so it likely re-runs there unconditionally, and the loader fails silently because fcntl and os.fork are absent.
Phoenix Blue Detection for LiteLLM
LiteLLM 1.107.0 has no CVE, and none is coming. Any tool that waits for an NVD entry shows nothing for this package.
Phoenix Blue works differently. It is Phoenix Security’s free threat intelligence feed for malicious and compromised open source packages, and it scores package versions on behavior, not on advisories. This payload is a textbook case of what it looks for: new .pth and sitecustomize.py files that run on every Python start, credentials sent over plain HTTP to a lookalike domain, and a background process that fetches remote code on a schedule.
Every Phoenix Blue verdict has a public page you can look up by package URL (purl), share with your team, and use as evidence in an incident ticket.
Phoenix Blue is free. Look up any package, follow new verdicts as they land, and get the signal before the advisory exists: phxintel.security
For Phoenix Security customers, the same verdicts feed the platform directly. A verdict becomes a finding on every repository, image and workload that carries the package, with an owner attached.
Protect yourself with the latest threat intelligence, get free access to Phoenix Blue Today
The Loader and the Orphaned Dead Drop
The dead drop history shows operator testing: localhost:18909 on October 18, test.test.com:18909 on October 19, emptied on October 20. domain.txt was empty at release time and the /codepy stage was never observed live. GitHub has since removed letters9813 and its repository. That does not neutralize infected hosts. The daemon keeps polling a URL tied to a username, and anyone who re-registers letters9813 and recreates pythonclient-test/main/domain.txt gets code execution on every host where the daemon still runs. Killing the daemon is a recovery step, not housekeeping.
Campaign Phases

| Phase | Window (UTC) | Activity |
|---|---|---|
| Phase 1: Initial foothold | Before Oct 13 to Oct 21 | Access to exposed internal LiteLLM deployment; GitHub CLI OAuth tokens scraped from logs; litellm[.]dev registered Oct 13 00:45; letters9813 created Oct 13 06:43 |
| Phase 2: Expansion | Oct 21 08:56 to 11:54 | Ten commits on Berri-AI/litellm; branch protection overridden 10:08:34; staging run 10:46; prod approval 11:11; wheels to PyPI 11:39; images 11:46; workflow records deleted 11:53 |
| Phase 3: Ecosystem targeting | Oct 21 11:39 to 14:20 | Wheels on PyPI (~1h44m); floating Docker tags on GHCR (to 13:51) and Docker Hub (to 14:20); mirrors kept serving cached wheels after PyPI removal (StepSecurity) |
| Phase 4: Persistence and use | Oct 21 16:20 onward | Stolen AWS role creds validated from 168[.]100[.]8[.]57 and 165[.]227[.]201[.]8; bedrock:ListFoundationModels; IAM and CloudTrail discovery in some environments; hourly loader polling continues on uncleaned hosts |
Lineage: March (TeamPCP) vs October
| Dimension | March 2026 (1.82.7 / 1.82.8) | October 2026 (1.107.0) |
|---|---|---|
| Attribution | TeamPCP, high confidence | Unattributed |
| Initial access | PyPI token stolen by poisoned Trivy action in LiteLLM CI | GitHub CLI OAuth tokens from exposed internal LiteLLM deployment logs (per BerriAI) |
| Publish path | Direct PyPI upload, bypassing GitHub | BerriAI’s own release workflow, sourced from lookalike repo |
| Docker images | Not affected (pinned) | Affected, including latest |
| Startup hook | litellm_init.pth (1.82.8) | cook.pth and sitecustomize.py together |
| Exfil domain | models.litellm[.]cloud | www.litellm[.]dev |
| Crypto | AES-256-CBC, RSA-4096 OAEP key wrap, tar.gz | AES-256-CBC, RSA-4096 OAEP-SHA256 key wrap, tar.gz |
| Lateral movement | Privileged pods on every K8s node, systemd persistence | None observed; hourly loader daemon |
| Distinctive target | AWS Secrets Manager and SSM via SigV4 | LiteLLM proxy DB, 20+ AI coding tools |
The overlap (LiteLLM lookalike domain, .pth startup execution, AES-CBC with RSA-4096 key wrap) is real but commodity-grade, and the March tradecraft was documented in detail publicly. The access path, the K8s behavior and the operator’s dead-drop design all differ. Treat a TeamPCP link as unproven until someone matches the RSA public key or infrastructure. Wiz notes strong signs of LLM-aided development in the payload: verbose naming, blanket try/except, untested Windows paths, and a dead exec_py module. The .pth plus sitecustomize.py combination is, per Wiz, the first documented in-the-wild use of both together.
Mis-citation risks: some March-era tooling tags 1.82.7/1.82.8 as CVE-2026-33634, which is a Trivy-related identifier and not a LiteLLM package CVE. CVE-2026-59822 (LiteLLM auth bypass before 1.84.0, exploited in Wiz honeypots in September) and CVE-2026-40217 are LiteLLM product vulnerabilities; neither has been confirmed as the access vector for this incident. Do not cite either as “the LiteLLM 1.107.0 CVE”.
Affected Versions
| Package | Vulnerable Versions | Fixed Version | Notes |
|---|---|---|---|
| litellm (PyPI wheels) | 1.107.0 (all 7: macOS x86_64/arm64, manylinux aarch64/x86_64, musllinux aarch64/x86_64, win_amd64) | 1.106.2 | Payload in litellm-1.107.0.data/purelib/ |
| litellm (PyPI sdist) | 1.107.0 | 1.106.2 | data = “cook” present, cook/ absent: tampered config, no payload |
| ghcr.io/berriai/litellm | v1.107.0, latest, main-latest, main-stable (11:46 to 13:51 UTC) | v1.106.2 by digest | Index sha256:1bbcb1443bc9… |
| ghcr.io/berriai/litellm-database | Same tags and window | v1.106.2 by digest | Index sha256:c00f9d92c427…; db_litellm fires on first start |
| ghcr.io/berriai/litellm-non_root | Same tags and window | v1.106.2 by digest | Index sha256:7b00e91cd2fe… |
| docker.io/litellm/litellm | v1.107.0, latest, main-latest, main-stable (11:46 to 14:20 UTC) | v1.106.2 by digest | Same index as GHCR |
| litellm 1.107.0rc1, rc2, 1.103.x to 1.106.x | Clean | n/a | 1.107.0.dev1 to dev3 not inspected (StepSecurity) |
| litellm-proxy-extras, litellm-enterprise | Clean | n/a | Reported clean in original disclosure |
| LiteLLM private enterprise registry | Not affected | n/a | Per BerriAI |
Exposure Analysis
| Environment | Risk Level | Reason |
|---|---|---|
| Internet-exposed LiteLLM proxies | Critical | Floating latest/main-stable tags pulled on restart; litellm-database image dumps provider keys on first start; gateway master and salt keys in env |
| CI/CD pipelines | Critical | Fresh resolves of litellm>= during the window; /proc/*/environ sweep captures secrets of sibling steps; AWS role creds observed abused from runners |
| Cloud workloads and Kubernetes nodes | High | IMDS role theft confirmed and validated; Bedrock enumeration consistent with LLMjacking; IAM discovery in some environments |
| Developer environments | High | Browser passwords decrypted locally, SSH/GPG keys, gh tokens, 20+ AI coding tool configs; loader daemon persists on macOS/Linux |
Scale: LiteLLM shipped 12 releases across four minor lines between October 17 and 21 (StepSecurity), and maturin platform wheels mean essentially every 1.107.0 install got the payload. Chainguard flagged 1.107.0 sixteen minutes after upload. No public download count for the 104-minute PyPI window has been released.
Real-World Impact
Post-exploitation is confirmed. Wiz observed the attacker validating AWS instance role credentials harvested from compromised CI runners and Kubernetes nodes between 16:20 and 17:01 UTC, roughly 4.5 hours after exfiltration, from two IPs. Each credential got sts:GetCallerIdentity, then bedrock:ListFoundationModels and a ListModels call. That sequence is the standard LLMjacking qualification step: confirm the identity, confirm the model access, sell or burn the quota. In a smaller set of environments the attacker moved to IAM enumeration (ListRoles, ListUsers, ListAccessKeys) and DescribeTrails.
The realistic scenarios for a team that ran 1.107.0:
- Model-key theft at the gateway: every upstream key in LiteLLM_CredentialsTable gone, plus the master key that mints virtual keys. Expect fraudulent spend against OpenAI, Anthropic, Azure OpenAI and Bedrock accounts.
- Cloud foothold via instance role: IMDS credentials from runners and nodes, already validated and used for discovery.
- Release pipeline repeat: gh CLI tokens, .pypirc and .npmrc from developer and CI hosts are the same class of credential that produced this incident. If you publish packages, your pipeline is now the next candidate.
- Latent RCE: uncleaned hosts with the loader daemon running are one username registration away from arbitrary code execution.
The broader lesson for AI platform owners is about logging. Prompt and response bodies are routinely logged for spend tracking, evals and debugging. When coding agents pass tool output through the gateway, those bodies can carry tokens. The log store becomes a credential store with weaker access control than your secrets manager.
Detection Guidance
Log and Network Indicators
| Indicator | Where to look | Meaning |
|---|---|---|
| DNS/HTTP to litellm[.]dev, www.litellm[.]dev | DNS resolver, egress proxy, Harden-Runner baselines | Stealer ran; exfil attempted |
| POST /healthcheck with X-Token: XWiA8gZNa7NcX2XRoAEb and UA Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 10.0; WOW64; Trident/7.0) | Proxy logs, NDR | Exfil payload (AES/RSA, contents unrecoverable) |
| GET raw.githubusercontent.com/letters9813/pythonclient-test/main/domain.txt hourly | Egress proxy | Loader daemon alive |
| Any request path ending /codepy | Proxy logs | Second stage executed: incident scope expands |
| sts:GetCallerIdentity then bedrock:ListFoundationModels from 168[.]100[.]8[.]57 or 165[.]227[.]201[.]8 | CloudTrail | Stolen role creds in use |
| Instance-role API calls from outside your IP space after Oct 21 11:39 UTC | CloudTrail, GuardDuty InstanceCredentialExfiltration | Credential replay |
| GitHub audit log: branch protection bypass, workflow run deletion, OAuth app token use from new IPs | GitHub org audit log | Same access pattern against your org |
Host Indicators
| Indicator | Type |
|---|---|
| cook.pth, cook.py, sitecustomize.py in site-packages root (or /app/.venv/lib/python3.13/site-packages/ in images) | File |
| litellm-1.107.0.dist-info | File |
| /tmp/.pc_once_72129118c59f5636c0bb | Marker (exfil succeeded) |
| /tmp/remote_code_loader.lock held by a process with PPID 1, cwd / | Process |
| .pc_sq_*, .pc_e_* in temp dir | File |
| PRIVCOLLECT_ACTIVE=1 in process environment | Env |
| data = “cook” under [tool.maturin] | Build config |
| __pycache__/cook*.pyc, sitecustomize*.pyc | File |
File Hashes (SHA-256)
| Artifact | Linux/macOS | Windows |
|---|---|---|
| cook.pth | 7f191632e7320705d154b85204f1ae546b543acbd7e8861229b76f5042b92bdb | 84dc8c82c0a4a4a62552ef826b49fcc86d1a5c4a5d0f2c06665723f93d129d7d |
| cook.py | 9dd0a3926f96a37eb86cf1ee2310d9be93139cc4ebef3ef9e2efe9531390be25 | 03ac2a8b752605d1d7710979c17ff18b03dd978203ba23ba97fd8ce3d6f897ea |
| sitecustomize.py | 34bef04fe88ac9d0aae0475630b1738681d15b94f233ac8aa46eacf5968305fa | b6122440d0d9cb1eb1ade9933f49214da8c31288de7710c076c2680dfa528465 |
| Wheel / Image | SHA-256 |
|---|---|
| litellm-1.107.0-cp310-abi3-macosx_10_12_x86_64.whl | 7598727f209d717041db37f7dca271d3a7d328a279890e0e1d55375729aeb86d |
| litellm-1.107.0-cp310-abi3-macosx_11_0_arm64.whl | 0aaf7ba346d41ef2f40390436738b001180d5a1c03aa093f121078a9969d9a1a |
| litellm-1.107.0-cp310-abi3-manylinux_2_28_aarch64.whl | 668169d35b87bf51324080390b1b1c5ab12bd335cfe5f1a99e3689abd5e02ae6 |
| litellm-1.107.0-cp310-abi3-manylinux_2_28_x86_64.whl | 462d0c9f67ca522d9e0e4acd245e90b36ce0d5353604a66bb7c43a1ec4a6fcf7 |
| litellm-1.107.0-cp310-abi3-musllinux_1_2_aarch64.whl | c2e32d0ce2548aa662c18bf5001911a79fe5b30cf8357132275add02fdcde054 |
| litellm-1.107.0-cp310-abi3-musllinux_1_2_x86_64.whl | 1bbce8caa94f01c0aaa0b7ddae51553a8a32e1ce53d96d44fe583272aff7ad00 |
| litellm-1.107.0-cp310-abi3-win_amd64.whl | 12d4ccc5c6f8d35cf4f2cf233b7f3e418e863e657d81ff5f5fc9e210005b9484 |
| litellm-1.107.0.tar.gz (sdist) | 2befe2a79fb4fbbd9a543b5b40b530c8cffa8771bab22b8fb22b2e4ae3b1f819 |
| ghcr.io/berriai/litellm:v1.107.0 amd64 / docker.io/litellm/litellm amd64 | bd1aeee419ea36da9aa2b657288da4101887be468f020ac63d22ba6ed84a3ea5 |
| ghcr.io/berriai/litellm:v1.107.0 arm64 / docker.io/litellm/litellm arm64 | 56b74573eed6e4aafe4b50603dbe7853e0345f188c8e44a09f1098caf1f4bcdf |
| ghcr.io/berriai/litellm-database:v1.107.0 amd64 | 24581ddc30767f16ecb69d2b0517556f2cd7dea56971adb42a3d1beb97d69523 |
| ghcr.io/berriai/litellm-database:v1.107.0 arm64 | 827dd893c74cf621a860a66a3a447a5c732533f087589ce905573e285c0a239c |
| ghcr.io/berriai/litellm-non_root:v1.107.0 amd64 | 645a67d810628252d63df85dcc8824c58bcc77cf8383f4d3e6321f0ccc3ce9e4 |
| ghcr.io/berriai/litellm-non_root:v1.107.0 arm64 | f75c8edae59b72b1ebfd617295833f465c85bb2fab9c1d60f21b496db86cb8da |
| Image index (litellm, Docker Hub) | 1bbcb1443bc962e45f5aad53fe3d2c30600e0379715fb6c664cdd3ac126c339e |
Scanner References
Phoenix Security ingests SCA, container and SBOM findings and matches litellm@1.107.0 and the image digests above across repositories, registries and running workloads through the campaign config published with this article. GitHub dependency graph and Dependabot will surface the version in committed lockfiles, but not floating Docker tags or unpinned CI installs. Container scanners need the digest list, not the tag, because the tags have been repointed to clean images. SBOMs generated from images built on October 21 are the fastest way to find rebuilt-in-house images that inherited the bad base.
Verification Steps for Teams
- Search lockfiles and manifests: grep -rniE “litellm[=<>~! ]*1\.107\.0([^0-9rc]|$)” . –include=requirements*.txt –include=*.lock –include=pyproject.toml –include=Pipfile*
- List every project with an open-ended litellm>= constraint and every Dockerfile, Compose file or Helm value using a floating LiteLLM tag; pull CI logs for installs between 11:39 and 13:23 UTC.
- Check what actually ran: kubectl get pods -A -o jsonpath='{range .items[*]}{.status.containerStatuses[*].imageID}{“\n”}{end}’ | grep -i litellm and compare against the digests above.
- On hosts: find / -name cook.pth -path “*site-packages*” 2>/dev/null and lsof /tmp/remote_code_loader.lock.
- Query internal PyPI mirrors and pull-through registry caches for the wheel and image hashes; some mirrors served cached 1.107.0 after PyPI removed it.
- Review CloudTrail for GetCallerIdentity from instance roles near install time and any later use from external IPs.
Remediation Guidance
Immediate Actions
- Do not start with pip install or pip uninstall. Every Python start re-runs both hooks. From the shell, delete cook.pth, cook.py, sitecustomize.py and matching __pycache__ entries from every affected site-packages.
- Kill the daemon holding /tmp/remote_code_loader.lock (lsof or fuser; PPID 1, cwd /).
- Rebuild virtualenvs and images from scratch on litellm==1.106.2; for containers, pin v1.106.2 by digest and replace, do not clean in place.
- Purge 1.107.0 from internal PyPI mirrors, Artifactory/Nexus caches and container registries by hash.
- Rotate everything the host could reach: instance and task IAM roles, cloud access keys, SSH and GPG keys, saved browser passwords and session cookies, GitHub (including gh OAuth tokens), npm, PyPI and other registry tokens, Vault and Kubernetes tokens, CI secrets, and AI tool tokens.
- On proxy hosts: rotate every provider key in LiteLLM_CredentialsTable and model litellm_params, the LiteLLM master key, salt key, all virtual keys, and the DATABASE_URL password.
- Block litellm[.]dev at DNS and egress; alert on raw.githubusercontent.com/letters9813/ and any /codepy path.
- Set provider-side spend alerts and Bedrock model-invocation logging to catch LLMjacking on keys you have not rotated yet.
Temporary Mitigations and Hardening
- Pin LiteLLM exactly and with hashes (pip install –require-hashes), and pin images by digest. Hash pinning would not have stopped a fresh resolve in March or October, but it stops silent upgrades.
- Add a package cooldown (reject versions younger than 72 hours) on your internal PyPI proxy. Both LiteLLM incidents were cleaned up in under four hours.
- Alert on any new .pth file or non-distribution sitecustomize.py in site-packages; both LiteLLM waves executed through them.
- Scrub request/response bodies in gateway logging. Redact token patterns (gho_, ghp_, github_pat_, sk-, AKIA) before persistence, restrict log access, and keep internal gateways off the public internet.
- Treat release workflows as Tier 0: require branch protection with no admin bypass, restrict workflow source inputs to the canonical repository, short-lived gh tokens, and alert on workflow run deletion.
- Restrict egress from CI runners and gateway hosts to an allowlist; the stealer re-runs while exfil fails, but nothing leaves.
–ignore-scripts-style controls are irrelevant here: there is no install script. Execution happens at interpreter start through standard CPython features.
MITRE ATT&CK Mapping
| Technique | ID | Observed behavior |
|---|---|---|
| Supply Chain Compromise: Software Supply Chain | T1195.002 | Malicious 1.107.0 via release workflow |
| Valid Accounts | T1078 | Three engineers’ GitHub accounts via OAuth tokens |
| Unsecured Credentials: Credentials in Files | T1552.001 | Tokens recovered from gateway logs; dotfiles and .env sweep |
| Event Triggered Execution: Python startup hooks | T1546.018 | cook.pth, sitecustomize.py |
| Credentials from Web Browsers | T1555.003 | Chromium/Firefox stores |
| Unsecured Credentials: Cloud Instance Metadata API | T1552.005 | IMDSv2, ECS, GCP, Azure metadata |
| Steal Application Access Token | T1528 | gh, AI tool OAuth tokens |
| Web Service: Dead Drop Resolver | T1102.001 | letters9813/pythonclient-test/domain.txt |
| Exfiltration Over Unencrypted Non-C2 Protocol | T1048.003 | HTTP POST to /healthcheck |
| Indicator Removal | T1070 | Release workflow run records deleted |
| Acquire Infrastructure: Domains | T1583.001 | litellm[.]dev registered Oct 13 |
| Cloud Infrastructure Discovery / Account Discovery | T1580 / T1087.004 | Bedrock, IAM, CloudTrail enumeration |
Phoenix Security Recommendations
This incident has no CVE, so any program that waits for an NVD entry to open a ticket has no ticket. Phoenix Security’s campaign for this wave matches on package coordinates and image digests directly, so the backlog exists the same hour the IOCs do.
Contextual deduplication. The same litellm@1.107.0 shows up as an SCA finding in a repository, a container finding in the registry, and an SBOM component on a running pod. Phoenix collapses those into one finding per affected asset, with the owning team attached, so a 40-repo exposure reads as 40 owned items rather than 300 scanner rows.
Reachability analysis. Reachability normally answers “is the vulnerable function called”. Here the answer is always yes, because the payload runs at interpreter start. What matters is runtime presence: Phoenix correlates the vulnerable component with deployed workloads and flags which images actually ran on October 21, which hold DATABASE_URL, and which sit behind internet-facing endpoints, so rotation starts with the proxies holding provider keys.
Remediation campaigns. Import the attached campaign config, then: create the campaign, let ownership attribution assign each affected repository and workload, track rebuild-and-pin to 1.106.2 per asset, and verify runtime exposure is zero once the old digests stop appearing in cluster inventory. Credential rotation is tracked as a campaign task alongside the package fix, not as a separate spreadsheet.


Attack surface management. Internet-exposed LiteLLM proxies were both the target of this payload and, per BerriAI, the starting point of the attack. Phoenix ASM inventories exposed gateway endpoints so teams can find the internal deployment that should never have been public before someone else does.
Behavioral detection. The structural gap is the same one we wrote about in March: CVE-driven SCA has no detection surface during the exposure window. Blue Shield CI/CD Firewall and PHX-Neural flag the behavior (a new .pth or sitecustomize.py landing in site-packages, a build directive that pulls an undeclared directory into a wheel, CI egress to a never-seen lookalike domain) instead of waiting for a version to land on a blocklist.
Phoenix Security correlates vulnerable components with runtime workloads, identifies exposed services via attack surface management, and assigns remediation ownership: the blast radius becomes an owned, trackable remediation backlog.
External References
- LiteLLM, Contained: Oct 21, 2026 LiteLLM release v1.107.0 Compromised
- BerriAI/litellm issue #47826 (initial report, Lidor Machluf)
- Attacker commit range via fork object sharing: https://github.com/BerriAI/litellm/compare/a8113b0d…20535d8f
- Malicious head commit 20535d8f
- Clean v1.107.0 tag commit 66c8f5b1a9
- Wiz Research, LiteLLM Compromised, Steals Credentials (Benjamin Read, Rami McCarthy), October 21, 2026
- StepSecurity, litellm 1.107.0 Compromised on PyPI
- Chainguard, LiteLLM attacked again
- OX Security, LiteLLM 1.107.0 Compromised With A RAT InfoStealer
- PyPI project page (1.107.0 quarantined)
- Python docs, site module (.pth and sitecustomize)
- Phoenix Security, TeamPCP Backdoors LiteLLM: Your AI Gateway Just Became the Attack Vector, (March 2026)














