去年十月初,网络安全圈里爆出一则让人倒吸凉气的消息:npm 引以为傲的 Trusted Publishing(可信发布)机制,被一个叫 SlugFest 的漏洞捅了个正着。超过 1500 个软件包暴露在被接管的风险之下,受影响包的总下载量轻松突破 1000 万次,平均每月还有 100 多万次安装。换句话说,无数开发者的项目、CI/CD 流水线,甚至整个公司的构建环境,都可能因为一个看似无害的 npm install 而引入恶意代码。
这件事最可怕的地方在于,攻击者根本不需要窃取 npm 令牌,不必入侵维护者的账号,甚至不用碰原始的 GitHub 仓库。发现这个漏洞的安全公司 Bloom Security 给它下了个精准的定义:这是一条软件包劫持路径,而不是一次正在进行的恶意软件攻击。也就是说,一旦有人动手,后果会比普通投毒严重得多。
名字背后的陷阱:GitHub slug 如何变成攻击入口
先来理解一下 Trusted Publishing 的初衷。这个功能本来是为了让 npm 发布更安全——维护者不再需要在 GitHub Actions 或 CI/CD 服务里存放长期有效的 npm 令牌,而是通过 OpenID Connect(OIDC)把软件包和指定的仓库、工作流绑定。发布时,工作流临时申请一个短期身份令牌,再换成 npm 发布凭证。听起来天衣无缝,密钥不落盘,凭证不过夜。
问题恰恰出在绑定的第一环上。对于那些尚未完成验证的配置,npm 记录的是人类可读的 GitHub 仓库路径,也就是所谓的 slug,形如 organization/project,而不是 GitHub 永久不变的数字仓库 ID 和所有者 ID。这意味着什么呢?一旦项目改名、转移、删除,或者所有者改了账号名,这个 slug 就可能"空出来",被任何人重新注册。
攻击者的操作链条清晰得令人发指:抢注一个已被弃用的 GitHub 所有者或仓库名,新建仓库,加一个文件名符合预期的工作流,向 npm 请求 OIDC 令牌。如果目标软件包的 Trusted Publishing 配置从未完成过第一次发布,npm 就会把新的仓库当成"老熟人",乖乖颁发有效的发布凭证。接下来,攻击者就能用合法的包名推送新版本——里面可以塞进任何他想塞的东西,包括恶意的 postinstall 脚本,在用户执行依赖安装时自动运行。
Bloom 的研究人员特意强调,这和以往大量 npm 安全事件里的令牌窃取完全是两码事。哪怕凭证本身再安全、有效期再短,只要背后的身份绑定依赖的是一个可以被重新占用的名字,而不是不可篡改的标识符,整条信任链就会断裂。
为什么 AI 编程助手让这件事变得更棘手
如果说漏洞本身已经足够让人警醒,那 Bloom 报告里另一组数据则指向了一个正在放大的风险敞口:AI 编码代理的普及。他们估算,一个典型的终端每个月通过 AI 代理安装的软件包大约有 70 个,而开发者自己的机器每月可能要装近 350 个。这还没算上每个包背后牵着的一大串间接依赖。
更微妙的是交互方式。编码助手、IDE 里集成的代理工具,选包、装包、跑脚本几乎一气呵成,人工审核的环节被压缩到近乎为零。如果某个常用依赖的合法版本被悄悄换成了恶意版本,AI 代理大概率会毫不犹豫地选"最新版",并在一次普通的安装任务里执行它的生命周期脚本。防范的重心,从"看好你的令牌"变成了"盯紧包的来源、锁死版本号、审查安装时脚本"。
这个判断和今年早些时候曝出的 AI 编码代理安全风险形成了呼应——被篡改的内容可以诱导代理运行攻击者控制的命令。而 SlugFest 给出的启示更加直接:危险不一定藏在提示词里,它可能就潜伏在一个走正规发布流程、看起来完全可信的包身上。
修复为何等了将近四个月
Bloom 在 6 月 6 日通过 GitHub 的 HackerOne 计划上报了这个问题,6 月 9 日即被确认。但修复直到 10 月 2 日才落地,中间隔了 120 多天。GitHub 的补救措施主要有两层:新建但尚未验证的 Trusted Publishing 配置,48 小时后自动过期,只有在完成首次成功发布后,配置才会被验证并绑定到仓库的不可变身份;此外,GitHub 还封禁了由 pull_request_target 事件触发的 Trusted Publishing 令牌——这类事件的评论互动很容易成为触发面。
如果你正在维护一个用 Trusted Publishing 发包的项目,有几件事值得立刻动手:把发布流程迁移到更安全的事件上,比如 push、release 或者 workflow_dispatch;对照官方文档检查每个可信发布配置,确认链接的 GitHub 仓库还在自己手里;一旦发生仓库改名、删除或转移,删掉旧绑定重新建;平时多留意异常的发布活动,条件允许就锁定依赖版本,认真核对来源证明。
另一个容易忽略的点是,来源证明(provenance)能告诉你包是在哪里构建的,却不能证明源码仓库、账号或构建环境本身没有出问题。早前 ChainDrop npm 蠕虫事件已经演示过,被感染的版本如何顺着开发者工具、仓库和自动化构建环境四处扩散。一条看似可信的发布路径,未必等于一条干净的供应链。
站在开发者的角度,这次事件其实传递了一个朴素但常被遗忘的道理:安全机制的价值,最终取决于它锚定的是什么。名字会变,账号会转手,仓库会易主,只有不可变的数字身份才是可靠的锚点。对于每天要和几十个 npm 包打交道的人来说,往后每一次 npm install,或许都值得多问一句——这个包,真的是我以为的那个包吗?