LiteLLM 1.107.0 supply-chain attack: are you exposed?

On Oct 21 a malicious litellm 1.107.0 reached PyPI and Docker for about two hours, plus two separate CVE stories landed the same month. Here is how to check a home lab box without starting Python, which versions to be on, and the hardening that worked when I tested it.

LLMBits card: LiteLLM 1.107.0 supply-chain attack, are you exposed?

If you run LiteLLM at home, the first check takes one command and does not start Python. Search the disk for the malicious release and its startup hook:

sudo find / -xdev \( -name 'litellm-1.107.0.dist-info' -o -name 'cook.pth' \) 2>/dev/null

No output is a good sign, and not proof. LiteLLM's own incident post says finding either file confirms exposure, while not finding one does not prove the box is clean. If you ran pip install -U litellm or started a latest, main-latest, main-stable or v1.107.0 image on the morning of Oct 21 (Pacific time), keep reading before you do anything else, and do not run pip show litellm to check. I explain why below, and I measured it in my home lab.

Status in this post is as of Oct 24, 2026, 10 PM PT. Three different LiteLLM problems hit the news this month, and most write-ups blur them together. I keep them separate here, because each one has a different fix and a different set of people who should care.

Who is exposed to the 1.107.0 release

LiteLLM's incident post defines the windows. You are affected if either of these happened on Oct 21, 2026:

  • You installed an unpinned LiteLLM with pip between 11:39 and 13:23 UTC (4:39 to 6:23 AM PT). That includes pip install -U litellm and any tool whose requirements let pip pick the newest release.
  • You used the Docker image tags v1.107.0, latest, main-latest or main-stable between 11:46 and 14:20 UTC (4:46 to 7:20 AM PT). GHCR tags were gone by 13:51 UTC. A community reproduction in the GitHub issue found that docker pull alone does not run the payload, while docker run, docker exec or a derived image's RUN step does. An image pulled in that window keeps those bytes on disk until you pull again.
Screenshot of LiteLLM's official incident post: the TLDR gives the PyPI window for 1.107.0 and the Docker window for the v1.107.0, latest, main-latest and main-stable tags, and the affected versions list includes the malicious image index digest
LiteLLM's own incident post, captured Oct 24. These are the windows I use above.

LiteLLM says its private enterprise registry was not affected. Wiz counted about 49,000 downloads of 1.107.0 from ClickPy data, and that count includes mirrors and CI, so it says little about how many hosts ran it.

The lower-profile exposure is a tool that pulled LiteLLM in for you. I checked which popular agent and home-lab tools depend on it further down.

Three separate issues

Issue A: malicious release Issue B: Check Point CVEs Issue C: CVE-2026-114612
What litellm==1.107.0 wheels on PyPI and poisoned Docker/GHCR tags, Oct 21 CVE-2026-35029 (missing admin check on POST /config/update) and CVE-2026-59823 (SSRF via user_config) One crypto context for at-rest secrets and session tokens, so an internal user can forge an admin credential
Severity Credential stealer plus remote code loader CVSS 4.0 8.7 and 5.3 CVSS 3.1 9.9
Affected 1.107.0 only 35029: below 1.83.0. 59823: below 1.83.9 1.87.0 to before 1.100.4, 1.101.0 to 1.101.2, 1.102.0 to 1.102.1, 1.103.0, 1.104.0-rc.1
Fixed Any other release. Current: 1.106.3, 1.105.4, 1.104.6, 1.103.8 (Oct 24) 1.83.9 covers both 1.100.4, 1.101.3, 1.102.2, 1.103.1, 1.104.0-rc.2
Who should care Anyone who installed or ran it in the window Proxy operators who hand keys to other users, old pinned installs Multi-user proxy operators on 1.87 to 1.103.0

Issue A: the malicious 1.107.0 release

The attacker published seven 1.107.0 wheels to PyPI between 11:39:51 and 11:40:13 UTC on Oct 21. The wheels used maturin's data = "cook" setting to place three files in the root of site-packages, outside the litellm package folder: cook.pth (12 bytes, one line: import cook), cook.py (22,252 bytes) and sitecustomize.py (25,509 bytes).

