如果你在 2026 年 10 月 21 日装过 LiteLLM 1.107.0,或者拉过 tag 为 latest / main-latest / main-stable 的 LiteLLM 镜像,这台机器需要按「已沦陷」处理。PyPI 上的 1.107.0 已被下架,Docker Hub 相关镜像与 tag 也已移除,但已经装到本地的东西不会自己消失。
这是 LiteLLM 今年第二次被投毒。上一次是 2026 年 3 月,JFrog 安全研究团队记录到它的流水线运行了未固定版本的 Trivy 0.69.4,CI/CD 与发布凭据被盗,随后被用于发布带后门的 1.82.7 与 1.82.8——窃取凭据、横向扩散到 Kubernetes 节点、装持久后门。JFrog 追踪编号 XRAY-955589。这一次的载荷和基础设施都不同,目前没有证据把它与三月份的 TeamPCP 攻击联系起来,应按独立事件处理。
投的是什么
JFrog、SafeDep、Socket、StepSecurity、Upwind 五家机构独立分析,结论一致:1.107.0 的全部七个 wheel(macOS、manylinux、musllinux、Windows)都被塞进了三个额外文件,安装到 site-packages 根目录:
cook.pth、cook.py、sitecustomize.py。
触发机制是 Python 的启动钩子。.pth 文件在解释器启动时逐行执行,sitecustomize 在启动时被自动导入——这意味着载荷在 Python 进程启动时就跑,跟你的代码有没有 import litellm 完全无关。哪怕你只是执行了一次 pip,恶意代码也已经执行了。StepSecurity 把它命名为 privcollect,Upwind 也确认了相同行为。
凭据窃取器(cook.py)收集的目标包括:SSH、GPG、TLS 私钥;.env 文件;Git、npm、PyPI 凭据;AWS、Azure、Google Cloud、Kubernetes、Docker、Vault 凭据与云元数据令牌;Claude、Codex、Gemini、Cursor 等 AI 工具的配置文件与 token;所有能读到的进程环境变量;如果设置了 DATABASE_URL,还有 LiteLLM_CredentialsTable 与 LiteLLM_ProxyModelTable 的内容——那里存着你所有上游模型供应商的 API key。数据经加密后 POST 到 www.litellm[.]dev/healthcheck,注意这是个仿冒域名,官方是 litellm.ai。
后门(sitecustomize.py)会 fork 一个后台守护进程,每小时去 GitHub 上的一个「死投点」文件读服务器地址,然后下载并 exec 返回的任意 Python 代码。地址文件在 13:40 UTC 被观测到已 404(GitHub 账号 letters9813 已删除),意味着二阶段暂时休眠——但那个文件随时可以被重新填上,JFrog 在撰稿时以只读方式取到它返回 HTTP 200、零字节、无跳转。空文件不代表安全,只代表攻击者还没填。
最值得记住的一点
这次事件在技术上有 lessons beyond LiteLLM 本身,其中最有价值的一条是:源码和产物讲的是两个故事。
sdist(源码包)里没有那三个载荷文件,只在 pyproject.toml 的 maturin 构建段被加了一行 data = "cook", instructs 构建时把一个 cook/ 目录复制进 wheel 的 data 区。而 git tag v1.107.0 里完全没有这些改动。也就是说,只审源码、只审 git tag、只审 sdist 的审查者,会完整地错过这次投毒。载荷文件的 ZIP 时间戳与元数据和合法 wheel 文件一致,说明是在正常构建过程中被纳入的,不是事后手工重打包。
推论很实用:依赖锁定要用 digest(哈希)而不是版本号。版本号 1.107.0 是一个标签,可以被指向任何内容;digest 锁定的是那一个确切的字节序列。
怎么善后
Upwind 和 SafeDep 的建议里有一条很反直觉但很重要:不要用 pip 排查。在受污染的机器上启动 Python 解释器,载荷会再跑一次。先在确认干净的环境里核对 lockfile、构建日志和镜像 digest。
具体动作:
1. 检查 site-packages 里是否存在 cook.pth、cook.py、sitecustomize.py;检查 /tmp/.pc_once_72129118c59f5636c0bb(凭据已外传标记)与 /tmp/remote_code_loader。
2. 固定到 1.106.2 或任何事发之前发布的版本,并按 digest 锁定。已核对干净的版本:1.103.6 至 1.106.2,以及 1.107.0rc1、1.107.0rc2。
3. 轮换该机器上所有可能读到的凭据:云密钥、GitHub token、SSH 私钥、CI/CD secrets、AI 工具 token,尤其是 LiteLLM 数据库里存着的上游 provider key。从干净环境重建。
4. 监控异常外联:www.litellm[.]dev/healthcheck、raw.githubusercontent[.]com/letters9813/...,以及 Python 进程对 169.254.169.254(云元数据服务)的请求。
5. 用私有 registry 的企业客户不受影响,官方说明走的是内部源。
LiteLLM 安全总监 Oliver Jensen 的说明里有一个细节值得记住:攻击是通过三名工程师的 GitHub 账号完成的,起因是被盗的 GitHub CLI OAuth token——一个账号触发 release workflow,另一个审批。所有 token 已吊销,GitHub 组织里除一名未受影响的用户外全部移除。但原始入侵路径尚未查明,所以官方明确表示「不能保证这些账号没有进一步访问」。这意味着 1.107.0 只是被堵住了,通道本身还没找到。
站长观点
我的观点是:这次事件的警示价值不在于「又一个包被投毒」,而在于它暴露了一个行业性的坏习惯——我们默认「审源码就够了」,而这个默认在现代构建体系里已经不成立。
Maturin、Rust 桥接层、平台相关的 wheel、构建期 data 目录注入,这些机制让「源码 → 产物」的链条上多了好几个源码看不见的环节。JFrog 自己也承认,现有证据只能说明注入发生在构建期间或之后,无法证明是哪个 CI 系统、哪台维护者机器、哪个 PyPI 账号出了问题。这种不确定性本身就是最大的问题:你没法审你审不到的东西。
所以我给个人开发者的取舍建议很直接:不是「要不要用开源 AI 网关这类基础设施」,而是「我的依赖锁定有没有到 digest 这一层」。这次五个独立安全团队的分析加起来不到 48 小时,但在那 48 小时里,已经有人在生产机器上跑了带后门的镜像。速度不是安全边界的替代品。
易变信息标注:本文涉及的版本状态、投毒文件清单与外联域名,截至 2026-10-22 以 LiteLLM 官方 issue #47826 与各安全厂商报告为准;事件仍在调查中,原始入侵向量与二阶段基础设施可能被更新。
来源清单
1. JFrog Security Research:LiteLLM Hijacked Again: Malicious 1.107.0 Wheels Steal Credentials and Load Remote Code
2. SafeDep:Compromised LiteLLM 1.107.0 on PyPI Installs a Credential Stealer and Backdoor
3. OSINTSights(引 LiteLLM 安全总监 Oliver Jensen 说明):LiteLLM Python Package Compromised in Supply Chain Attack
4. 7minAI News:LiteLLM 1.107.0 shipped a credential stealer on PyPI and Docker
5. QPulse 风险简报:LiteLLM Version 1.107.0 Compromised via Stolen GitHub CLI OAuth Tokens