所有文章
AI

LiteLLM 投毒事件复盘:GitHub 源码是干净的,你装到的包却在偷密钥

LiteLLM 投毒完整复盘:1.107.0 的 wheel 和 Docker 标签怎么带上窃密程序,北京时间逐分钟时间线,偷了什么,以及先 find 再 pip 的自查清理步骤。

传送带上一个封好的包裹,胶带下面夹着一个没有标签的小信封,比喻 LiteLLM 投毒:源码干净,发布出来的包却被塞了东西

2026 年 10 月 21 日晚上,北京时间 19:40 到大约 21:12 之间,如果你敲过一次 pip install -U litellm,装到的就是 1.107.0。表面上一切正常:GitHub 上的 tag 是干净的,发布说明也没毛病,新版本 import 起来不报错。可它的 7 个 wheel 包里都藏着一个窃密程序,只要 Python 一启动就会运行,另外还埋了一个以后能远程拉代码来跑的加载器。这次 LiteLLM 投毒值得认真拆一遍,因为它绕过了大多数人唯一会做的那道检查:「源码我看过,没问题。」

今晚我把官方 GitHub issue #47826连同下面每一条评论都读了,又对照了 AISLE 的分析、JFrog 和 Aikido 的报告,查了 PyPI、Docker 两边的仓库接口,也翻了攻击者自己的提交。下面按时间顺序把经过捋一遍,每条都标了出处,还不知道的就直说不知道。只想赶紧处理的,可以直接跳到自查那一节;精简版也写进了我们的 Claude Code 报错速查。

先说结论

  • 哪些东西有毒:PyPI 上的 litellm==1.107.0(7 个 wheel 全部中招),以及 Docker Hub 和 GHCR 上的官方镜像 v1.107.0、latest、main-latest、main-stable。来源:issue #47826。
  • 它干了什么:往 site-packages 里放 cook.pth、cook.py、sitecustomize.py 三个文件。窃密程序每次启动 Python 都会跑,哪怕你的代码根本没 import litellm,偷到的东西加密后发往 www.litellm.dev。官方域名其实是 litellm.ai。
  • 北京时间 23:18 的状态:PyPI 上 1.107.0 显示为「已隔离」,最新版是 1.106.2。维护者 22:32 说恶意 Docker 镜像已删除。标签确实都没了,但我们实测 Docker Hub 按 digest 还能取到带毒镜像(下面细说)。
  • 还不知道的:攻击者怎么进的 LiteLLM 内部那台部署。发布权限是怎么被拿到的,LiteLLM 已经在北京时间 10 月 22 日 05:22 给出了说法,见下面的更新:LiteLLM 公布的攻击链。

更新:LiteLLM 公布的攻击链(2026-10-22)

本节写于 2026 年 10 月 22 日北京时间 06:56(22:56 UTC)。今天北京时间 05:22(21:22 UTC),LiteLLM 维护者在 issue #47826 里发了一条状态更新,第一次讲了他们认为的完整攻击链。他们的原话是,攻击者得同时突破好几道独立的安全控制才做得成。下面按原文整理,每一步都是 LiteLLM 的说法,他们自己也写了其中一部分还在确认。

  1. token 怎么进的日志:按 LiteLLM 的说法,他们观察到在某些特定情况下,Claude Code 会把用户的 GitHub CLI OAuth token 写进响应正文;如果网关记录了响应正文,这些 token 就可能出现在 LiteLLM 的日志里。「特定情况」具体指什么,原文没说。
  2. 日志在哪台机器上:LiteLLM 内部有一台 LiteLLM 部署是暴露在公网上的,他们在里面发现了一个出乎意料、名字看着很无害的用户账号。LiteLLM 认为攻击者进了这台部署,从它的日志里拿到了 GitHub CLI OAuth token。
  3. 拿到了谁的:LiteLLM 称攻击者手里有三名工程师的 GitHub CLI 凭证,其中一个有权限运行 GitHub workflow,另一个属于他们两名 GitHub 组织 owner 之一。北京时间 00:32 那条更早的更新还说,攻击者用一个账号触发了 project-releaser,再用另一个账号批准。
  4. 时机:LiteLLM 说周一他们临时放宽了发布权限,好让两名内部工程师每天给客户发 bugfix backport。
  5. 改发布流程:攻击者先想合并一个改动,让发布工具可以指向别的源码仓库,被保护规则挡住了。接着用那位组织 owner 的 token 关掉分支保护,把改动强推了进去。
  6. 发布:最后运行被改过的 workflow,指向仿冒仓库 Berri-AI/litellm(正版是 BerriAI/litellm),流程照常签名并发布了带毒的产物。