The stealer has 16 modules. It goes after browser credentials, SSH and GPG keys, shell history, AWS, GCP and Azure credentials, cloud metadata, Kubernetes, Docker, Vault, CI secrets, .env files, .npmrc, .pypirc and .git-credentials, plus AI tool configs including ~/.claude, ~/.cursor, ~/.ollama and Hugging Face tokens. On a LiteLLM proxy it also reads LiteLLM_CredentialsTable and LiteLLM_ProxyModelTable, the tables that hold your provider keys. It encrypts the bundle (AES-256-CBC, key wrapped with RSA-4096) and POSTs it to www[.]litellm[.]dev/healthcheck. The sitecustomize.py part checks a GitHub dead drop every hour for a second-stage address. Wiz and the issue thread report that the dead drop only ever held test values.

Wiz then saw stolen AWS instance-role credentials validated from two IPs between 16:20 and 17:01 UTC, first sts:GetCallerIdentity, then bedrock:ListFoundationModels. That is someone checking whether the stolen role can run models on your bill.

How it got published comes from LiteLLM's root cause write-up of Oct 23. An Agent Platform master key was committed to a public LiteLLM-Labs repo on May 27 and force-pushed over, but never revoked. On Oct 10 the attacker used it to get an org owner's GitHub CLI token, then ran throwaway CI workflows that posted repository secrets to webhook[.]site. On Oct 21 they disabled branch protection on the release repo, pointed the release workflow at a lookalike org (Berri-AI/litellm instead of BerriAI/litellm) and approved the production gate with a second maintainer's token. Release approval had been relaxed that week to "any two engineers". The attacker was inside the org for at least 11 days before the release.

Two early claims have since been corrected. LiteLLM's first update said the tokens leaked through Claude Code response bodies in logs, and suspected "a recently patched vulnerability". The RCA replaced that with the leaked master key, and a maintainer said on Oct 24 that the patched-vulnerability idea was conjecture. So posts that tie the attack to CVE-2026-114612 are wrong. Several sources also said "the v1.107.0 git tag is clean". A maintainer said on Oct 24 that no official v1.107.0 tag exists, and my own git ls-remote on Oct 24 at 10:20 PM PT showed only v1.107.0-dev.1 to -dev.3 and -rc.1/-rc.2.

This is the second time this year. In March 2026, litellm 1.82.7 and 1.82.8 were malicious too, also using a .pth file.

Issue B: Check Point's CVE-2026-35029 and CVE-2026-59823

Check Point published its write-up on Oct 13, after testing v1.77.7. CVE-2026-35029 is a missing admin check on POST /config/update: any authenticated user could change proxy config and environment variables, read server files and reach code execution. CVE-2026-59823 lets a caller with a valid virtual key hide an api_base inside user_config and send the proxy's outbound request (with your provider key) to a host they choose.

The sources disagree on one number. The CVE record says 35029 was fixed in 1.83.0, and Check Point says both are resolved as of 1.83.9. Being on 1.83.9 or later covers both. Both bugs need an authenticated user, so they matter most if you hand proxy keys to other people.

Issue C: CVE-2026-114612, CVSS 9.9

Published Oct 22 at 18:01 UTC as GHSA-7hp6-4w63-5g45. The proxy used one cryptographic context for stored secrets and for session tokens. An internal user could get the API-key flow to encrypt a forged admin credential, replay it as a bearer token and get admin access, including command execution through the MCP stdio endpoint.

The version boundary trips people up, and it tripped me. Versions 1.87.0 through 1.90.x are exploitable only with EXPERIMENTAL_UI_LOGIN enabled, while 1.91.0 and later are exploitable in the default config. So 1.91.0 is where default exploitability starts, and it is not the patch line. The fixes are 1.100.4, 1.101.3, 1.102.2, 1.103.1 and 1.104.0-rc.2. My first audit rule flagged everything at 1.91.0 or above, which marked the safe 1.106.3 as vulnerable. More on that below.

How a .pth file runs without an import

Python reads every .pth file in site-packages when the interpreter starts, and any line that begins with import is executed. That happens before your code runs, whether or not anything imports litellm. Microsoft's analysis describes a routine pip install of an unrelated package setting off the collector.

I wanted to see that on my own VM, without going near the real package. Built in my home lab: Proxmox VM 105, Ubuntu 26.04.1 LTS, Python 3.14.4. I made a throwaway venv and dropped in a benign stand-in: zzz_canary.pth with the single line import canary_hook, a canary_hook.py that appends a timestamped line to a local log, and a sitecustomize.py that does the same. No network, no data read.

