一个包能绑十个发布身份,你却还在 CI 里揣着一根永不过期的 token——npm 9 月 3 日把信任发布的规则改了
一个包现在能绑定十个”发布身份”,可你的 CI 里可能还躺着一根永不过期的 publish token——npm 在 9 月 3 日把「信任发布」的规则改了,而且改法有个反直觉的坑:配置是累加的,你新加的那条更窄的规则,管不住旧的那条更宽的规则。
先说结论:这次三个变更里,真正会咬人的不是”多了几个配置”,而是”新规则不会自动收窄旧规则”。你的安全边界等于所有配置的并集,不是最后写的那条。
9 月 3 日到底改了什么
GitHub Changelog 当天发了一条 npm trusted publishing 的 GA 更新,三件事一起上:
- 一个包可以配多个 trusted publisher,不再只有一个(npm 文档现在允许最多十个)
- staged(暂存)版本在 npm 的发布时恶意软件扫描跑完之前,不允许被批准
- 维护者可以在包版本界面里查看 staged / approved / rejected 的完整历史
听起来都是”更安全了”,对吧。问题出在第一条的语义上。
坑在哪:配错了——授权是并集,不是交集
官方描述得很清楚:多个配置之间是独立且累加的,任何一个 incoming OIDC identity 只要匹配上任意一条被允许的配置,就能执行对应动作;而且匹配顺序不保证。
翻译成人话:
# 你两年前配的:任何 main 分支的 push 都能直接 publish
publisher: repo=team/app, workflow=publish.yml, action=publish
# 你这次新加的:只允许 release 环境 + 签名 tag 的步骤发布
publisher: repo=team/app, workflow=hotfix.yml, environment=release, action=stage
你以为第二行收紧了口子。实际上第一行还在,攻击者(或者一个忘了删的旧 workflow、一个改过名的文件、一张留作”应急”的长期 token、一条自托管 runner 的路径)照样能走第一行直接发布。
这就是把”多配置”当成便利功能、而不是当成发布治理问题来对待的代价。
正确的用法:stage-only + 硬件 2FA 人工批准
Evil Martians 团队给的路子比较稳,核心是把 CI 和”最终发布权”拆开:
- CI 里跑
npm stage publish,而不是npm publish - 配置 trusted publishing,只允许
stage这个 action,把直接 publish 关掉 - 打开 npm 用户菜单里的 Staged Packages,或者用
npm stage approve,用硬件 2FA 手动批准
这样即使 CI 被拿下,攻击者也发布不出去,只能干等你自己发布。npm 现在还会在恶意软件扫描跑完之前锁住批准按钮,多了一道闸。
别忘了 provenance 不是免罪金牌
npm Provenance 回答的是”这个包是不是来自正确的流水线”,它不回答”这条流水线当时是不是干净的”。
2026 年 8 月有一波 npm 供应链蠕虫(payload 是个 5.4MB 的 JS 文件,用 XOR + AES-GCM 加密、下载 Bun runtime 偷凭证),攻击者利用的正是发布用的 GitHub Actions workflow 漏洞——生成的恶意 release 依然带着有效 provenance 签名。所以 provenance 可以看,但不能当信任证明。
今天就能落地的四步
- 检查你所有包的 trusted publisher 配置,逐条看它的 repo / workflow / environment / action,按并集理解你的真实权限
- 删掉所有失效配置,把 direct publish 关掉,只留 stage
- 给 workflow 加上 CI linter——CodeQL 或 zizmor(zizmor 是 Rust 写的,规则更多),避免
pull_request_target这类 pwn request - 把 CI 里的 action 版本从
@v7这种 tag 换成 commit SHA,因为 tag 不是不可变的,攻击者可以回头改你的 tag
最后一条最容易被忽略:修完 workflow 漏洞后,记得删掉旧分支。有团队修了主分支的 pull_request_target,却留着带旧 workflow 的历史分支——攻击者直接对着旧分支开 PR 就绕过去了。
一句话:多配置是能力,但也意味着你的信任边界需要审计,不是”配了就完事”。
评论区
登录后可评论。