一个被改写的 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 里的权限往往不小:仓库写权限、包发布权限、云平台凭据。

三条防线

  1. 钉死 commit SHA。uses: tj-actions/changed-files@<40位SHA>,配 Dependabot 或 Renovate 来升级 SHA。这是唯一能挡住 tag 改写的做法;
  2. 最小权限。workflow 顶层写 permissions: contents: read,需要什么再单独加;token 的默认权限越窄,泄露的代价越小;
  3. 假设日志会公开。不要把密钥直接放进环境变量再让第三方脚本随便读;需要秘密时用 OIDC 换短期凭据,而不是长期 token。

另外两件事值得顺手做:审计 3 月以来的 workflow 运行日志,看有没有异常输出;把所有可能暴露过的 token 轮换一遍——包括 npm、Docker Hub 和云厂商的。

一句判断

供应链攻击最容易被误解成“我用的包很可信”。真实情况是:可信的包也可能在某一天被改指向,而你的 CI 会在无人值守的凌晨把它拉下来执行。把引用钉到 SHA,是这件事里成本最低、收益最确定的一步。

封面图:tj-actions/changed-files 仓库(GitHub)。

← 返回文章列表

评论

…