Terminal render: the canary .pth hook logs a line on python -c, pip --version, pip list and pip install six, and stays silent only with python -S
The benign canary fired on every Python start, including pip installing an unrelated package.

Each trigger was a separate interpreter process:

Trigger Hook fired?
python --version no (exits before site setup)
python -c "print(1)", no litellm import yes
pip --version yes
pip list yes
pip install six, an unrelated package yes
python -c "import six" yes
python -E -c (ignore environment) yes
python -S -c (site disabled) no

Here is the raw log for the pip cases:

2026-10-25T05:25:55Z pth-import pid=1086317 argv=['-m', '--version']
2026-10-25T05:25:56Z pth-import pid=1086318 argv=['-m', 'list']
2026-10-25T05:25:56Z pth-import pid=1086319 argv=['-m', 'install', 'six']

That is why pip show litellm is a bad first check on a suspect box, even though LiteLLM's incident post suggests it. The same post also says any Python start in an affected environment runs cook.pth again, pip included, and Microsoft says not to start Python or pip in a suspect environment. Check from the shell.

One more result surprised me. My sitecustomize.py in site-packages never fired. Ubuntu ships its own /usr/lib/python3.14/sitecustomize.py, which sits earlier on sys.path and wins. Python only imports one sitecustomize, so on a stock Ubuntu box the .pth is the dependable path. That fits why the attackers shipped both: the .pth for distros like mine, sitecustomize.py as the fallback where the distro has none.

The audit script I ran on three targets

I wrote a read-only audit that needs no network and does not import anything from the environment it checks. It looks for six things:

  1. litellm versions on disk (from dist-info folder names), checked against 1.107.0 and the CVE ranges above
  2. .pth files with import lines, and sitecustomize.py or usercustomize.py outside /usr/lib
  3. startup hooks modified in the last 10 days
  4. 1.107.0 files in the pip and uv caches
  5. Docker images on floating tags (latest, main-latest, main-stable, 1.107.0)
  6. requirements or constraints files with an unpinned litellm or no --hash= pins

Results on the lab VM:

Target Flags What they were
Canary venv plus test fixtures 7 all real: the .pth import line, the sitecustomize.py, 2 recent-mtime hooks, 2 real :latest Docker tags, 1 unpinned requirement
Clean venv 2 the VM's real LibreChat :latest images, from my LibreChat setup
Patched venv, litellm 1.106.3 3 version OK, the same 2 Docker tags (after the fix below)

Each run took 1.0 to 1.9 seconds wall clock with a max RSS of about 47 MB. On the patched venv, the first run flagged 1.106.3 for CVE-2026-114612, because my rule said "1.91.0 and up". That was my false positive. I rewrote the check to the exact affected ranges from the advisory, then confirmed it on synthetic version folders: 1.107.0 flagged critical, 1.95.0 flagged as exploitable by default, 1.90.0 flagged as needing EXPERIMENTAL_UI_LOGIN, 1.82.0 flagged for the older CVEs, and 1.103.1 and 1.106.3 passed.

While writing this post I made one change: the version compare now runs as python3 -I -S, so it skips site setup and cannot set off a .pth hook even if the system Python were the infected one. I re-ran the script on my box against synthetic dist-info folders and a test .pth, and it gave the same verdicts with 7 flags.

Two limits. A .pth flag means "go read this file", since some normal packages ship .pth files with an import line too. The malicious one is 12 bytes and says import cook. The version check also does not parse the 1.104.0 release candidates, so check those by hand. Run it as your user for $HOME, and pass other roots (a system site-packages, a Docker volume) as arguments.