有几件事要分开看:

  • 这是 LiteLLM 的调查结论,不是 Anthropic 的确认。LiteLLM 没说「特定情况」是什么、涉及哪些 Claude Code 版本,也没给复现方法。截至北京时间 06:56,我在 @AnthropicAI、@claudeai 和 Anthropic 的公开页面上都没找到回应,Anthropic 尚未回应。在那之前,我不会把这件事写成「Claude Code 有漏洞」。
  • 攻击者是怎么进的那台内部部署,LiteLLM 说还没确认,目前怀疑是一个最近刚修补的漏洞,没说是哪一个。
  • LiteLLM 说下一次状态更新在北京时间 07:20(23:20 UTC)。在那之前他们的镜像仓库仍然锁着,正常发版暂停。有新内容我会补到这里。

用中转站或自建网关跑 Claude Code 的,怎么自查

这条链最后怎么定性还要等,但如果你的 Claude Code 流量经过中转站或自己搭的网关,同一台机器上又登录过 gh,下面几步花不了多少时间:

  1. 运行 gh auth status,看看这台机器登录了哪些 GitHub 账号、token 带哪些权限。
  2. 作废旧 token。中转站的日志你看不到,也没法让人家删,最稳的办法就是让 token 失效:打开 GitHub 的 Settings → Applications → Authorized OAuth Apps,找到 GitHub CLI,点 Revoke,然后在本机重新 gh auth login。登录过的每台机器都要重新登录一次。
  3. 自建网关的,先把请求/响应正文日志关掉,再去查已有的日志。LiteLLM 用户可以看看 litellm_settings 里的 turn_off_message_logging 和 general_settings 里的 store_prompts_in_spend_logs,接了 Langfuse 之类日志回调的也要一并检查。
# 列出含 GitHub token 的日志文件(只打印文件名,不打印 token)
grep -rlE 'gh[opsu]_[A-Za-z0-9]{20,}' /path/to/网关日志目录

# 压缩过的日志
find /path/to/网关日志目录 -name '*.gz' -exec zgrep -lE 'gh[opsu]_[A-Za-z0-9]{20,}' {} +

这条正则覆盖 gho_(gh 登录用的 OAuth token),也顺带覆盖 ghp_、ghs_、ghu_。正文如果存进了数据库或日志平台,也去那边搜一下 gho_。查到了就先去 GitHub 撤销对应的 token(gho_ 按上面第 2 步撤销 GitHub CLI 授权,ghp_ 到 Personal access tokens 里删掉),再清理含 token 的日志,然后翻一下账号的 Security log,看有没有不认识的操作。

原文:LiteLLM 在 issue #47826 的攻击链更新。

源码明明是干净的,为什么没挡住

很多人默认「仓库干净」就等于「包干净」,其实这是两回事。你装的是构建产物,也就是 wheel 或镜像,是某条流水线拿某份源码打出来的。这次 LiteLLM 投毒里,BerriAI/litellm 上的 v1.107.0 tag 确实是干净的。AISLE 说他们的 AI 审查「在 LiteLLM 源码里没发现问题」,提 issue 的 Upwind 团队也确认 payload 不在 tag 里。

差别只在 sdist 的 pyproject.toml 里多出来的一行:[tool.maturin] 下面的 data = "cook"。这行让 LiteLLM 用的 Rust/Python 构建工具 maturin 把一个 cook/ 目录打进 wheel 的数据区,安装时这些文件会直接落到 site-packages 根目录。cook/ 这个目录不在 sdist 里,也不在任何分支上。JFrog 发现 payload 文件的 ZIP 时间戳和元数据跟正常文件完全一致,说明它们是正常构建时打进去的,不是事后手工塞的。所以你读 tag 也好,解开 sdist 也好,都看不到毒,只有打开 wheel 才看得见。

