☰
你装的那层网关,握着所有钥匙
2026/10/27 10:22:46 网站建设 项目流程

摘要:一层几乎没人盯着的 AI 网关,被攻击者用它的发布流水线推出了带窃密程序的版本。GitHub 上的源码是干净的,日志里却躺着三枚能改发布流程的令牌。最反常识的地方是,从令牌到手之后,攻击者再没撬过任何一道门。

你在docker-compose.yml里多加了一个服务。

理由完全正当。产品要同时接三家模型,直连就得写三套客户端、管三份 key、对三份账单。加个 LiteLLM 当统一出口,base_url改一行就换供应商,用量报表还能合成一张。

这一层没有一行业务逻辑,不碰订单,不碰用户表。上线那天你只给了它一份配置和一个DATABASE_URL。

十月二十一号早上,它成了你们整条技术栈里权限最大的那个进程。

那份源码,是干净的

先说最容易踩空的一步。

听说某个包被投毒,你的第一反应大概是去看它的代码——至少翻翻那个 tag 里改了什么。换成我也会这么干。

这次这么做,什么也查不出来。

LiteLLM 的事故报告里写得很清楚:GitHub 上 v1.107.0 那个 tag,一个恶意文件都没有。干净到你可以逐行 review,然后放心地签收。

那份东西是从哪儿进去的?

构建的时候。攻击者把发布流水线指到了一个仿冒仓库Berri-AI/litellm——正主叫BerriAI/litellm,只差一个连字符。流水线照常跑,照常签名,照常发布。

于是pip install litellm==1.107.0装下来的,是真的 LiteLLM,多三个文件。

你 review 的是 A,装下去的是 A 加 B。这两件事中间本来有一道门,那天那道门是从里面被打开的。

它站在什么地方

要看清这次为什么严重,得先承认一件不太舒服的事:你从来没把这一层当回事。

用户看不见它,产品经理不知道它存在,你的监控大盘上它只有一条几乎平着的 CPU 曲线。

可它的位置是这样的——所有模型请求都从它过,所有厂商的 key 都放在它手里。

一台跑了 LiteLLM 的机器上,它能摸到的东西有:数据库里那张存着每家厂商 API key 的表、自己的 master key 和 virtual key、DATABASE_URL里的密码、这台机器的实例角色凭证,还有每个进程的环境变量。

研究人员在恶意包里翻出的窃取清单更长。SSH 和 GPG 私钥、AWS、Azure、GCP 的凭证、Kubernetes 和 Docker 的 secret、CI/CD 和 Terraform 的凭证、浏览器存的密码、shell 历史,以及二十多种 AI 编程工具的登录配置。

你为了少写三套客户端装进来的东西,权限比你亲手写的业务服务都大。

更别扭的地方在后面。网关天生要记录请求和响应,不然用量报表和排错都做不了。

而日志里出现的东西,比数据库里的更杂。

官方报告里那段话值得逐字读一遍:他们在内网部署里发现了一个命名很不起眼、来路不明的多余账号,而这台部署暴露在公网,他们判断攻击者是从它的日志里拿到了三名工程师的 GitHub CLI token。

它把本该自己保护的东西,写进了自己会泄漏的地方。

你上一次给这台机器开公网访问,是因为业务真需要,还是因为调试顺手?

三枚令牌走完一条流水线

官方把时间线给到了秒。

十月二十一号 10:08:29,GitHub 的分支保护拦住了一次对私有发布工作流的未授权修改。它尽责了。

五秒之后,10:08:34,一枚与特权开发者账号关联的凭证把这层保护关掉,把改动强推了上去。

10:46:39,被改过的工作流跑了一次 staging,源码指向那个仿冒仓库。

11:11:18,第二个开发者账号的凭证通过了生产审批门。

11:39:51 到 11:40:13,二十二秒,七个恶意 wheel 传上了 PyPI。

11:53:49,第三个账号的凭证把生产与 staging 的工作流记录删了。这一步不拿东西,只擦脚印。

到 13:23,PyPI 下架。14:20,Docker Hub 上那批标签清干净。

PyPI 上的暴露窗口是一小时四十三分钟,Docker Hub 那边两小时三十三分。

这一个多小时里,有人发现吗?

发现了。AISLE 在首个 wheel 上传后三分钟十四秒标红,Socket 七分钟。可拦下来的是扫描器,不是任何一道流程控制——那些门在十分钟前就被人从里面卸了。

得把起点算进去才准确。攻击者先拿下了那台暴露在公网的网关,从日志里取到三枚令牌。从令牌到手那一刻往后,整条链上没有一处是攻破——五次操作,三次是拿三枚分属不同人的合法凭证去开的门,另外两次干脆由流水线自己动手。跑的是这家公司自己设计、自己加固过一整轮的流程。

还有个细节挺说明问题。事故前那个周一,他们临时把发布权限扩给了两名工程师,让他们做每日的 bugfix backport。攻击者拿到的那三枚令牌里,有一枚属于组织所有者。

权限的强度,永远等于持有它的那个人的权限。

门不是被撬开的,是被人关掉的

这家公司三月份刚出过事。

二〇二六年三月二十四日早上,v1.82.7 被推到 PyPI,v1.82.8 紧随其后,两个版本在线约四十分钟才被隔离。官方复盘里写了三条成因:CI 全跑在 CircleCI 上、所有步骤共用一个环境;发布凭证当静态 secret 放在环境变量里;扫描组件里的 Trivy 依赖没有锁版本。一个被投毒的 Trivy 在扫描时跑了起来,拿到了它不该拿到的凭证。

