一个被改写的 tag:两万多个仓库的密钥进了构建日志
2025 年 3 月,tj-actions/changed-files 的版本 tag 被指向恶意提交,CI 里的 token 被打印进日志。CVE-2025-30066 不是"某个包被投毒",而是 tag 可变这个默认习惯的账单。
2025 年 3 月,很多仓库的 CI 日志里突然出现了一段不该出现的输出:base64 编码的环境变量。它们来自一个几乎人人都用过的 GitHub Action —— tj-actions/changed-files。
漏洞编号 CVE-2025-30066,同期的 reviewdog/action-setup@v1 是 CVE-2025-30154。CISA 在 3 月 18 日发了供应链告警。
发生了什么
攻击者没有攻破 GitHub,也没有攻破 npm。他们改的是仓库里的 tag 指向:
- tj-actions/changed-files 的多个版本 tag 被重新指向一个恶意提交;
- 恶意代码在 Action 运行时把 CI 环境里的 token、密钥打印到 workflow 日志;
- 因为日志默认对仓库协作者可见,部分日志还被公开仓库暴露出去。
工作流里写 uses: tj-actions/changed-files@v45 的仓库,会在下一次运行时自动拉到被改写的提交。据事后统计,受影响的仓库数量在两万以上。
为什么 tag 能杀人
GitHub Actions 的 uses: owner/repo@ref 里,ref 可以是分支、tag 或完整 commit SHA。前两者都是可变的:
- 分支会前进;
- tag 可以被删除后重新指向另一个 commit。
这本来是 Git 的常规操作,但在“别人写的代码会在你的 CI 里以你的权限运行”这个场景下,可变引用等于把执行权交给上游的任意一次推送。更麻烦的是,CI 里的权限往往不小:仓库写权限、包发布权限、云平台凭据。
三条防线
- 钉死 commit SHA。
uses: tj-actions/changed-files@<40位SHA>,配 Dependabot 或 Renovate 来升级 SHA。这是唯一能挡住 tag 改写的做法; - 最小权限。workflow 顶层写
permissions: contents: read,需要什么再单独加;token 的默认权限越窄,泄露的代价越小; - 假设日志会公开。不要把密钥直接放进环境变量再让第三方脚本随便读;需要秘密时用 OIDC 换短期凭据,而不是长期 token。
另外两件事值得顺手做:审计 3 月以来的 workflow 运行日志,看有没有异常输出;把所有可能暴露过的 token 轮换一遍——包括 npm、Docker Hub 和云厂商的。
一句判断
供应链攻击最容易被误解成“我用的包很可信”。真实情况是:可信的包也可能在某一天被改指向,而你的 CI 会在无人值守的凌晨把它拉下来执行。把引用钉到 SHA,是这件事里成本最低、收益最确定的一步。
封面图:tj-actions/changed-files 仓库(GitHub)。

评论
…