SecurityWeek put the date on the package: October 21, LiteLLM 1.107.0, PyPI and Docker. Attackers used compromised GitHub CLI OAuth tokens, disabled branch protections, pointed the releaser at a different source repository, ran a compromised workflow against a lookalike repo, then signed and published the poisoned artifacts. CSO Online says the Python packages were up about two hours before they came down. Docker went later the same day. Two hours is a window. It is not a pin.
We already filed LiteLLM 1.83.9 closing /config/update and pip 26.2.2 dropping emSign while the OS store lagged. This week the named version is 1.107.0. The named clean version is 1.106.2. Do not confuse a CVE-style config patch from October 16 with a stolen-token release on October 21.
1.107.0 is a wheel. The GitHub tag was not.
CryptoTimes on October 22 is the sentence most teams will miss: the official v1.107.0 source tag on GitHub did not contain the malicious files. The compromised packages were built from a lookalike repository named Berri-AI/litellm, not the official BerriAI/litellm. If your install path is pip install litellm==1.107.0, you did not install the tag. You installed whatever the workflow signed.
That is the March lesson with a new door. CSO: earlier this year, malicious LiteLLM Python packages went out after a compromise the project linked to the Trivy scanner in the release pipeline. After that, LiteLLM moved releases to a private repository and introduced signed Docker images. Stolen GitHub credentials then modified the release workflow, which signed and published the malware. A signature says the artifact matches what the workflow signed. It does not say the workflow was still pointed at BerriAI.
SecurityWeek: three sets of developer GitHub CLI credentials were in play; how the attackers got internal access is still unclear. CryptoTimes: one account overrode branch protection and changed the private release workflow, a second approved the production release, a third deleted workflow records. Tokens involved were revoked. The “how” is not a root-cause report yet. Do not wait for the how before you pin.
LiteLLM told SecurityWeek that enterprise customers pulling from its private repository were not affected. If you pull from PyPI or public Docker, you are not that sentence.
Two hours on PyPI. Docker lagged.
CSO’s window is the one to write on the incident ticket: Python packages available approximately two hours, then removed. Docker images withdrawn or replaced later that day. Two hours is long enough for a CI cron. It is long enough for a pip install -U litellm in a laptop bootstrap. It is not long enough to assume “we would have noticed.”
If your lock file says litellm==1.107.0, treat the host as burned until you prove it never ran. If your lock file says >=1.100 and the resolver ran on October 21, read the lock. If you have no lock, you do not have a timeline. You have a guess.
SecurityWeek’s clean version is 1.106.2. The Hacker News and CryptoTimes repeat the pin. Pin by digest if you can. A version pin without a hash is how the next lookalike wheel slides in under the same number.
We already wrote the general version of this in pip-audit, SBOMs, and Trusted Publishing. This incident is the worked example. Trusted Publishing and provenance are the controls CSO’s Prabhu wants: cryptographically signed releases backed by verifiable build provenance, plus stronger protection of publishing credentials. “A trusted source repository does not guarantee that the package users install was built from that source.” That sentence is the postmortem title.
privcollect and an hourly GitHub dead drop
The Hacker News quoting StepSecurity: the first payload is a credential stealer that calls itself privcollect. The second forks a background daemon that checks a GitHub dead drop every hour and runs whatever Python the attacker serves. SecurityWeek adds that Wiz saw attackers querying Amazon Bedrock models with stolen AWS credentials, which they flag as LLMjacking, and that the RAT checks a C2 as well as the hourly GitHub drop.
THN’s collection list: cloud credentials, private keys, browser data, authentication files for AI development tools. It also targets LiteLLM deployments to extract the proxy’s saved credentials and model configuration. Data is packed into a GZIP-compressed TAR and POSTed to a host they render as litellm[.]dev/healthcheck. Separately, the malware reads domain.txt from github.com/letters9813, and if that file holds a domain, it runs the stealer and then GETs /codepy and executes the response. THN says that GitHub repo currently 404s.
Affected wheels in that article include manylinux and musllinux aarch64 and x86_64, macOS x86_64 and arm64, and win_amd64, plus a 1.107.0 sdist whose note is tampered pyproject.toml without the payload files. If you installed from sdist, do not use that note as a blessing. You still pulled a tampered project file from a poisoned release.
LiteLLM, per THN, updated public registries to remove trusted links to its GitHub organization so further malicious packages could not ride those links. That is a registry-side brake. It is not your laptop.
Do not start Python to “just check”
CryptoTimes, citing cleanup advice in the coverage: do not run pip first, because starting Python can trigger the malicious files. Delete cook.pth, cook.py, sitecustomize.py, and related cached files from every affected site-packages. Stop the background Python process. Reinstall pinned to litellm==1.106.2, or rebuild images and virtualenvs from scratch. If 1.107.0 ran on a host with outbound internet, assume credentials were stolen and rotate every key and secret that host could reach. SlowMist’s public advice in that piece matches: avoid 1.107.0, rebuild from clean images, pin 1.106.2.
sitecustomize.py and a .pth file are how you get code execution on interpreter start. That is why “open a shell and pip show litellm” is the wrong first move on a dirty venv. Snapshot the disk if you need forensics. Work from a clean machine to rotate keys.
Rotate the obvious: OpenAI, Anthropic, Azure, Bedrock, GCP, GitHub PATs, cloud access keys, browser session cookies if a developer profile lived on the box. LiteLLM’s job is to hold many provider keys behind one proxy. One dirty proxy is many dirty providers. Wiz’s Bedrock query is the example, not the boundary.
Rebuild the image. Do not docker commit a running container after deleting three files. The hourly dead drop means you do not know what else ran. THN’s 404 on letters9813 is current state, not history.
cook.pth is the quiet file. People grep for privcollect and miss the path configuration that reloads it. Delete the pth, the py, sitecustomize, and the pyc next to them. Then destroy the venv anyway. The file list is for the incident timeline, not for a surgical save of your half-working Jupyter kernel.
If the dirty host was a shared GPU box, every user on that box is in the rotation list. LiteLLM’s saved provider keys are one pile. Browser profiles of the people who SSHed in are another. Do not rotate “the LiteLLM key” and leave the AWS key the same human used in a neighboring tmux pane.
Pin 1.106.2 by digest. Then freeze the resolver.
The operational close is boring on purpose. Lock litellm==1.106.2 with a hash. Turn off unattended pip install -U. If your base image had pip install litellm with no pin, that line is the bug. If Dependabot or Renovate opened a 1.107.0 PR, close it and require a human on LiteLLM bumps for a while.
Check CI caches. A poisoned wheel in a cache will outlive the two-hour PyPI window. Purge the pip cache on runners. Purge the Docker layer that ran pip install. If you vendor wheels in an internal index, query that index for 1.107.0 and delete it.
THN’s wheel list is the inventory for that query: cp310-abi3 builds for macOS x86_64 and arm64, manylinux 2_28 aarch64 and x86_64, musllinux 1_2 aarch64 and x86_64, win_amd64, plus litellm-1.107.0.tar.gz. Search the strings, not just the version. An internal mirror that strips platform tags can still hold the sdist. The sdist is the tampered pyproject.toml. That file is enough to make the next build interesting.
If Renovate already merged 1.107.0 on October 21, roll the lock back to 1.106.2 in the same PR series as the key rotation. A revert without rotation is a tidy git history on a dirty cloud account. Wiz’s Bedrock traffic is the proof that the keys were used, not only copied.
SecurityWeek’s “sweep for malware” is the host-level half. The Python-level half is a new venv. Mixing those by deleting files in a dirty venv and calling it done is how sitecustomize comes back from __pycache__.
This is the second LiteLLM supply-chain event in 2026. CSO is blunt that the post-March signing work did not save a pipeline the attacker could edit. March was the scanner. October is the OAuth token. Different door, same publish path. If you are still taking LiteLLM’s Docker tag as a trust root, you are trusting the workflow that tags it. Pin the digest of 1.106.2 that you pulled from a time before October 21, or rebuild from the official source tag that CryptoTimes says was clean, and then hash that.
Branch protection that a stolen CLI token can disable is not branch protection. The CryptoTimes sequence (override, approve, delete logs) is a three-person ceremony performed by stolen credentials. If your org still stores gh tokens on developer laptops with repo admin, this write-up is about you even if you never imported LiteLLM. The proxy was the distribution. The tokens were the bug.
1.107.0 lasted two hours. Your lock file lasts until you edit it. Edit it.
If you never installed 1.107.0, still pin 1.106.2 before the next unattended upgrade. The lookalike org name is the kind of string a tired reviewer misses in a lockfile diff. Write the allowed index URL in pip.conf. Write the hash. Then go back to the 1.83.9 config article if you still run a proxy old enough to have /config/update open, because that hole did not close itself while you were reading about wheels.
Docker users: CSO said images were withdrawn or replaced later than the PyPI takedown. If your node pulled litellm:1.107.0 after the PyPI window closed, you can still be dirty. docker images and the digest, not the tag. Tags move. Digests do not, until you delete them.
Discussion
Leave a comment
No comments yet
Be the first to start the conversation.