On October 21, a malicious litellm 1.107.0 reached PyPI and LiteLLM's official Docker images. LiteLLM says its own release pipeline was compromised. The release carried compiled malware that steals credentials and loads remote code.
Here is the detail that should worry every security team. LiteLLM says the official v1.107.0 source tag does not contain the malicious files. Anyone reviewing the code that morning saw a clean project.
The LiteLLM 1.107.0 attack, hour by hour
The attacker used developer credentials that LiteLLM believes were stolen GitHub CLI OAuth tokens. Times are UTC on October 21 and published analyses of the release.
Time (UTC) | Event |
Oct 13, 00:45 | Lookalike domain |
08:56 to 10:46 | Attacker commits pushed to |
10:46 | A modified workflow runs a staging build from that repository |
11:11 | A second developer credential passes the production approval gate |
11:39 to 11:40 | Seven malicious wheels uploaded to PyPI |
11:46 | Docker tags |
12:06 | Compromise reported in issue #47826 |
13:23 | PyPI removes 1.107.0 |
14:20 | Docker tags pointed back at clean sources |
That is about 1 hour 44 minutes on PyPI and 2 hours 34 minutes on Docker. The attacker staged infrastructure a week early and passed the project's own approval gate. Every step went through LiteLLM's real release path.
What the LiteLLM 1.107.0 payload does
The malicious code shipped as compiled files inside all seven wheels. The source distribution carried a tampered pyproject.toml but no payload files.
- Runs at every Python start. Startup hooks named
cook.pthandsitecustomize.pylaunch the payload whenever Python starts. Nothing has to import LiteLLM. - Takes the keys. It collects LLM provider API keys, cloud credentials, environment variables and the credentials stored in a LiteLLM proxy's own database.
- Sends them out. Stolen data goes to
litellm[.]dev, a lookalike domain. - Leaves a door open. . A background loader checks a GitHub dead drop every hour for remote code to run.
The second time this year
In March, litellm 1.82.7 and 1.82.8 hit PyPI through a stolen publishing token, using a .pth startup hook of the same kind. PyPI measured that window at 2 hours 32 minutes. Researchers say October used different tooling and infrastructure.
The lesson repeats anyway. The code you review and the code you install are two different files, and only the second one runs.
Why reviewing the code missed it
Code review, pull requests and repository scans all read source. The v1.107.0 tag holds no payload, so every source check passed.
Provenance had the same blind spot. This build came out of LiteLLM's own pipeline, approved by a real developer credential. Signed provenance tells you who built a package, and on October 21 the who looked right.
Identity, integrity and intent
Before a package enters a build, three questions decide whether it deserves trust. Each one catches something the other two miss.
Identity: is the publisher who it claims to be?
Identity asks whether today's release matches the history that earned the package its reputation. Signals include account changes, unusual workflows and a release that breaks the usual pattern.
October is the hard case for identity. The release used real credentials and passed a real approval gate, so identity alone could not settle it.
Integrity: does the artifact match its source?
Integrity compares the shipped artifact with the source it claims to come from. It also watches for new files, new dependencies or a jump in size.
October fails this check clearly. The wheels carried startup hooks and compiled files that the v1.107.0 tag does not contain. Integrity needs no understanding of the payload, because it is a comparison. A mismatch is a mismatch.
Intent: does the code do what the package is for?
Intent compares what the code does with what the package says it does. A library that routes LLM requests has no reason to install hooks that run at every Python start, read cloud credentials and call out to an unfamiliar domain.
No single check is enough. In October, identity looked normal because the attacker used real credentials. Integrity and intent both raised a flag, and two independent warnings on the same release are a strong signal.
How CleanStart checks what the registry ships
CleanStart's Tricorder system treats every new version as untrusted until checks on the shipped files pass. It works in four steps:
- Fetch. A collector watches each registry's feed and downloads the actual package.
- Read every file that runs. For PyPI that means
.pyfiles,setup.py,.pthfiles,sitecustomize.pyand entry points. If the ecosystem's tools will execute a file, Tricorder reads it. - Parse for behavior. Each source file becomes a syntax tree in a common form, so one rule covers the same trick in any supported language.
- Score agreement. Independent checks raise findings, and a correlator scores them higher when they agree on the same release.
Here is what those checks see in 1.107.0.
Question | How CleanStart checks it | What it shows for 1.107.0 |
|---|---|---|
Identity | Compares the release with the package's own publishing history | No clear signal. The release came through LiteLLM's real pipeline, with real credentials and a real approval |
Integrity | Compares the files in each wheel with the source tag the release claims to come from | Startup hooks ( |
Intent | Reads every file Python will run, including | Startup hooks that launch compiled code at every Python start, in a package whose job is routing LLM requests |
Identity coming up empty is the reason to ask three questions. Integrity and intent both flag this release, and the correlator scores that agreement above either finding alone. A release scored that way goes to an analyst before it can be promoted into the verified catalog.
Two habits that would have helped
Pin by digest. On October 21, the Docker tags latest, main-latest and main-stable pointed at the malicious image for 2 hours 34 minutes. Anyone who pulled by tag got the bad build. A digest points to one exact image and can't be moved, so builds pinned by digest kept running the version they already had.
Wait until a release is checked. Fast detection only helps if it finishes before you install. PyPI removed 1.107.0 in under two hours, and unpinned pipelines grabbed it in that time. A safer rule is simple: automation installs a new release only after something has inspected it.
A release published minutes ago hasn't been checked by anyone yet, so its safety is unknown, and unknown should mean wait. CleanStart calls this state not yet assessed and holds those versions out of its catalog until the checks finish.
What to do this week
- Look for 1.107.0. Search images, CI caches and laptops for litellm 1.107.0,
cook.pthand unexpectedsitecustomize.pyfiles. Check egress logs forlitellm[.]dev. - Rotate if it ran. LiteLLM advises treating every secret the host could read as stolen, including provider keys stored in the proxy database.
- Pull by digest. Replace
latestandmain-stablereferences with image digests. - Add a cooldown. Stop automation from installing releases younger than a few days. In uv, as shown in PyPI's guidance:
[tool.uv]exclude-newer = "P3D" # 3 days as an ISO 8601 duration; needs uv 0.9.17 or later