LiteLLM 投毒逐分钟时间线

以下都是北京时间(UTC+8),括号里是 UTC。攻击者那一侧的时间来自 Upwind 对提交记录和镜像构建证明的分析(见它的第一次更新),其余来自各方自己的时间戳。

LiteLLM 投毒时间线(北京时间):从 16:56 攻击者提交到 23:08 镜像仓库实测
根据 issue #47826(Upwind)、AISLE、PyPI、Docker Hub 和 GitHub API 整理。北京时间(UTC+8),括号内为 UTC。
  • 10 月 13 日 08:45(00:45 UTC):仿冒域名 litellm.dev 注册。按 Upwind 的说法,后来当投递点用的 GitHub 账号 letters9813 也是这天建的;AISLE 说投递点仓库是 10 月 18 日建的。准备工作提前了一个多星期。
  • 16:56–17:48(08:56–09:48):作者署名 Berri-AI <[email protected]> 的 9 个提交,先后加入三个恶意文件、把版本号改回 1.107.0,最后加上 data = "cook"。这些提交是在 GitHub 网页编辑器里做的,所以 GitHub 会标「Verified」。这个标记只说明提交是在 github.com 上完成的,不代表可信。完整 diff 还能看到,这些提交挂在 fork 网络里,不属于任何分支或 tag。
  • 18:46:29(10:46:29):第 10 个提交 20535d8f,往三个 Dockerfile 里加了 COPY cook ./cook。我用 GitHub API 查过,它是 10 个提交里唯一没有签名的(verified: false),提交说明还原样引用了一段构建报错:「The gateway/backend/migrations builds failed with: maturin failed: No such data directory /app/cook」。
  • 18:51:23(10:51:23):5 分钟后,官方发布工具 BerriAI/project-releaser 开始跑 run 39587059944。按 Upwind 的分析,它的构建证明里写的源码是 github.com/Berri-AI/litellm 的 20535d8f。
  • 19:39:51–19:40:13(11:39:51–11:40:13):8 个文件陆续传上 PyPI(AISLE)。
  • 19:43:05:AISLE 的依赖防火墙拦下第一个 wheel,距上传 3 分 14 秒。这是他们内部的检测,当时还没公开。
  • 约 19:48:Docker Hub 上的 v1.107.0、latest、main-latest、main-stable 都指向了带毒镜像。
  • 19:54:18:@UpwindMDR 在 X 上发帖,说发现了「可能的供应链攻击」,点名 cook.pth、sitecustomize.py 和每小时轮询 C2。这是我能找到的最早公开预警,离上传 14 分钟,比 GitHub issue 早 12 分钟。
  • 20:06:41(12:06:41):Upwind 研究员 LidorMachluf 提交 issue #47826,给出所有文件哈希、payload 拆解和清理步骤。20:22 JFrog 在 X 上发预警,我 23:09 左右看的时候是 68,417 次浏览。
  • 20:42:54:一个说明毒版本扩散有多快的例子。RAG 项目 VectifyAI/PageIndex 合并 PR #619,为了用上一个修复,把依赖下限改成 litellm>=1.107.0。21:11:12 合并 PR #620 回滚,标题就是「1.107.0 was compromised」。这 29 分钟里,从它 main 分支全新安装,只能装到带毒版本。
  • 20:57:Upwind 补充:Docker 镜像也中招了,而且所有 v1.107.0 镜像的构建证明都指向仿冒仓库。
  • 最晚约 21:12:PyPI 隔离这个版本。确切时间没人公布。我找到的最早公开提及是 21:12:09 的一条 X 帖子,21:17 有网友贴出的 PyPI 截图里已经显示「RELEASE QUARANTINED」。AISLE 说 PyPI 是在他们报告后「不久」主动联系并隔离的。
  • 21:50:33:维护者 ojensen-berri 第一次回复:「按 SEV-0 处理,PyPI 已隔离 1.107.0。」
  • 22:32:11(14:32:11):第二次回复:「恶意 Docker 镜像已删除。」开源用户先锁 1.106.2,企业版私有仓库不受影响。下次更新在 16:30 UTC,也就是北京时间 10 月 22 日 00:30。
  • 23:08 和 23:18:我们自己对镜像仓库的两次实测(见下一节),第二次是发文前复测,结果一样。
