刪除檔案未能撤銷金鑰:LiteLLM 1.107.0 如何被植入惡意軟件並發佈
Deleting the File Didn't Revoke the Key: How LiteLLM 1.107.0 Shipped Malware
LiteLLM 的 litellm==1.107.0 套件及 Docker 映像於 10 月 21 日遭攻擊者植入惡意軟件並發佈。攻擊始於 5 月 27 日公開 Git 歷史中的管理金鑰;檔案雖約三小時後刪除,金鑰仍有效逾四個月,受感染產物約公開兩小時,Wiz 估計該時段有約 49,000 次下載。
文章梳理未撤銷管理金鑰如何一路導致套件遭植入惡意軟件,並整理先撤銷憑證、限制自動化權限等防護做法。
- Book: System Design Pocket Guide: Fundamentals
- The series: Pocket Guides for Developers — 8 short books on agents, RAG, prompts, observability, events, databases and system design — the whole series
- My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools
- Me: xgabriel.com | GitHub
On May 27, 2026, at 18:53 UTC, a file called opencode.json landed in the public LiteLLM-Labs/lite-harness repository. It held an administrative key for an internal deployment of the LiteLLM Agent Platform. About three hours later, a follow-up commit deleted the file. In LiteLLM's own words, "nobody followed through on revoking the administrative key" (LiteLLM, Oct 23).
On October 21, that key turned out to be the first link in a chain that ended with a malicious litellm==1.107.0 on PyPI and the Docker latest tag pointing at a compromised image (LiteLLM, Oct 21).
Everything about the incident below comes from LiteLLM's two postmortems and its October 30 follow-up, plus one download estimate from Wiz.
The chain, step by step
All times are UTC, from the attack-chain postmortem unless marked otherwise.
-
May 27. The
opencode.jsoncommit. The file is deleted three hours later, but the commit stays in public history until October 22, and the key stays valid. -
October 10, 21:13 to 21:28. The attacker makes authenticated requests to the Agent Platform API with that key. At 21:27:10, one call,
GET /api/v1/managed_agents/agents?limit=100, returns the configuration of every managed agent. One of those configs holds the GitHub CLI OAuth token of one of the two owners of LiteLLM's GitHub organization. - Why that token was there. The platform opened pull requests, so it needed a GitHub token. It was given the owner's GitHub CLI token instead of a personal access token scoped to the right repositories. The postmortem says "GitHub CLI tokens are not scoped," and that with this one the attacker "had owner capabilities over our GitHub organization."
-
October 10, 22:11 to 22:14. Two throwaway branches,
ci-tmp-a351ccandci-tmp-be33e9, run workflows that post repository secrets to awebhook.siteendpoint. The haul includes tokens belonging to two more maintainers. Both runs are deleted within three minutes. -
October 21, 10:08. Branch protection rejects a push to the private release repository. Five seconds later the owner token pushes it anyway. The change adds a
source_ownerinput to the release workflow, so it can build from any organization. -
10:46 to 11:11. Staging runs against the lookalike
Berri-AI/litellm, notBerriAI/litellm. At 11:11:18 a second stolen token approves the production gate. LiteLLM had recently relaxed that gate so "any two engineers" could trigger and approve a release. -
11:39 to 11:47. Seven malicious wheels go to PyPI. Images go to Docker Hub and GHCR under
v1.107.0,latest,main-latestandmain-stable(LiteLLM, Oct 21). - 11:53 onward. A third stolen token deletes the workflow run records. PyPI has removed the release by 13:23, and the last Docker Hub tags are repointed to clean images at 14:20 (LiteLLM, Oct 21).
The compromised artifacts were live for "about two hours" (LiteLLM, Oct 23). The release "contained compiled malware which steals credentials and loads remote-code execution tools," and the official v1.107.0 source tag on GitHub never contained the malicious files (LiteLLM, Oct 21). Wiz, citing ClickPy data, put downloads of 1.107.0 at about 49,000 in that window (Security Boulevard).
LiteLLM's first post got the token source wrong. It suspected the tokens came from the logs of an internal deployment. The October 23 post says the request logs show the first token came from a different service, the Agent Platform in step 2 (LiteLLM, Oct 23). The steps above follow the corrected account.