#!/usr/bin/env bash
# litellm-audit.sh -- READ-ONLY, NO NETWORK local exposure audit for the Oct 2026 LiteLLM
# supply-chain compromise (malicious 1.107.0) plus known CVE ranges. Defensive only.
# Usage: litellm-audit.sh [ROOT ...]   (default roots: $HOME and common venv locations)
set -u
RECENT_DAYS="${RECENT_DAYS:-10}"
ROOTS=("$@"); [ ${#ROOTS[@]} -eq 0 ] && ROOTS=("$HOME")
RED() { printf "[FLAG] %s\n" "$*"; }
OK()  { printf "[ok]   %s\n" "$*"; }
INFO(){ printf "[info] %s\n" "$*"; }
flags=0

# 1) litellm versions found on disk (dist-info dirs), flag 1.107.0 + CVE ranges
INFO "== 1. litellm installs =="
mapfile -t DI < <(find "${ROOTS[@]}" -maxdepth 8 -type d -name "litellm-*.dist-info" 2>/dev/null)
if [ ${#DI[@]} -eq 0 ]; then OK "no litellm dist-info found under scanned roots"; fi
for d in "${DI[@]}"; do
  ver=$(basename "$d"); ver=${ver#litellm-}; ver=${ver%.dist-info}
  verdict=$(python3 -I -S - "$ver" <<"PYEOF"
import sys
v=sys.argv[1]
def t(x):
    out=[]
    for p in x.split("."):
        n="".join(c for c in p if c.isdigit())
        out.append(int(n) if n else 0)
    return tuple(out+[0]*(3-len(out)))[:3]
tv=t(v)
msgs=[]
if v=="1.107.0": msgs.append("CRITICAL malicious supply-chain release 1.107.0 (quarantined)")
if tv < (1,83,9): msgs.append("<1.83.9: CVE-2026-35029 and CVE-2026-59823 range")
# CVE-2026-114612 (salt-key priv-esc) affected ranges per GHSA-7hp6-4w63-5g45; fixed in 1.100.4/1.101.3/1.102.2/1.103.1
def inrange(lo,hi): return lo<=tv<hi
if inrange((1,87,0),(1,100,4)) or inrange((1,101,0),(1,101,3)) or inrange((1,102,0),(1,102,2)) or inrange((1,103,0),(1,103,1)):
    note="exploitable by default" if tv>=(1,91,0) else "if EXPERIMENTAL_UI_LOGIN set"
    msgs.append("CVE-2026-114612 salt-key priv-esc (%s); patch to 1.103.1+/current" % note)
print("|".join(msgs) if msgs else "OK")
PYEOF
)
  if [ "$verdict" = "OK" ]; then OK "litellm $ver at $d"; else RED "litellm $ver at $d -> $verdict"; flags=$((flags+1)); fi
done

# 2) unexpected .pth files + sitecustomize/usercustomize; .pth lines starting with import
INFO "== 2. startup hooks (.pth / sitecustomize / usercustomize) =="
mapfile -t PTH < <(find "${ROOTS[@]}" -maxdepth 8 -type f -name "*.pth" 2>/dev/null)
for f in "${PTH[@]}"; do
  if grep -qE "^[[:space:]]*import[[:space:]]" "$f" 2>/dev/null; then
    RED ".pth executes code (import line): $f"; grep -nE "^[[:space:]]*import" "$f" | sed "s/^/        /"; flags=$((flags+1))
  fi
done
[ ${#PTH[@]} -gt 0 ] && INFO "scanned ${#PTH[@]} .pth files"
mapfile -t SC < <(find "${ROOTS[@]}" -maxdepth 8 -type f \( -name sitecustomize.py -o -name usercustomize.py \) 2>/dev/null | grep -v "/usr/lib/")
for f in "${SC[@]}"; do RED "sitecustomize/usercustomize in a package dir: $f"; flags=$((flags+1)); done
[ ${#SC[@]} -eq 0 ] && OK "no sitecustomize/usercustomize outside the distro"

# 3) recently modified startup hooks
INFO "== 3. startup hooks modified in last ${RECENT_DAYS} days =="
mapfile -t REC < <(find "${ROOTS[@]}" -maxdepth 8 -type f \( -name "*.pth" -o -name sitecustomize.py -o -name usercustomize.py \) -mtime -"${RECENT_DAYS}" 2>/dev/null | grep -v "/usr/lib/")
if [ ${#REC[@]} -eq 0 ]; then OK "none modified recently"; else for f in "${REC[@]}"; do RED "recently modified: $f ($(date -r "$f" +%FT%T))"; flags=$((flags+1)); done; fi

# 4) pip / uv caches holding 1.107.0
INFO "== 4. pip/uv caches for 1.107.0 artifacts =="
CACHES=("$HOME/.cache/pip" "$HOME/.cache/uv" "$HOME/.cache/wheel")
hit=0
for c in "${CACHES[@]}"; do
  [ -d "$c" ] || continue
  mapfile -t M < <(find "$c" -type f \( -name "*litellm*1.107.0*" -o -name "*1.107.0*litellm*" \) 2>/dev/null)
  for m in "${M[@]}"; do RED "cached 1.107.0 artifact: $m"; flags=$((flags+1)); hit=1; done
done
[ $hit -eq 0 ] && OK "no 1.107.0 artifacts in pip/uv caches"

# 5) docker images with floating / malicious tags
INFO "== 5. docker images (floating tags) =="
if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then DCK="docker"; elif sudo -n docker info >/dev/null 2>&1; then DCK="sudo docker"; else DCK=""; fi
if [ -z "$DCK" ]; then INFO "docker not accessible, skipping image check"; else
  while read -r img; do
    case "$img" in
      *:latest|*:main-latest|*:main-stable|*:1.107.0|*:v1.107.0|*:stable)
        RED "floating/at-risk image tag: $img (pin by @sha256 digest)"; flags=$((flags+1));;
    esac
  done < <($DCK images --format "{{.Repository}}:{{.Tag}}" 2>/dev/null | grep -iE "litellm|open-webui|librechat|crawl4ai|ghcr|registry")
  OK "docker image tag scan done"
fi

# 6) requirements / lock files without pins or hashes
INFO "== 6. requirements/lock files without pins or hashes =="
mapfile -t REQ < <(find "${ROOTS[@]}" -maxdepth 6 -type f \( -name "requirements*.txt" -o -name "constraints*.txt" \) 2>/dev/null)
for f in "${REQ[@]}"; do
  haship=$(grep -c -- "--hash=" "$f" 2>/dev/null)
  mapfile -t loose < <(grep -nE "(^|[[:space:]])litellm([<>!~ ]|$)|litellm>=|litellm~=" "$f" 2>/dev/null | grep -vE "litellm==")
  if [ "${#loose[@]}" -gt 0 ]; then RED "unpinned litellm in $f:"; printf "        %s\n" "${loose[@]}"; flags=$((flags+1)); fi
  if [ "$haship" -eq 0 ] && grep -qiE "litellm" "$f" 2>/dev/null; then INFO "no --hash= pins in $f (consider pip --require-hashes)"; fi
done
[ ${#REQ[@]} -eq 0 ] && OK "no requirements/constraints files found"

echo
echo "AUDIT COMPLETE: $flags flag(s)"
exit 0

To check containers without starting them, compare the image you ran with the malicious index digest LiteLLM published (it starts 1bbcb144 and ends 339e):

docker image ls --digests | grep -i litellm

If the audit finds 1.107.0

LiteLLM's guidance starts with not running pip. From the shell, delete cook.pth, cook.py, sitecustomize.py and any matching __pycache__/cook*.pyc and sitecustomize*.pyc from every affected site-packages. Kill the detached Python daemon that holds /tmp/remote_code_loader.lock. Then rebuild the venv or image from scratch. LiteLLM says to reinstall pinned to litellm==1.106.2, and the Oct 24 releases are now the current ones.

Here is that rebuild on my lab VM tonight. I made a fresh venv, pinned litellm[proxy]==1.106.3, and started the proxy on 127.0.0.1 with a fake master key, no provider keys and one local Ollama model behind it. The venv had no .pth files at all, the health check answered, and a test chat came back through the proxy. I pinned by version only to keep the demo short. For a real host, use the hash pins and cooldown from the next section.

Terminal render: a fresh venv with litellm 1.106.3, pip show and litellm --version both report 1.106.3, find counts 0 .pth files, the proxy listens only on 127.0.0.1:4000, the liveliness check says I'm alive, /v1/models lists gemma4-local and a test chat returns proxy ok
A clean rebuild on my lab VM: pinned 1.106.3, zero .pth files, localhost only, fake key.

The proxy's built-in API page shows the same version, which is a quick check if you only have a browser on the host.

Screenshot of the LiteLLM proxy's Swagger page on localhost with the version badge 1.106.3 and links to the admin panel at /ui
The proxy's own API page in my lab, with the 1.106.3 badge.

I shut it down and deleted the venv after taking these.

If 1.107.0 was ever installed on a host with outbound internet, LiteLLM says to assume the exfil worked. The malware writes a marker file only after a successful exfil, so finding it confirms the worst case and not finding it proves nothing. Rotate everything that host could reach: provider keys stored in the proxy database, the master and virtual keys, the DATABASE_URL password, any secret in any process's environment, and any cloud role. On a home lab box I would add SSH keys, the Hugging Face token and anything under ~/.ollama, because the stealer's target list names them.

Hardening that worked in my lab

I tested each control on the same VM. Every one of them would have stopped or contained this attack on its own.

A release-age cooldown

The malicious wheels were on PyPI for about 1 hour 43 minutes. Waiting a day or two before taking new releases skips that window entirely.

With uv 0.13.5, resolving with --exclude-newer 2026-10-20T00:00:00Z picked litellm==1.106.0, the release before the attack:

uv pip compile --exclude-newer 2026-10-20T00:00:00Z requirements.in

pip 26.2.2 has the same idea as --uploaded-prior-to, which takes an ISO date or a duration like P3D. pip 25.1.1 does not have the flag, so use uv there.

pip install --dry-run --uploaded-prior-to 2026-10-20T00:00:00Z litellm
# Would install ... litellm-1.106.0 ...

Hash-pinned installs

With --require-hashes, pip refuses anything that is not pinned with == and carries a hash. A lockfile with hashes would have rejected the unexpected 1.107.0 bytes. This is what pip 25.1.1 printed when I fed it a loose requirement:

ERROR: In --require-hashes mode, all requirements must have their versions pinned with ==. These do not:
    six>=1.0 from https://files.pythonhosted.org/.../six-1.17.0-py2.py3-none-any.whl

Docker digest pinning, with a caveat

Pinning image@sha256:... in compose gets you the exact bytes you tested. These are the digests of two images from earlier posts that run on this VM:

image: unclecode/crawl4ai:0.9.2@sha256:bd36741e7bdd35ddc1a05d9183e1d6d8cefb61dd640d944a25d026b76e917690
image: ghcr.io/open-webui/open-webui:slim@sha256:abd02d41e522d41be389d28d1a73752cd2afc4aa46f980b60d490ecc9379c16b

The caveat matters in this incident. A digest proves the bytes and says nothing about who built them. The malicious images were signed by LiteLLM's own release workflow, so a community analysis in the issue concluded that cosign verify with the old key would have passed. LiteLLM rotated the signing key on Oct 24 (commit e82a3c6), and those releases also removed the eMudhra root CA from their bundles. The same analysis points to the provenance attestation as the check that tells them apart: it names Berri-AI/litellm for 1.107.0 and BerriAI/litellm for clean builds. I have not verified that attestation myself. Pin to a digest you already trust, and treat a valid signature as one signal.

A default-deny egress allowlist

This is the control I'd put first for a self-hosted proxy, and a LiteLLM maintainer in the issue thread recommends default-deny egress for running the proxy too. I used the same approach as my agent sandbox VM: drop all new outbound traffic, log it, and allow only named destinations.

The ruleset I loaded allows loopback, established replies (so inbound SSH keeps working), DHCP, IPv6 neighbor discovery and exactly one destination, 1.1.1.1:443:

# LLMBits litellm lab: default-deny egress ALLOWLIST with logging.
# Allows loopback, established replies (incl. inbound ssh), DHCP, IPv6 ND,
# and exactly ONE allowlisted destination (1.1.1.1:443). Everything else new is dropped+logged.
table inet litellm_lab {
  counter egress_drop {}
  counter egress_allow {}
  chain output {
    type filter hook output priority 0; policy drop;
    oifname "lo" accept
    ct state established,related accept comment "replies incl. inbound ssh"
    udp dport 67 accept comment "keep DHCP lease"
    icmpv6 type { nd-neighbor-solicit, nd-neighbor-advert, nd-router-solicit } accept
    ip daddr 1.1.1.1 tcp dport 443 ct state new counter name egress_allow accept comment "allowlisted host"
    ct state new counter name egress_drop log prefix "LLMLAB-DROP " drop
    log prefix "LLMLAB-DROP-X " drop
  }
}

A small benign script then tried TCP connects to the allowlisted host and to 8.8.8.8:443, standing in for the attacker's server. No data was sent.

Terminal render: with the nftables allowlist loaded, 1.1.1.1:443 connects in 0.03 s, 8.8.8.8:443 is blocked after 6.01 s, counters show 1 allowed and 6 dropped, and the kernel log shows LLMLAB-DROP SYN lines
The phone-home stand-in was dropped and logged at the kernel. The allowlisted host still connected.

With the table loaded, the allowlisted connect took 0.03 s and the other one timed out after 6.01 s. The counters read 1 allowed, 6 dropped: six SYN retries, each logged with its destination and port:

LLMLAB-DROP IN= OUT=eth0 SRC=192.168.88.16 DST=8.8.8.8 ... PROTO=TCP SPT=37014 DPT=443 ... SYN

Two practical notes. This ruleset allows no DNS, which was fine for a test against fixed IPs. A real allowlist needs your resolver plus the provider endpoints your proxy calls. LiteLLM also calls GitHub and docs.litellm.ai by default, and there is no single offline switch yet (issues #46466 and #47910), so expect a few drops in your log on first start and decide on each.

No cloud credentials in the environment

The lab VM had no AWS_, GCP_, AZURE_, OPENAI_, ANTHROPIC_ or LITELLM_MASTER_KEY variables and no ~/.aws or ~/.config/gcloud. A stealer that reads the environment and those folders finds nothing there. The LLMjacking Wiz saw started with an instance role, so a home box without one is a smaller target.

Did another tool pull litellm in for you?

One Hacker News commenter complained that Google ADK, CrewAI and DSPy brought litellm into projects without anyone choosing it. I checked the requires_dist of the latest release of each package on PyPI on Oct 24, 2026. This shows what a fresh install asks for today. An older version you already have may declare something else.

Package (latest) litellm requirement Could an unpinned install have pulled 1.107.0?
crewai 1.15.30 litellm<2,>=1.88.6 (extra litellm) yes, open range
dspy 3.4.1 litellm>=1.65.8 (a normal dependency on Python below 3.15, and in extras) yes, no upper bound
langchain-litellm 0.11.1 litellm<2.0.0,>=1.101.0 yes
agno 3.1.10 litellm>=1.83.0 (extra) yes
smolagents 1.26.0 litellm>=1.60.2 (extra) yes
google-adk 2.13.0 litellm<1.106.3,>=1.84 (extras all, extensions, test) no, capped below
mem0ai 2.2.1 litellm<1.98.0,>=1.83.7 (extra llms) no, capped below
instructor 1.17.0 litellm<=1.83.7 (extra) no, capped below
aider-chat 0.86.2 litellm==1.81.10 no, pinned
openhands-ai 1.11.0 litellm==1.84.1 no, pinned
crawl4ai 0.9.5 unclecode-litellm==1.81.13, a fork with its own package name no, different package
open-webui 0.12.3 none not a direct dependency
hermes-agent 0.19.0 none not a direct dependency
pydantic-ai-slim 2.61.0 none not a direct dependency
browser-use 0.13.11 none not a direct dependency

LibreChat is a Node app, so it has no pip dependency on litellm at all. For the tools from my earlier posts, Open WebUI, LibreChat and Hermes do not pull litellm directly. Crawl4AI uses its own fork, which kept it out of this release. Trusting that fork is now its own question.

The pins cut both ways. aider-chat's 1.81.10 and the instructor cap at 1.83.7 both sit below 1.83.9, the Check Point fix line. Those CVEs are in the proxy server, so they matter if you run the proxy from that environment.

What I did not test

I never installed, downloaded or ran the real 1.107.0, and the canary is a stand-in that writes one local log line. I did not reproduce the Check Point bugs or CVE-2026-114612 against a live proxy, and I chose not to collect status codes from an old vulnerable version. Doing that means standing up a deliberately vulnerable proxy and sending a privilege escalation request, which is exploitation. Against a patched proxy the same request returns a refusal that tells you nothing the version check doesn't. The audit's version check is the defensive substitute. I also did not verify the provenance attestation difference myself.

What is still unknown

  • How many hosts actually sent data out. Nobody has published a number.
  • Whether the second stage ever served code. Every source says it was never observed.
  • What the embargoed vulnerability due for disclosure on Oct 27 is. LiteLLM's incident post only says a fix is coming with it.
  • What process changes LiteLLM will make. They said "next week".
  • How much of the 49,000 download count is mirrors and CI.

Timeline

All times Oct 21, 2026 unless noted. PT is UTC-7.

UTC PT Event
Mar 24 litellm 1.82.7 and 1.82.8 malicious (earlier .pth compromise)
May 27, 18:53 11:53 AM Agent Platform master key committed to a public LiteLLM-Labs repo
Oct 10, 21:27:10 2:27 PM Master key returns an org owner's GitHub CLI token
Oct 10, 22:11:45 3:11 PM First CI workflow posts repository secrets out
Oct 12, 12:08 to 12:30 5:08 to 5:30 AM Second round across private repos
Oct 13 Check Point publishes CVE-2026-35029 and CVE-2026-59823
08:56 to 10:46 1:56 to 3:46 AM 10 commits in the lookalike Berri-AI/litellm
10:08:34 3:08 AM Branch protection on the release repo disabled
10:44:22 3:44 AM Legit 1.106.2 uploaded to PyPI
11:11:18 4:11 AM Production gate approved with a stolen token
11:39:51 4:39 AM Malicious 1.107.0 on PyPI
11:46:56 4:46 AM Poisoned Docker Hub and GHCR images published
12:06:41 5:06 AM GitHub issue #47826 opened by an Upwind researcher
13:20 to 13:23 6:20 to 6:23 AM PyPI quarantine (Wiz says 13:20, LiteLLM 13:23)
13:51 6:51 AM GHCR tags gone
14:20 7:20 AM Docker Hub tags fixed
16:20 to 17:01 9:20 to 10:01 AM Wiz sees stolen AWS credentials used
18:20 11:20 AM LiteLLM incident post
Oct 22, 18:01 11:01 AM CVE-2026-114612 published
Oct 23 Microsoft analysis, Socket timeline, LiteLLM RCA
Oct 24, 07:56 to 09:34 12:56 to 2:34 AM 1.103.8, 1.104.6, 1.105.4, 1.106.3 with a new signing key
Oct 27 Embargoed vulnerability disclosure expected

As of Oct 24, 10 PM PT, PyPI's latest litellm is 1.106.3 and the 1.107.0 page returns 404.

Indicators of compromise (defanged)

These are as published by LiteLLM, Wiz, Microsoft and the GitHub issue. File hashes are shortened here; the full values are in the Wiz and Microsoft posts.

  • Network: litellm[.]dev, POST to www[.]litellm[.]dev/healthcheck, hxxp://<domain>/codepy, raw[.]githubusercontent[.]com/letters9813/pythonclient-test/main/domain.txt, header X-Token: XWiA8gZNa7NcX2XRoAEb, user agent Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 10.0; WOW64; Trident/7.0), webhook[.]site (CI exfil)
  • IPs: 69.48.229[.]143, 199.91.221[.]39 (pipeline), 168.100.8[.]57, 165.227.201[.]8 (use of stolen credentials)
  • Files: cook.pth, cook.py, sitecustomize.py in site-packages, litellm-1.107.0.dist-info, /tmp/.pc_once_72129118c59f5636c0bb, /tmp/remote_code_loader.lock, /tmp/.pc_sq_*, /tmp/.pc_e_*, environment variable PRIVCOLLECT_ACTIVE=1, an orphan python process with cwd /
  • Hashes (Unix / Windows): cook.pth 7f191632...2bdb / 84dc8c82...9d7d, cook.py 9dd0a392...be25 / 03ac2a8b...97ea, sitecustomize.py 34bef04f...05fa / b6122440...8465
  • Image index digests: litellm 1bbcb144...339e, litellm-database c00f9d92c427..., litellm-non_root 7b00e91cd2fe...
  • GitHub: org Berri-AI, author ckdi82fkoos@proton[.]me, branches ci-tmp-a351cc, ci-tmp-be33e9, ci-tmp-4d8517
  • GitHub commits: 20535d8f, 9cf97e9, 285fbdc5, b69bc36, 78d968b, 1a6e3c1
  • Microsoft Defender names: Trojan:PyPI/LiteLlmSupplyChain.A/.B, Backdoor C, Trojan:PyPI/Stealer.SA

What I'd watch next

The Oct 27 disclosure is the next date on the calendar, and I'd patch to whatever LiteLLM ships that day. Then I'd watch the offline-mode issues, because a proxy that phones GitHub on start makes a tight egress allowlist harder to keep. On my own boxes the audit stays in a cron job and in CI, every Python install goes through a cooldown, and the proxy VM keeps its default-deny egress table.

Sources

Official and primary:

Security vendor analysis:

Great! Next, complete checkout for full access to LLMBits.
Welcome back! You've successfully signed in.
You've successfully subscribed to LLMBits.
Success! Your account is fully activated, you now have access to all content.
Success! Your billing info has been updated.
Your billing was not updated.