GitHub issue #47826 标题,以及维护者北京时间 22:32 宣布恶意 Docker 镜像已删除的回复,LiteLLM 投毒官方记录
来源:GitHub issue #47826,BerriAI/litellm。截图时间:2026 年 10 月 21 日北京时间 23:00(15:00 UTC)。评论上的「28m ago」对应 22:32。

「已删除」删的是标签,Docker Hub 上的镜像还在

这是这次 LiteLLM 投毒里很容易漏掉的一点。北京时间 23:08(15:08 UTC),我向两个镜像仓库发了只查 manifest 的 HEAD 请求,没拉镜像,更没运行;23:18 发文前又复测了一遍,结果完全一样。Docker Hub 和 GHCR 上,latest、main-stable、v1.107.0 都返回 404,v1.106.2 返回 200。也就是说 latest 是被删掉了,没有改指到干净版本,所以现在不写标签直接 docker pull litellm/litellm 会失败。这倒不是坏事,本来就该写死版本。

问题在于,Docker Hub 上带毒的 index(digest 以 sha256:1bbcb144… 开头)和它的 amd64(bd1aeee4…)、arm64(56b74573…)manifest 还是返回 200。GHCR 上同样的 digest 是 404,那边是真删了。这为什么要紧?如果今晚有人在 k8s 清单、Helm values 或 compose 文件里写死了这个 digest,比如很「规范」地把当时 latest 的 digest 固定了下来,那它会继续拉到窃密程序。请拿你的 digest 对照 Upwind 更新里的 IOC 列表。我这里故意不写完整的拉取命令。

Docker Hub 上 litellm/litellm 已经没有 latest 标签,但实测带毒的 1.107.0 digest 仍返回 200,LiteLLM 投毒后的镜像状态
上:Docker Hub 标签页,北京时间 23:01 截图。下:binmage 北京时间 23:08(15:08 UTC)对 manifest 发的 HEAD 请求,digest 故意截短。

恶意代码是怎么工作的

为什么不 import 也会中招:.pth 文件

Python 的 site 模块在启动时会处理 site-packages 里的每个 .pth 文件。大部分行只是往路径里加目录,但官方文档写得很清楚:以 import 加空格或 tab 开头的行会用 exec() 执行,而且「总是在 Python 启动时运行……不管这个模块实际会不会被用到」。cook.pth 只有 12 字节,内容就是 import cook。于是这个解释器或 venv 每启动一次,cook.py 就跑一次:你的应用、单元测试、Jupyter 内核都算,连 pip 自己也算。Python 3.15 已经把 .pth 里的 import 行标成弃用,但弃用期内照样执行,所以在大家实际在用的 Python 版本上,这条路完全畅通。

sitecustomize.py 是第二个钩子。路径设置好之后,Python 会尝试 import 一个叫这个名字的模块。它本来是给管理员定制安装用的,这里被拿来启动加载器。

AISLE 技术分析截图:1.107.0 wheel 里的 cook.pth、cook.py 和 sitecustomize.py,LiteLLM 投毒的核心手法
来源:AISLE Research Team。截图时间:2026 年 10 月 21 日北京时间 23:01(15:01 UTC)。

窃密程序偷什么

