LiteLLM 1.107.0 被投毒:删掉包之后,恶意代码还在

两个小时四十分钟

10 月 21 日,一个每周下载量约 2300 万次的 Python 包,把自己签过名的恶意版本推上了 PyPI 和 Docker Hub。PyPI 上 11:39 到 13:23,Docker Hub 上到 14:20——这两个多小时里,LiteLLM 1.107.0 是一颗已经插上引信的雷。

这不是「依赖里有个漏洞」。漏洞是别人在你用的代码里留了道口子;这次是发布者用自己的签名、自己的流水线、自己的生产审批,把一个带后门的包正式发了出去。下游再怎么「只用官方源」,也防不住这个。

时间线:分支保护是怎么被关掉的

攻击者手里有三个不同开发者的 GitHub CLI 凭据。注意这三个凭据的分工——它比漏洞本身更值得看:

UTC 发生了什么
10:08:29 私有发布 workflow 的改动被分支保护拦下,理由很标准:没过 PR 与状态检查
10:08:34 一个有权限的开发者凭据覆盖保护,强推 commit 9cf97e93
10:46:39 改过的 workflow 跑了一次 staging,源码指向 Berri-AI/litellm
11:08:53 生产发布流水线启动
11:11:18 第二个开发者凭据通过了生产审批
11:39:51 七个 1.107.0 的 wheel 上传到 PyPI
11:46:56 同名镜像开始发布,latest 等三个标签一起被指向恶意镜像
11:53:49 第三个凭据删掉了 production 与 staging 的 workflow 记录
13:23 PyPI 移除该版本
14:20 Docker Hub 上的相关标签全部撤下或指回干净镜像

Berri-AI/litellm 和官方的 BerriAI/litellm,差别只有一个连字符。官方的源码 tag 是干净的——恶意文件只存在于那个仿冒仓库构建出来的产物里。

三个凭据分别干三件事:改 workflow、过审批、删日志。三件凑齐,一套「双人审批 + 分支保护」就形同虚设。这也解释了为什么事后调查先卡在「凭据怎么丢的」这一步:流程本身没坏,是流程依赖的那个假设坏了。

为什么它能活过 pip uninstall

恶意 wheel 往 site-packages 里塞了三个文件:cook.pth、cook.py、sitecustomize.py。

关键在 .pth。Python 启动时,site 模块会读 site-packages 下的 .pth 文件,凡是以 import 开头的行都会被当成代码执行。这套机制本来只是给包加搜索路径,副作用是它变成了一个「解释器一启动就执行」的持久化位置。

sitecustomize.py 更直接:site 初始化时会自动导入它。于是只要这个环境里有 Python 进程起来,载荷就跑——哪怕你从头到尾没 import 过 litellm。

所以官方处置说明的第一句是「别先跑 pip install」。pip 自己就是 Python 程序,在受污染的环境里执行任何 Python 命令,都等于把载荷再点一次火。删掉 litellm 包目录同样没用,那三个文件不在包目录里。

第二阶段更省事:sitecustomize.py 每小时去一个 GitHub 仓库读 domain.txt,拿到域名就下载代码执行。本次那个文件里只放测试值,所以没观察到真正的二次载荷。但设计意图很清楚——攻击者可以随时换远程内容,不必再碰一次发布流程。

它到底在偷什么

按分析报告的分类,清单长得有点吓人:

  • 云与编排:AWS / GCP / Azure 凭据,包括实例元数据;Kubernetes、Docker、Vault、Terraform 的密钥;
  • 开发者:SSH、GPG、TLS 私钥,git / npm / PyPI 的 token,.env 文件,shell 历史;
  • AI 工具:各家模型 API key,以及 Claude、Codex、Gemini、Cursor、Copilot 这些工具的登录态;
  • 浏览器:保存的密码与 cookie;
  • LiteLLM 自己:如果配了数据库,会直接查代理存上游 key 的那几张表。

看这份清单就明白攻击者图什么了。LiteLLM 在很多团队里是「所有模型 key 的唯一入口」。投毒一个网关,等于一次拿到一屋子钥匙——这比挨个偷开发机划算得多。

如果这台机器装过 1.107.0

顺序别跳,跳一步就可能白做:

  1. 先隔离(断网或停容器),不要在环境里执行 python / pip;
  2. 删掉 site-packages 下的 cook.pth、cook.py、sitecustomize.py,连 __pycache__ 里的对应缓存一起;
  3. 杀掉那个脱离终端的 python 守护进程,它握着 /tmp/remote_code_loader.lock;
  4. 重建 venv 或镜像,pin 回 litellm==1.106.2;能重建就别就地修;
  5. 把这台机器能碰到的凭据全部轮换,包括 LiteLLM 的 master key 和上游 key;
  6. 翻云审计日志:安装时段有没有 GetCallerIdentity 之类的调用,之后有没有人从你的网络外面用这套凭据。

取证时有个小技巧:python -S 会跳过 site 初始化,.pth 和 sitecustomize 都不会被执行。查文件的时候用它起解释器,能少踩一次雷。

三条能落地的纪律

第一条,生产环境别让包管理器自己决定装什么:

# requirements.txt 里每个包都带 --hash,装不上就失败,而不是装个新版本
pip install --require-hashes -r requirements.txt

第二条,镜像用 digest,不用 tag。这次 Docker Hub 上的 v1.107.0、latest、main-latest、main-stable 四个标签一起中招——「跟 latest」在供应链攻击面前等于没有版本控制,而同一个 tag 前后指向不同内容这件事,只有 digest 能戳穿。

第三条,把「密钥中心」单独划区。LiteLLM、CI runner、私有包代理这类服务,天然握着比别人多的凭据,就不该和业务跑在同一台机器、共用同一套云角色。划区的意义不是让它不出事,而是让它出事时损失可数。

一年两次,问题不在某个人

这是 LiteLLM 在 2026 年的第二次投毒事件,前一次是 1.82.7 / 1.82.8。同一个项目一年两次,与其归因于某个维护者手滑,不如看结构:发布凭据的集中度太高,而三道关卡(改流程、过审批、删日志)都建立在「凭据没被偷」这一个假设上。假设一破,层层审批只是让攻击者多花十分钟。

对自建 LiteLLM 的人,最该改的不是「以后小心点」。先把它的定位改掉:它不是普通依赖,它是密钥仓库。密钥仓库要有自己的网络段、自己的凭据集、自己的重建流程。剩下的,交给锁版本和轮换周期。

参考来源

延伸阅读

© 版权声明
THE END
喜欢就支持一下吧
点赞11 分享
评论 抢沙发
头像/SUENCA
欢迎您留下宝贵的见解!
提交
头像/SUENCA

昵称

取消
昵称表情代码图片快捷回复

    暂无评论内容