不上传新包,也能改变用户拿到的版本:npm dist-tag 的 OIDC 权限治理
一、最新变化:标签管理成为独立授权项
GitHub 官方更新说明确认,新旧 trusted publisher 配置中的 Allow npm dist-tag 默认关闭;它与直接发布权限相互独立,暂存配置也可单独获得标签管理能力。匹配任意一条已启用该权限的配置,即可获得相应授权。
这是一项 2026-09-30 的产品安全能力更新,没有关联的漏洞编号、受影响版本范围或已确认攻击事件。不能写成 npm 刚修复了一个标签劫持 CVE。
npm 官方文档补充了客户端门槛:dist-tag OIDC 支持要求 npm 11.21.0+ 的 11.x 分支,或 12.2.0+ 的 12.x 分支。这比 trusted publishing 基础能力的版本门槛更高,不能只确认"支持 OIDC"就认为标签命令也支持。
二、技术原理:制品身份和渠道选择是两个对象
下面是工程分析。
一个已经发布的包版本可以保持内容不变,而标签从版本 A 指向版本 B。用户如果选择标签,就可能拿到另一份制品。对此,审计只验证"没有新增版本"并不充分,因为供应链的选择关系已经改变。
可以把发布治理拆成三种能力:
| 能力 | 主要控制对象 | 工程审计重点 |
|---|---|---|
| 生成或上传候选制品 | 待发布内容 | 来源、构建身份、内容检查 |
| 批准公开发布 | 版本可见性 | 审批人与批准对象一致 |
| 移动发布标签 | 渠道指向 | 旧值、新值、变更身份和理由 |
表格是建议采用的治理模型,并非 npm 角色系统的完整描述。它帮助团队避免用一句"这个工作流只能做发布"掩盖多个不同权限。
1. 短期身份减少长期密钥,但不缩小错误授权
OIDC 能让流程使用短期身份,减少长期写令牌的保存与轮换负担。然而,如果一个不应控制正式渠道的工作流被授予 dist-tag 能力,那么短期凭据仍可能在有效期内执行错误操作。
所以评审重点要从"凭据放在哪"进一步扩展为"什么仓库、什么工作流、什么环境能获得什么能力"。凭据时效和授权范围是两个独立维度。
2. 多配置下应审计权限并集
如果包绑定多条可信发布配置,不能只看其中最严格的一条。只要另一条匹配路径拥有更多能力,实际可达权限就可能扩大。
举例说,团队希望候选构建只暂存包,但给同一个身份又匹配了一条可管理标签的规则。单看第一条规则,评审者会认为它不能影响正式渠道;看完整配置集合,结论可能不同。
3. 回滚也属于发布控制
将标签指回旧版本看起来像恢复操作,但旧版本可能带有已修复缺陷或不兼容行为。因此回滚也应保留目标版本的安全检查与审批证据,不能把所有"向后移动"都定义成低风险。
三、安全实验:在内存里计算权限并集
以下例子不申请 OIDC Token、不访问 npm、不修改任何标签。它只展示为什么多条匹配规则必须一起检查。
python
def effective_actions(identity, configs):
result = set()
for rule in configs:
if rule["identity"] == identity:
result.update(rule["actions"])
return result
configs = [
{"identity": "candidate-job", "actions": {"stage"}},
{"identity": "release-job", "actions": {"dist-tag"}},
]
assert effective_actions("candidate-job", configs) == {"stage"}
assert "dist-tag" in effective_actions("release-job", configs)
# 模拟一次错误授权;没有对真实平台进行配置。
configs.append({
"identity": "candidate-job",
"actions": {"dist-tag"},
})
assert effective_actions("candidate-job", configs) == {"stage", "dist-tag"}
assert effective_actions("unknown-job", configs) == set()
print("4 permission checks passed; registry unchanged")
真实 OIDC 验证还包含签名、受众、有效期和提供方声明匹配。这里的字符串 identity 仅用于解释权限组合,不可代替 Token 验证器。
团队可以把同样的思路用于配置审计:将每个包的可信发布者展开,计算某个工作流最终可以做什么,再和职责表比较。审计产物应是"身份---包---动作"的矩阵,而不是只有"已开启 OIDC"的勾选项。
四、迁移方案:先验证新路径,再撤销旧权限
阶段一:建立基线
盘点哪些工作流上传候选包,哪些负责正式发布,哪些维护标签。记录标签当前指向、操作身份和应急流程。特别注意单独的回滚脚本,长期令牌往往藏在这些低频路径中。
阶段二:只给必要身份授权
按职责启用 dist-tag 权限。不要为了让一个命令不报错,给所有可信发布配置都打开相同能力。候选构建与正式渠道管理应尽量分离,正式操作前验证目标版本和审批对应关系。
阶段三:验证具体操作
官方文档提醒,npm whoami 不是 trusted publishing 权限验证方法。测试应针对实际目标操作,在专门测试包和受控标签上进行;私有包读取标签的权限要求,也不能由公开包的可读性推导。来源:npm 文档
本文不提供自动修改真实标签的命令串。实际演练必须明确测试包、允许的版本及恢复方案,避免误触生产渠道。
阶段四:撤销不再需要的长期写凭据
仅删除 CI 中的变量不代表令牌失效。验证 OIDC 路径后,应在凭据签发侧撤销已不需要的令牌,并保留凭据标识、用途和撤销时间,避免它在别处仍能使用。
这一步必须考虑其他业务依赖,例如安装私有依赖可能仍需要单独的只读认证。迁移标签写权限,不等于所有 npm 操作都已经无令牌化。
五、研发与安全团队行动清单
- 发布工程:区分上传、批准、标签三个动作,记录每次渠道变化的前后版本。
- 安全团队:审计所有可信发布配置的可达权限,不只检查主要工作流。
- 仓库管理员:保护能够改变可信工作流和发布环境的修改,关注外部贡献是否能影响最终执行内容。
- 消费方团队:对生产部署固定经审核的依赖输入;渠道变化不应绕过既有更新审批。
- 应急负责人:演练回滚目标校验,确保"旧版本"不会自动绕过漏洞检查。
六、总结
标签不是无关紧要的元数据,它决定一部分用户最终获得哪个版本。此次更新使长期标签令牌有机会退出流水线,但安全收益取决于能力是否正确分配。把制品安全和渠道安全分开审计,才能避免"包没变,所以发布没变"的盲区。