之后他们做了认真的加固。发布迁移到私有仓库,走 Trusted GitHub Publishers,docker tag 设成不可变,镜像用 Cosign 签名。

按常规水准衡量,这套配置算好的。

那为什么没挡住?

攻击者压根没去碰这些。他们找了一个组织所有者,让那枚令牌帮忙把分支保护关掉。

问题不在加固做得对不对,在加固假设了什么。所有建立在「必须走 PR、必须过状态检查」之上的保护,前提都是「没人能改这条规则」。而规则本身也是权限对象——能改它的人,可以让它下游的所有保护一起作废。

顺手把一处必须交代清楚的地方说了。LiteLLM 的报告推测,特定情况下 Claude Code 会把用户的 GitHub CLI OAuth token 嵌进响应体,响应体被记进日志,token 就留在了网关日志里。这是 LiteLLM 一方的说法,Anthropic 没有确认过。它可能是真的,也可能只是他们目前最合理的猜测。同一份报告也写明,攻击者究竟怎么进的那台公网部署,至今尚未确认,只怀疑是一个近期修补过的漏洞。

不知道就是不知道,不该为了故事完整而填上。

三个文件,不用你 import

恶意载荷是三个文件,落在site-packages:cook.pth、cook.py、sitecustomize.py。

.pth是 Python 的老机制,解释器启动时会读它。所以这段代码跑起来的时机,不是「你 import litellm 的时候」,而是「这台机器上任何一个 Python 进程启动的时候」。哪怕那个进程跟 LiteLLM 一点关系都没有。

cook.py是混淆过的窃密程序,把收集到的东西用 AES-256-CBC 加密,再用 RSA 包一层密钥,POST 到litellm.dev。真站是litellm.ai,这个仿冒域名十月十三号才注册。

sitecustomize.py拉起一个游离的守护进程,每小时去一个 GitHub 仓库取一次指令,取到什么跑什么,权限跟宿主一样,没有沙箱。

官方处置步骤里有一条最反直觉,也最容易踩:不要先用 pip install 去修。pip 也是 Python 进程,一跑就等于把恶意代码再执行一遍。正确顺序是先手工删掉那三个文件、杀掉握着/tmp/remote_code_loader.lock的进程,然后才重装。

换到 Java 项目上该动哪儿

回到你手上那个 Spring AI 项目。加网关这件事本身没问题,要动的是你给它的定位。

它不是顺手起的 sidecar,它是一台装着全部钥匙的生产数据库。

该做什么挡的是哪一步不做会怎样
版本写死,不用区间制品上传到 PyPI 那二十二秒恶意包在架上一小时四十三分,你随时可能装到
镜像按 digest 拉,不按 tagDocker 标签被重指到污染镜像latest正是这次被污染的四个标签之一
走私有源,不直连公共仓库上游被投毒的瞬时窗口只能赌自己刚好赶上扫描器报警
网关不挂公网日志被读走这一步这次攻击链的起点就是一台公网暴露的内网部署
日志脱敏,别用 debug 长存令牌落盘请求体和响应体里有 prompt、有代码、有 token
按密钥库标准轮换泄漏之后官方清单漏一样,前面的清理都白做

锁版本这一条,其实是官方的原话。三月那次复盘里,他们就把「升级新版本之前先留一段冷却期」写进了路线图,理由是留出时间查一遍。这次的窗口给了个参照:恶意包在 PyPI 上总共只活了一小时四十三分,你晚一天升级,就躲过去了。

你的pom.xml里那行版本号写死了吗?

但别把它删了

写到这儿得自己先答一个反问:那是不是干脆别用网关?

不是。取消这一层,key 会散进每个服务的配置文件,你会同时失去统一轮换的能力、失去用量可见性、也失去出事时「只有一个地方要查」的便利。那不是变安全,是变糊涂。

要改的不是用不用,是当什么用。

不是给它加块监控面板就算完,不是把 key 挪进 vault 就算完,也不是把版本锁死就算完。这三件都该做,可它们都只是补丁;真正要挪的,是这层东西在你架构图上的位置。

一个没被当成基础设施的网关,配置随手改,日志随手开,版本随手升,公网随手通。这四个动作单看都不算错,凑在一起,就是这次事故的全部条件。

回头看

你 review 的源码可以是干净的。tag 干净、源码干净,装下来的东西未必干净。看代码解决不了构建期注入。

网关是密钥最密集的地方,也是日志最密的地方。它保护的和它泄漏的,是同一批东西。

分支保护不是被绕过的,是被有权关掉它的人关掉的。权限的强度等于最强那个持有者。

真正挡住这次攻击的,只有锁版本和依赖冷却。一套挺讲究的加固,输给了一行没写死的版本号。

处置顺序反了会二次中毒。pip 也是 Python 进程,别拿它去修。

参考资料

  • Contained: Oct 21, 2026 LiteLLM release v1.107.0 Compromised——官方事故报告,含秒级时间线与处置步骤
  • Security Townhall Updates——事故的官方复盘,含版本号、三条成因与冷却期建议
  • Announcing CI/CD v2 for LiteLLM——事故后的加固措施
  • BerriAI/litellm——项目仓库,可对照 v1.107.0 的 tag 与事故报告的差异

作者:唐悦玮 | 从后端出发,用 AI 拓展到全栈的工程师。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询