cook.py 只有一行,是 zlib 加 base64 包起来的代码,解开大约 61 KB,自称 privcollect。Upwind 在后续评论里贴出了解码后的完整源码。它在后台线程里跑一次,不需要任何 C2 服务器,收集的东西包括:

  • 主机和网络信息,shell、数据库、REPL 的历史记录,~/.ssh,GPG 和 TLS 私钥
  • Chromium 系浏览器保存的密码(会解密)、Cookie,以及 Firefox 的登录数据
  • AWS、Azure、GCP、Kubernetes、Docker、GitHub CLI 和各种包管理器的凭证,.env*、*.pem、*.key 文件
  • 云元数据:AWS 实例角色和 ECS 任务凭证(还会调 STS GetCallerIdentity 验证能不能用),以及 GCP、Azure 的 token
  • AI 工具的配置和登录文件:Claude、Codex、Cursor、Gemini、Copilot、Windsurf、Kiro、Hugging Face token 等等
  • Linux 上所有可读进程的环境变量(/proc/*/environ),不只是它自己的
  • 只要设置了 DATABASE_URL,就导出 LiteLLM_CredentialsTable 和 LiteLLM_ProxyModelTable,也就是代理里存的所有上游模型 key

打包好的数据用 AES-256-CBC 加密,密钥再用内置的 RSA-4096 公钥包一层,然后用明文 HTTP POST 到 http://www.litellm.dev/healthcheck。抓包也看不出偷走了什么。上传成功后它会写一个 /tmp/.pc_once_72129118c59f5636c0bb。JFrog 特别提醒:反过来说,没有这个文件什么也证明不了。

每小时一次的加载器:在休眠,没有失效

sitecustomize.py 会 fork 出一个脱离终端的后台进程(setsid,输出丢进 /dev/null,工作目录切到 /),并锁住 /tmp/remote_code_loader.lock。它每 3600 秒去 GitHub 仓库 letters9813/pythonclient-test 读一次 domain.txt。如果里面写了域名,就再跑一遍窃密程序(这一份不写标记文件),然后下载 http://<域名>/codepy,不做任何校验,直接交给 exec()。Windows 上 fork 会静默失败,但 cook.py 照样执行。

按 Upwind 的说法,domain.txt 从 10 月 20 日起一直是空的,所以没有任何受害机器收到过远程命令。今晚我查的时候,这个仓库和 letters9813 账号都已经 404。这是好事,但别想太多:窃密程序第一次启动时就已经干完活了,这个用户名以后能不能被人重新注册也还不知道。C2 没了只能算「没那么糟」,不等于「安全了」。

它是怎么发出去的:哪些确认了,哪些没有

LiteLLM 投毒路径图:干净的 tag、fork 网络里的提交、仿冒仓库、PyPI wheel 和 Docker 标签、Python 启动即执行 cook.pth、外传和每小时加载器
「从仿冒仓库构建」这一步是 Upwind 根据镜像构建证明得出的结论,官方尚未确认。

下面是我对 LiteLLM 投毒发布过程的还原,证据和推断分开写。

  • 有产物为证:tag 里没有 payload;sdist 的 pyproject.toml 多了 data = "cook";wheel 和镜像里的恶意文件逐字节一致。
  • Upwind 的结论,证据较强,LiteLLM 后来的说明也对上了(见开头的更新):这次发布是从仿冒仓库 Berri-AI/litellm(注意多了个连字符)构建的。所有 v1.107.0 镜像的 SLSA 构建证明都记录了这个源码地址和 20535d8f,而同一个发布工具在构建 v1.106.2、v1.106.1、v1.107.0-rc.2 时记录的都是正版 BerriAI/litellm。Upwind 在 21:24 的更新里还做了逐字节比对:1.107.0 wheel 里的源码跟 main 分支的 a8113b0d 完全一致(3131/3131 个文件),跟 tag 反而对不上。它还主动撤回了自己早先的一个判断,也就是「PyPI 先收到上传、GitHub 后发 release」很可疑,因为这本来就是 LiteLLM 的正常顺序。报告里能看到这种自我更正,我觉得是加分项。
  • 我的推断,不是结论:20535d8f 引用的那段 Docker 构建报错,只有在加了 data = "cook"、却没把目录拷进构建阶段时才会出现,而官方发布流程 5 分钟后就开跑了。这说明攻击者很可能看得到真实构建的输出,可能是官方日志,也可能是在别处原样复刻的流水线。这只是线索,不是证据。
  • 后来有了说法的:谁触发了 run 39587059944、源码仓库参数怎么变成了 Berri-AI/litellm。按 LiteLLM 的说法,攻击者用偷来的 GitHub CLI token 触发并批准了发布,又用组织 owner 的 token 关掉分支保护、强推了改动,详见上面的更新。还不知道的:攻击者怎么进的那台内部部署,以及到底有多少人下载了 1.107.0。目前没有任何来源给出按版本统计的下载量,我也不打算猜一个。
攻击者提交 20535d8f 不属于任何分支,提交说明引用了 Docker 构建报错,LiteLLM 投毒的关键证据
来源:GitHub 提交 20535d8f,页面提示该提交不属于仓库任何分支。截图时间:2026 年 10 月 21 日北京时间 23:09。

Trusted Publishing 和构建证明:LiteLLM 用了没有

PyPI 支持 Trusted Publishing,让 CI 用短期有效的 OIDC 身份发布,不必存长期 token;也支持 attestations,就是用 Sigstore 签名、说明某个文件由哪个工作流构建的声明。在 PyPI 发布历史页上,litellm 每个版本都写着「No provenance」,安全的 1.106.2 也一样,我查它的 provenance 接口返回 404。Upwind 说 litellm 从来没发布过构建证明。

有了构建证明就能挡住吗?说实话,如果是官方发布工具自己被指到了错误的仓库,单靠它大概挡不住。有意思的是,Docker 镜像是带 SLSA 构建证明的,Upwind 正是靠它发现了那个带连字符的 Berri-AI。记录一直都在,只是没有任何工具在部署前去核对。LiteLLM 的 release 说明里写了 Docker 镜像都用 cosign 签名。带毒镜像当时有没有有效签名,我没找到任何来源提到;就算有,也只能证明「是我们的流水线打的」,而被利用的恰恰就是流水线。

PyPI 页面:litellm 最新版为 1.106.2,1.107.0 标记为 Release Quarantined 且 No provenance,LiteLLM 投毒后的处置结果
来源:PyPI 上的 litellm。截图时间:2026 年 10 月 21 日北京时间 23:01(15:01 UTC)。

这也是 LiteLLM 今年第二次在 PyPI 上出事。3 月份,1.82.7 和 1.82.8 被发布了带窃密程序的版本,按 LiteLLM 官方的说法,是 CI 里用到的 Trivy 被攻破,导致凭证泄露。JFrog 认为这次的 payload、持久化手法和基础设施都不一样,也没找到跟那个团伙有关的证据,所以应该当成两起独立事件。不过七个月里发布链路两次被攻破,也说明这件事有多难做好。

为什么网关被投毒最要命

LiteLLM 的意义在于用一个 OpenAI 风格的接口挡在几十家模型服务前面。所以它在本站读者里特别常见:用 LiteLLM 代理给 Claude Code 或 Codex CLI 转接(比如把 ANTHROPIC_BASE_URL 指向它),换便宜的模型、分摊负载,或者让 Claude 额度撑得更久。这类方案确实省钱,但也意味着同一台机器上同时放着你的 Anthropic、OpenAI、Gemini、Bedrock 的 key。

LiteLLM 投毒最要命的地方就在这里,窃密程序就是冲着这种机器来的:有专门的模块去读代理数据库里存的上游 key,读每个进程的环境变量,还顺手拿走同一台机器上 Claude Code、Codex、Cursor 的登录文件。如果你的代理跑在自己的笔记本上(个人用户很常见),浏览器密码和 SSH 密钥也一起没了。我想把这个取舍说清楚:省钱的 AI 方案把所有 key 集中到了一处,那就得把它当成要紧的东西来管。

LiteLLM 投毒的影响还会顺着下游项目扩散。我逐个读了各项目 main 分支的依赖文件:DSPy 的核心依赖是 litellm>=1.65.8(Python 3.15 以下),今晚不锁版本全新安装就会解析到 1.107.0。CrewAI(crewai[litellm])、Google ADK(extensions 可选依赖)、smolagents([litellm])和 OpenAI Agents SDK([litellm])通过可选依赖引入,都没有上限。GraphRAG 的 LLM 包锁死 litellm==1.100.1,mem0 限制在 1.98.0 以下,aider 的 requirements 锁 1.82.3,crawl4ai 用的是分叉包,这几个都不受影响。「会解析到」不等于「已经中招」,实际装了哪个版本,要看你的锁文件和安装时间。

LiteLLM 投毒自查与清理

顺序很重要。在已经中招的环境里,任何一次 Python 启动都会再跑一遍 payload,而 pip 本身就是 Python。所以先用 shell 查。

# 1) 先找被放进来的文件,全程不启动 Python
#    (覆盖 venv、conda、~/.local,以及 Debian/Ubuntu 的 dist-packages)
find / \( -path '*site-packages*' -o -path '*dist-packages*' \) \
  \( -name cook.pth -o -name cook.py \) 2>/dev/null

# sitecustomize.py 可能本来就有合法的,要看内容
find / -name sitecustomize.py 2>/dev/null | \
  xargs grep -l -E 'remote_code_loader|letters9813|litellm\.dev' 2>/dev/null

# 2) 主机标记文件和后台加载器
ls -la /tmp/.pc_once_72129118c59f5636c0bb /tmp/remote_code_loader.lock 2>/dev/null
fuser -v /tmp/remote_code_loader.lock 2>/dev/null   # 或者 lsof /tmp/remote_code_loader.lock
  1. 先按上面的命令找文件。这个写法同时覆盖 site-packages 和 Debian/Ubuntu 系统级 pip 用的 dist-packages。Mac 上可以把 / 缩小到 ~、/usr/local、/opt、/Library/Frameworks,省点时间。找到 cook.pth 就按整机失陷处理。
  2. Docker:今晚 19:48 左右以后拉过 latest、main-latest、main-stable 或 v1.107.0 的,默认已经中招。仓库删了镜像,你本机的不会跟着消失,用列表下面的命令查一下。
  3. 再查版本:pip show litellm,但只在第 1 步什么都没找到的环境里跑。
  4. 确认感染的话:先结束占着锁文件的那个进程,再用 rm 删掉 cook.pth、cook.py、sitecustomize.py 和 __pycache__ 里对应的文件,条件允许就把 venv、容器或镜像从零重建。用 pip 卸载不算干净:它会先启动一次 Python,而已经在跑的后台进程在文件删掉后还会留在内存里。
  5. 轮换这台机器能碰到的所有密钥。只要机器能上外网,就默认已经传出去了。先换 LiteLLM 代理里存的所有上游 key,再换 master key 和 virtual key、DATABASE_URL 的密码、云服务 key 和实例角色、SSH 和 GPG 密钥、GitHub/npm/PyPI token、AI 工具的登录(Claude Code、Codex、Cursor)、浏览器里保存的密码,以及这台机器上任何进程环境变量里的密钥。
  6. 盯紧账单。去 Anthropic、OpenAI 等平台的用量页面看有没有陌生的峰值,还没设消费上限的现在就设。AWS 用户可以按 issue 里的建议,在 CloudTrail 里查实例角色在安装时间前后有没有调用 GetCallerIdentity,以及这些凭证有没有在你的网络之外被用过。
# 本机有哪些 LiteLLM 镜像、什么时候拉的
docker images --digests | grep -i litellm

# 在运行中的代理容器里看(用 ls,不用 python)
docker exec <容器名> sh -c 'ls /app/.venv/lib/python3*/site-packages | grep -E "^cook|^sitecustomize"'

这次之后还管用的几条

这些都不需要新工具。能让这次 LiteLLM 投毒少伤人的,大多是本周就能配好的基本功。

  • 锁版本,也锁哈希。用 pip install --require-hashes,或者 uv(uv lock)、poetry 的锁文件。但要明白:哈希只能证明你装到的跟当初锁的是同一个文件,证明不了这个文件干净。1.107.0 的哈希在 PyPI 上一样是「官方」的。
  • 镜像按 digest 固定,别用 latest。同时记下你审核通过的是哪个 digest,因为今晚已经证明了,标签删了不等于镜像删了。
  • 新版本晚一天再升。今晚 3 分钟就被检测到,14 分钟就有了公开预警。非安全更新统一晚 24 小时(比如 Renovate 的 minimumReleaseAge,或者 CI 里干脆别用 -U),几乎所有人都能躲开这个窗口。
  • 锁 1.106.2 靠的是信任,不是验证。它同样没有构建证明。在 LiteLLM 发布 provenance 之前,「已知安全」的意思只是「暂时没人发现问题」。这是该去推动项目用 Trusted Publishing 的理由,不是该恐慌的理由。
  • 把网关当保险柜管。给网关单独开上游 key,设低额度上限;把它放在单独的机器或容器里,别跟存着浏览器配置和 SSH 密钥的笔记本混在一起;出网流量只放行它需要的模型 API。连不上 litellm.dev 的窃密程序,什么也发不出去。
# requirements.txt,然后运行:pip install --require-hashes -r requirements.txt
litellm==1.106.2 \
  --hash=sha256:cb1a2a1e55a4a8350ae2167525dc0d06f59b078cd685fb3e0f5419f02da5420e \
  --hash=sha256:34bfdc26c2c075726eda59ec0675e2a8a67b6e59a03980def7ef7651b849141a
#  (这是 manylinux x86_64 和 aarch64 两个 wheel 的哈希,其余 6 个文件的哈希在 PyPI 上)

# Docker:标签加 digest 一起写
ghcr.io/berriai/litellm:v1.106.2@sha256:7294b90f868c5c38c99b83021c7ba46f9b439e95f8e32964c925c7d2563a03f7

常见问题

我只用 LiteLLM 的 Python SDK,没跑代理,会中招吗?

装了 1.107.0 的 wheel 就会。payload 不依赖 import litellm,也不依赖代理。只有导出数据库那一步是代理特有的,需要 DATABASE_URL。

我已经把 1.107.0 卸载了,算干净了吗?

不一定。按上面的 find 命令查一遍,看看有没有还在跑的加载器,密钥照样要换。窃密程序第一次启动就会执行,卸载也追不回已经被偷走的 key。

1.106.2 安全吗?

Upwind 检查了 1.106.2 和之前几个版本的所有 wheel,都是干净的,维护者也建议先锁它。但它在 PyPI 上没有构建证明,所以这是目前能拿到的最好证据,不是密码学意义上的保证。

Claude Code 或 Codex 本身受影响吗?

这次投毒没有改动 Claude Code 或 Codex 本身。不过 LiteLLM 在 05:22 的更新里说,特定情况下 Claude Code 会把 gh 的登录 token 写进响应正文、被网关日志记下来,这是他们认定的攻击起点。「特定情况」是什么没说明,Anthropic 也还没回应,详见上面的更新,那里也有自查步骤。另外,窃密程序如果在同一台机器上跑过,会读走它们的登录和配置文件。那就退出重新登录,并吊销存在里面的 API key。

到底有多少人中招?

不知道。没有任何来源公布过 1.107.0 的下载量,偷走的数据又是用攻击者的公钥加密的,外人没法统计。今天看到的任何 LiteLLM 投毒受害人数,都要打个问号。

接下来该关注什么?

维护者说下一次更新会在 issue #47826 里发,时间是北京时间 10 月 22 日 07:20(23:20 UTC)。发布链路怎么被劫持的,05:22 那次更新已经给了说法(见上面的更新),还没答案的是攻击者怎么进的那台内部部署。在这个问题有答案、正常发版恢复之前,我对 LiteLLM 的任何新版本都会多一分怀疑,先等等再升。

资料来源:GitHub issue #47826(Upwind 的报告和维护者回复)、AISLE、JFrog、Aikido、PyPI、Docker Hub、Python site 模块文档、OSSF malicious-packages #1655。镜像仓库实测由 binmage 于 2026 年 10 月 21 日北京时间 23:08 和 23:18 完成。

留下你的想法

你的邮箱不会公开。