From one unrevoked key in May to a poisoned release in October.
Why deleting the file did nothing
A commit that deletes a file adds a new snapshot. The old snapshot, with the key in it, is still reachable by its hash from every clone and fork made in between. GitHub's guide to removing sensitive data lists where an old commit survives even after you rewrite history and force-push: clones, forks, cached views reachable by SHA, and pull requests that reference it.
That is why the same guide puts one step first: revoke and/or rotate the secret. Once the key is dead, it says, rewriting history "may not be warranted." A revoked key in public history is harmless. A live key whose file was deleted is still a live key, and this one stayed live for more than four months.
Coding agents make the pattern easier to miss. An agent session can add a config file in one turn and remove it in a later one. The pull request's final diff nets the two out, so a reviewer who reads only that diff never sees the key. LiteLLM doesn't say who or what made the May commit, and this post doesn't assume an agent did. The point is that a review of the final diff would have missed it either way.
Keep the key out of the file
opencode.json is the project config for OpenCode. OpenCode looks for it in the current directory and walks up to the nearest git root, so it sits next to your code by design (OpenCode docs). The same docs give you a way to keep the key out of it: {env:VAR} and {file:path} substitution. This is their example:
{
"$schema": "https://opencode.ai/config.json",
"model": "{env:OPENCODE_MODEL}",
"provider": {
"anthropic": {
"models": {},
"options": {
"apiKey": "{env:ANTHROPIC_API_KEY}"
}
}
}
}
That file can be committed, reviewed and diffed. The key lives in your environment, or in a file under ~ that never enters the repo. Other agents' config files have their own ways to reference a secret; check yours before an agent writes the literal value in for you.
Find it in your history, then revoke first
git log can search every commit for a string. -S finds commits that change the number of times a string appears, so it catches both the commit that added a key and the one that removed it. -G matches added or removed lines against a regex. -p prints the patch, and --all walks every ref you have locally, not only the current branch.
# commits that added or removed the string, any ref
git log --all --oneline -S 'sk-'
# same commits, with the lines themselves
git log --all -p -S 'sk-'
# looser: added or removed lines matching a regex
git log --all -p -G '(api|API)_?(key|KEY)'
# files that were deleted, and in which commit
git log --all --diff-filter=D --name-only --oneline
Swap sk- for the prefix your provider uses. The last command is the one that surfaces the LiteLLM shape: a config file that exists in history but not in the tree.
When something turns up, the order is:
- Revoke or rotate the key at the provider. Before anything else.
- Check the provider's logs for use of that key since the commit date.
- Only then decide whether to rewrite history, knowing clones and forks keep the old commit anyway (GitHub).
LiteLLM's October 30 post turns step 1 into process: "Secret scanning must open tickets automatically, and the ticket is only closed when revocation is confirmed" (LiteLLM, Oct 30).

Deleting the file adds a layer. The key is still in the one below.
Scope the gh token your agent session can see
Step 3 of the chain is where a leaked platform key became control of a GitHub organization. A service that only needed to open pull requests was holding an owner's CLI login.
The same shape exists on a laptop at a smaller scale. A coding agent in your terminal runs as you. If you are logged in with gh, the agent can run gh auth token, which "outputs the authentication token" for your account. And if no credential store is available, gh falls back to writing the token to a plain text file (gh docs).
gh reads GH_TOKEN first: the gh environment docs say it "takes precedence over previously stored credentials." So for an agent session, export a fine-grained personal access token limited to one repository, with only the permissions the task needs and an expiry date:
# in the shell you start the agent from
export GH_TOKEN="$(cat ~/.config/gh-agent/app-repo.token)"
gh auth status
Two caveats. This changes which token gh uses, not what a process running as you can read; a sandbox or a separate OS user is the stronger boundary. And for automations that act for an organization, GitHub recommends a GitHub App over any personal token (GitHub).
LiteLLM landed on the same rules. "Automations will authenticate as a GitHub App or with a fine-grained, expiring token with minimal scopes," and people who run agents in their daily work don't get owner or admin roles (LiteLLM, Oct 30). The same post says the release controls added after March worked as designed but couldn't fully prevent this one, and is blunt about why: "no pipeline controls can defend against an attacker who is an organization owner, because the owner owns the pipeline."
Where Hermes fits, and where it doesn't
I build Hermes IDE, so weigh this with that in mind. Hermes runs Claude Code, Codex, OpenCode and other agents, each in its own terminal and its own worktree. Hermes never asks for tokens: agents use their own CLI sign-in (README). That also means it doesn't scope what an agent can reach. The GH_TOKEN step above is still yours to do.
What it changes is the review. Every agent turn is saved, and the Review Desk (⌘G, or Ctrl+Shift+G on Windows and Linux) shows changes by turn or by file, with risk flags (README). The add-then-delete pattern from earlier doesn't net out there: the turn that wrote the config file shows its own diff, even if a later turn removed the file.

The Review Desk (⌘G) in By turn view for a Claude Code task in orders-api: two turns listed, turn 1's diff of src/db.ts open, the changed-files list, and a commit message drafted from the turns.
If a turn like that shows a key, the review has done its job, and the next step is still the provider's revoke button. Deleting the file afterwards is what happened in May.
Hermes is free, source-available (BSL 1.1), runs on macOS, Windows and Linux, and isn't affiliated with any agent vendor. Download it or read the code on GitHub.
If this was useful
The LiteLLM chain is a design story as much as a security one: one credential reached much further than the service holding it ever needed, and every gate after it trusted that credential. System Design Pocket Guide: Fundamentals is my short read on the core building blocks those systems are made of. It's one of eight short Pocket Guides, covering agents, RAG, prompts, observability, events, databases and system design: the whole series is here.
來源:DEV Community · dev.to
