Hotdry.
systems-engineering

GitHub Actions 内建包管理器的设计缺陷与供应链攻击面

通过近期 tj-actions 供应链事件拆解 GitHub Actions 在版本锁定、运行器隔离与包缓存上的三大设计缺陷,并给出可立即落地的缓解参数与清单。

2025 年 3 月,仅一个被泄露的机器人 PAT(Personal Access Token)就让 tj-actions/changed-files 这个明星 Action 全线沦陷:攻击者把恶意 commit 强制推到历史标签,2.3 万个仓库的 CI 机密被转储到公开日志,企业密钥在几小时内被搜索引擎收录。事件看似是 “令牌保管不善” 的锅,但真正放大冲击波的,却是 GitHub Actions 内建 “包管理器” 长期缺失的三条安全契约:不可变版本、隔离执行、可验证出处。

一、可变标签:官方默认就允许 “穿越”

npm、PyPI 在 2020 年后相继关闭了包的 “重新发布” 开关 —— 版本一旦上传,hash 永不可变。反观 GitHub Marketplace,任何 Action 维护者仍可用 git tag -f 把 v3.0.0 指向全新 commit,平台不做任何二次确认,更不会像容器 registry 那样强制签名。结果就是:

  • 用户以为 uses: tj-actions/changed-files@v44 是 “稳定引用”,实则背后哈希可被维护者或拿到 PAT 的攻击者任意移动;
  • 一旦恶意代码被推到旧标签,所有历史工作流在下次触发时自动升级,毫无预警。

缓解参数

- uses: tj-actions/changed-files@daef52d   # 固定 commit SHA

企业级加固:在组织级启用 Actions allow-list,仅白名单 SHA 或自家 Fork 可执行。

二、Runner 环境:机密明文注入,脚本即宿主

GitHub 托管的运行器为了兼容 99% 的存量脚本,把 Secret 直接展开成 shell 变量,攻击者只需在 PR 里植入一行 echo ${{ secrets.GITHUB_TOKEN }} 就能把令牌打印到日志;更隐蔽的写法是 echo 'MALICIOUS' >> $GITHUB_ENV,后续步骤通过环境变量读入,实现 “跨步骤污染”。

2025 年 FreeBuf 的实测显示,自托管 runner 上 /home/runner/work/_temp/*.sh 脚本可被同机监听进程实时读取,机密在落地前就已暴露。

缓解参数

permissions:
  contents: read          # 最小化 GITHUB_TOKEN 范围
  actions: none
env:
  NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}  # 仅在需要步骤内注入

进阶方案:把敏感步骤移入容器化 Job Service,启用 jobs.<id>.container.options: --read-only --tmpfs /tmp 只读根文件系统,阻断回写与命令注入。

三、包缓存:lockfile 即真理,却无人验真

actions/setup-nodesetup-python 等官方 Action 默认开启 package-manager-cache: true,缓存 key 仅由 lockfile 哈希生成。攻击者在 PR 中替换 package-lock.json 即可让缓存命中自家恶意包,CI 无需下载就原地 “投毒”。

GitHub 目前没有类似 npm audit 的 “缓存前校验” 钩子,也不支持把缓存 key 与 SBOM 签名绑定。

缓解参数

- uses: actions/setup-node@v5
  with:
    node-version: 20
    cache: npm
    package-manager-cache: false   # 关闭自动缓存

替代方案:自建 “干净缓存桶”,在私有 runner 上预装官方依赖 tar,统一通过 npm ci --offline 安装;或启用 Sigstore cosign 在 install 前验证包签名。

四、没有出处就没有信任

GitHub Packages 与 Actions 之间尚无原生 SLSA provenance,下游无法判断 “是谁、在哪、用什么” 构建了包。tj-actions 事件中,攻击者正是利用出处真空,把恶意代码伪装成 “官方发版”,而消费者侧没有任何自动化手段可验证。

可落地清单

  1. 在发布工作流里集成 slsa-framework/slsa-github-generator,自动生成符合 SLSA L3 的 provenance.json;
  2. 消费侧使用 slsa-verifier 校验出处,失败即阻断 CI;
  3. 把 provenance 哈希写入 GitHub Deployment API,实现 “发布 — 验证” 闭环。

五、小结:缺的不是功能,而是契约

GitHub Actions 作为事实上的 “全球最大包管理器”,日均触发量早已超过 npm 下载量,却仍在用 2019 年的 “信任所有人” 模型运行 2025 年的供应链。可变标签、无隔离执行、无出处校验 —— 三大设计缺陷叠加,让一次 PAT 泄露就能演变成 2 万仓库的密钥雪崩。

短期,把 “固定 SHA + 最小权限 + 关闭缓存” 三条红线写进 CI 模板即可挡住 90% 的 opportunistic 攻击;长期,平台必须提供不可变版本命名空间、默认沙盒运行器与原生 SLSA provenance,否则下一次 tj-actions 只是时间问题。


参考资料

  • InfoQ《GitHub 遭入侵凸显 CI/CD 供应链风险》,2025-05
  • CSDN《热门 GitHub Action 遭供应链攻击,2.3 万代码库受影响》,2025-03
查看归档