写过 npm 包的人都踩过这个坑——每次发布都要靠一个长年有效的 token,今天 GitHub 把这件事用 OIDC 彻底原生化了
写过 npm 包的人都踩过这个坑——每次发布都要靠一个长年有效的 token,今天 GitHub 把这件事用 OIDC 彻底原生化了
Shai-Hulud 事件之后,npm 的供应链安全终于有了真正的工程解法。
2025 年 9 月,npm 遭遇了以 Shai-Hulud 为代表的系列供应链攻击。黑客没有去挖漏洞,而是直接盗取维护者的长生命周期 npm token——因为 CI/CD 发布流程里必须要存一个永久有效的凭证,这个 token 一旦泄露,攻击者就可以随时「帮忙」发布新版本。
问题出在机制上,不在个人身上。
token 模式的核心风险
传统的 npm 发布依赖静态 token:CI job 需要一个 NPM_TOKEN,这个 token 通常有 months 甚至 years 的有效期,保存在 GitHub Secrets 里。问题不在于你保管得好不好,而在于这个 token 的「有效期」本质上是攻击者的「机会窗口」。2025 年 9 月那波攻击,几百个包被污染,根源都是 token 泄露。
业界早有替代方案:Trusted Publishing(OIDC)。原理很简单——让 GitHub 告诉 npm:「这次发布是我这个特定仓库的特定 workflow 发起的」,不需要任何静态 token。npm 验证 GitHub 的 OIDC token 之后,当场签发一个极短期的临时凭证,完成发布后立即失效。
但之前有一个硬限制:每个 npm 包只能配一个 OIDC 配置。如果你有 stable / prerelease / staging 三套发布流程,只有一个能享受 OIDC,其余还得靠 token。
2026 年 9 月 3 日,这个限制被解除了。
多配置 OIDC:每个发布通道独立授权
现在每个 npm 包可以配置最多 10 个 Trusted Publisher 配置,每个配置独立、互不干扰:
- 各自绑定自己的 repository、workflow、environment
- 各自决定是
npm publish(直接发布)还是npm stage publish(暂存待审批) - 互不限制,评估顺序不保证——设计业务逻辑时不要依赖「哪个配置先命中」
这个加法模型实际上是安全优势:你不用再为不同发布通道共用一个宽泛的 token,而是给每个通道画一个最小权限的圈。以前「只要有一个 workflow 被破,production 就裸奔」的风险,现在可以被物理隔离。
staged publishing:人工审批的最后一道锁
配合多配置,GitHub 还把 staged publishing(暂存发布)设为所有新配置的默认行为。暂存模式下,包先进入 npm 的审查队列,必须有维护者手动在 npmjs.com 上点击 Approve,包才会进入正式仓库。这步审批发生在 OIDC 验证之后,所以即使一个 CI workflow 被攻破,攻击者也只能把包放到暂存区,无法直接污染生产。
9 月 4 日的更新加了一个细节:审批按钮在 npm 恶意扫描完成之前是禁用的,扫描通过才释放。「恶意扫描 + 人工审批」两道关,比以前的纯 token 模式安全了不止一个量级。
迁移步骤:把 token 换成 OIDC
第一步:清点现有的发布路径。哪些 workflow 有 npm 发布权限?stable / prerelease / staging 分别用哪个?对应哪些 environment?把每个通道对应到一个 OIDC 配置。
第二步:在 npmjs.com → 包 → Settings → Publishing 上添加新配置。建议新配置全部设为 staging only(npm stage publish),验证 workflow 能正常提交暂存区。确认每条路径都走通了,再对真正需要无人值守发布的那个配置开启 direct publish。
第三步:移除 legacy token 路径。在包设置里,把「Tokens」页签下曾经填过的 NPM_TOKEN 全部撤销。保留 read-only token 用于 install 私有依赖,但发布权限必须走 OIDC。
第四步:workflow 里加 id-token: write 权限。在需要 OIDC 认证的 job 里加上这行:
permissions:
contents: read
id-token: write # 核心:允许 GitHub 为这个 job 签发 OIDC token
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
- name: Publish npm package
run: npm publish --access public
注意:publish job 里不要 npm install 任何第三方依赖,也不要在同一个 job 里跑 lint / test / build——这些步骤里的任何第三方工具理论上都可以调用 npm publish,只有 job 本身的权限才是干净的。必要时把 build 和 publish 拆成两个 job,用 artifact 传产物。
限制与注意事项
目前 OIDC 认证仅支持 GitHub-hosted runners(ubuntu-latest、macos-latest 等),self-hosted runners 暂不支持。如果你用自建 runner 跑发布,暂时还得保留 token 方案,等 GitHub 后续支持。PyPI / RubyGems / GitHub Packages 等其他 registry 的 Trusted Publishing 机制大同小异,思路可以迁移。
下一步
以下几个动作,今天就可以做:
- 在团队内部发起 npm 包发布安全审计,确认哪些包还在用 token
- 对还在用 token 的包,按上述步骤逐个迁移
- 把「所有新包默认 staging only」作为团队规范,direct publish 需要单独申请
npm 供应链安全这件事,从「被动改密码」到「主动用 OIDC」,这个拐点已经过去了——工具链已经ready,剩下的就是迁移成本。
评论区
登录后可评论。