← 返回
安全技术

删了文件不等于吊销密钥:LiteLLM 1.107.0 是怎么发出带毒版本的

✍️ zhirenhun 📅 2026/11/4 👁 5 阅读 ⏱ 19 分钟
删了文件不等于吊销密钥:LiteLLM 1.107.0 是怎么发出带毒版本的

2026 年 5 月 27 日 18:53 UTC,一个名为 opencode.json 的文件被提交到公开仓库 LiteLLM-Labs/lite-harness。文件里存着 LiteLLM Agent Platform 某内部部署的管理密钥。约三小时后,后续一次提交删除了该文件。用 LiteLLM 自己的话说,“没有人跟进吊销这把管理密钥”(LiteLLM,10 月 23 日)。

到了 10 月 21 日,这把密钥成了整条攻击链的第一环,最终导致 PyPI 上出现恶意版本 litellm==1.107.0,Docker 的 latest 标签也指向了被入侵的镜像(LiteLLM,10 月 21 日)。

下文关于此次事件的全部内容,均来自 LiteLLM 的两份事后复盘、10 月 30 日的后续说明,以及 Wiz 提供的一项下载量估算。

攻击链逐步还原

除特别注明外,所有时间均为 UTC,来源为攻击链复盘。

  1. 5 月 27 日。 opencode.json 被提交。三小时后文件被删除,但这次提交一直留在公开历史中,直到 10 月 22 日,而密钥始终有效。
  2. 10月10日 21:13 至 21:28。攻击者用该密钥向 Agent Platform API 发起已认证请求。21:27:10,一次调用 GET /api/v1/managed_agents/agents?limit=100 返回了所有受管 Agent 的配置。其中一份配置里存着 LiteLLM GitHub 组织两位所有者之一的 GitHub CLI OAuth token。
  3. 这个 token 为何会出现在那里。平台需要开 PR,所以需要一个 GitHub token。结果给的是所有者的 GitHub CLI token,而不是限定到相应仓库的个人访问令牌。事后报告称“GitHub CLI token 没有作用域限制”,攻击者凭它“对我们 GitHub 组织拥有所有者权限”。
  4. 10月10日 22:11 至 22:14。两个临时分支 ci-tmp-a351cc 和 ci-tmp-be33e9 运行工作流,把仓库密钥发送到 webhook.site 端点。窃取内容还包括另外两名维护者的 token。两次运行都在三分钟内被删除。
  5. 10月21日 10:08。分支保护拒绝了一次向私有发布仓库的推送。五秒后,所有者 token 还是把这次推送推了上去。改动给发布工作流加了一个 source_owner 输入,使其可以从任意组织构建。
  6. 10:46 至 11:11。预发布环境针对仿冒的 Berri-AI/litellm 运行,而非 BerriAI/litellm。11:11:18,第二个被盗 token 通过了生产门禁。LiteLLM 此前刚放宽该门禁,允许“任意两名工程师”触发并批准发布。
  7. 11:39 至 11:47。七个恶意 wheel 包被上传到 PyPI。镜像以 v1.107.0、latest、main-latest 和 main-stable 为标签推送到 Docker Hub 和 GHCR(LiteLLM,10月21日)。
  8. 11:53 起。第三个被盗 token 删除了工作流运行记录。PyPI 在 13:23 前已移除该发布版本,Docker Hub 上最后几个标签也在 14:20 重新指向干净镜像(LiteLLM,10月21日)。

被植入恶意代码的构建产物在线存活了“约两小时”(LiteLLM,10月23日)。该版本“包含编译后的恶意软件,会窃取凭据并加载远程代码执行工具”,而 GitHub 上官方 v1.107.0 源码标签从未包含这些恶意文件(LiteLLM,10月21日)。Wiz 引用 ClickPy 数据称,该时间段内 1.107.0 的下载量约为 49,000 次(Security Boulevard)。

LiteLLM 的第一篇公告把 token 来源搞错了。当时怀疑 token 来自某内部部署的日志。10月23日的公告称,请求日志显示第一个 token 来自另一个服务,即第 2 步中的 Agent Platform(LiteLLM,10月23日)。上文所述步骤依据的是更正后的说法。


从5月一把未吊销的密钥,到10月一个被投毒的发布版本。

为什么删掉文件毫无作用

删除文件的提交只是新增了一个快照。包含密钥的旧快照仍可通过其哈希值,从期间产生的每个克隆和 fork 中访问到。GitHub 关于移除敏感数据的指南列出了即使重写历史并强制推送后旧提交仍然存活的去处:克隆、fork、可通过 SHA 访问的缓存视图,以及引用该提交的 pull request。

正因如此,同一份指南把一步放在最前面:吊销和/或轮换密钥。指南指出,一旦密钥失效,重写历史“可能就没有必要了”。已吊销的密钥留在公开历史中无害。而文件被删、密钥却仍有效,那它依然是一把有效的密钥——这一把足足有效了四个多月。

编码 Agent 让这种模式更难被发现。一次 Agent 会话可能在一轮对话里新增配置文件,又在后续某一轮把它删掉。PR 的最终 diff 会把这两次改动抵消掉,所以只看最终 diff 的审查者根本看不到那个密钥。LiteLLM 没有说明 5 月那次提交是谁、以什么方式做出的,本文也不假设是 Agent 所为。关键在于,无论哪种情况,只审查最终 diff 都会漏掉它。

别把密钥写进文件

opencode.json 是 OpenCode 的项目配置文件。OpenCode 会从当前目录开始向上查找,直到最近的 git 根目录,所以它按设计就放在代码旁边(OpenCode 文档)。同一份文档也给出了把密钥排除在文件之外的办法:{env:VAR} 和 {file:path} 替换。下面是文档中的示例:

{
  "$schema": "https://opencode.ai/config.json",
  "model": "{env:OPENCODE_MODEL}",
  "provider": {
    "anthropic": {
      "models": {},
      "options": {
        "apiKey": "{env:ANTHROPIC_API_KEY}"
      }
    }
  }
}

这个文件可以提交、审查和做 diff。密钥放在环境变量里,或者放在 ~ 下某个永远不会进入仓库的文件中。其他 Agent 的配置文件各有各的引用密钥方式;在 Agent 替你写入明文值之前,先查清楚你的配置该怎么写。

先在历史记录里找到它,然后第一时间吊销

git log 可以搜索每一个提交中的字符串。-S 查找的是改变某字符串出现次数的提交,因此既能找到添加密钥的提交,也能找到删除密钥的提交。-G 用正则匹配新增或删除的行。-p 打印补丁,--all 则遍历本地所有 ref,而不只是当前分支。

# 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

把 sk- 换成你的服务商使用的前缀。最后一条命令才能暴露出 LiteLLM 那种情况:配置文件存在于历史记录中,却不在当前文件树里。

一旦发现问题,处理顺序如下:

  1. 先在服务商处吊销或轮换密钥。这一步要放在最前面。
  2. 查看服务商日志,确认该密钥自提交日期以来是否被使用过。
  3. 然后才决定是否重写历史,同时要清楚克隆和分叉仍会保留旧提交(GitHub)。

LiteLLM 在 10 月 30 日的文章中把第一步变成了流程:“密钥扫描必须自动创建工单,只有确认吊销后工单才能关闭”(LiteLLM,10 月 30 日)。


删除文件只是多加了一层。密钥仍然在下面那一层里。

限定 Agent 会话能看到的 gh token 范围

链条中的第三步,正是泄露的平台密钥演变成 GitHub 组织控制权的地方。一个只需要创建拉取请求的服务,却持有着组织所有者的 CLI 登录凭证。

同样的情况在笔记本电脑上以更小的规模存在。终端里的编码 Agent 以你的身份运行。如果你用 gh 登录过,Agent 就能运行 gh auth token,它会“输出你账户的认证令牌”。而如果没有可用的凭据存储,gh 会退而把令牌写入一个纯文本文件(gh 文档)。

gh 会优先读取 GH_TOKEN:gh 环境变量文档称它“优先于此前存储的凭据”。因此,对于 Agent 会话,应导出一个细粒度个人访问令牌,限定到单个仓库,只授予任务所需的权限,并设置过期日期:

# in the shell you start the agent from
export GH_TOKEN="$(cat ~/.config/gh-agent/app-repo.token)"
gh auth status

有两点需要注意。这一改动改变的是 gh 使用哪个 token,而不是以你的身份运行的进程能读到什么;沙箱或独立的操作系统用户才是更强的边界。另外,对于代表组织执行操作的自动化,GitHub 建议使用 GitHub App,而不是任何个人 token(GitHub)。

LiteLLM 也得出了同样的结论。“自动化将以 GitHub App 身份认证,或使用权限范围最小、会过期的细粒度 token,”而在日常工作中运行 Agent 的人不会获得 owner 或 admin 角色(LiteLLM,10 月 30 日)。同一篇文章还提到,三月之后加入的发布管控按设计正常工作,但无法完全阻止这一次事件,并直言原因:“任何流水线管控都挡不住身为组织 owner 的攻击者,因为 owner 拥有整条流水线。”

Hermes 适合什么,不适合什么

我是 Hermes IDE 的开发者,请带着这一点来看下面的内容。Hermes 运行 Claude Code、Codex、OpenCode 等 Agent,每个都在自己的终端和自己的 worktree 中运行。Hermes 从不索取 token:Agent 使用各自 CLI 的登录方式(README)。这也意味着它不会限制 Agent 能访问的范围。上面那步设置 GH_TOKEN 仍然需要你自己完成。

它改变的是审查方式。Agent 的每一轮操作都会被保存,Review Desk(⌘G,Windows 和 Linux 上为 Ctrl+Shift+G)可以按轮次或按文件查看改动,并带有风险标记(README)。前面那种先添加再删除的模式在这里不会相互抵消:写入配置文件的那一轮会显示自己的 diff,即使后面某一轮把该文件删掉了。


orders-api 中一个 Claude Code 任务的 Review Desk(⌘G)按轮次视图:列出两轮,第 1 轮 src/db.ts 的 diff 处于展开状态,还有改动文件列表,以及根据这些轮次起草的提交信息。

如果某一轮对话像这样把密钥暴露出来,审查就尽到了职责,下一步仍然是去服务商那边点撤销。事后删掉文件,就是五月份那套做法。

Hermes 免费、源码开放(BSL 1.1),支持 macOS、Windows 和 Linux,不隶属于任何 Agent 厂商。下载试用,或在 GitHub 上阅读代码。


如果这篇文章对你有用

LiteLLM 这条链路既是安全故事,也是设计故事:一份凭证的触达范围远远超出了持有它的服务所需,而它之后的每一道关卡都信任这份凭证。System Design Pocket Guide: Fundamentals 是我写的一本小册子,讲这类系统的核心构件。它是八本 Pocket Guide 之一,涵盖 Agent、RAG、提示词、可观测性、事件、数据库和系统设计:整个系列在这里。

——

广告 广告
🧑‍💻

zhirenhun

一个热爱技术的程序员,喜欢分享前沿AI知识和开发经验。

security git github ai