
最近折腾了 GitHub Actions 的缓存机制,踩了一个安全策略升级的坑,这篇把问题说清楚。
缓存是 CI(持续集成)流水线里没人会做威胁建模、直到它咬你一口才后悔的那部分。它快、方便,大家都当它是只读状态,偏偏它是流水线里少数攻击者可以在不克隆仓库的情况下写入数据的地方。(你做缓存的威胁建模了吗?)
2026年6月26日,GitHub 发布了一条更新:"不可信触发的只读 Actions 缓存",把缓存写入重新定义为一种权限,而新的默认值是"不允许写入"。
想想默认分支上的一个缓存条目本质上是什么。一块数据,由 GitHub 存储,键名由你的工作流指定,下一个在默认分支上运行的 Job 会在执行前静默提取它。如果任何互联网上的人都能触发的事件被允许写入这块数据,那你的受保护分支现在实际上在执行一个陌生人构造的代码。签名提交策略拦不住这个。必须 review 也拦不住。这个产物永远不会出现在你的仓库里。它只是在下次构建运行到两分钟时悄悄出现在 ~/.cache 里。
GitHub 把这个变更描述为对 Actions 缓存应用最小权限原则。操作层面来说:当两个条件同时满足时,工作流的缓存令牌就是只读的。
GitHub 明确列出了不可信触发器:pull_request_target、issue_comment,以及 fork pull-request 的 workflow_run 级联触发。这些正是多年来"夺权请求(pwn request)"漏洞的粘合剂,因为它们允许 fork 的载荷以目标仓库的身份运行。现在,只读是它们的最低保障。
保持读写权限的列表也是明确的:push、schedule、workflow_dispatch、repository_dispatch、delete、registry_package 和 page_build。这些来自已经在信任边界内的主体,所以它们继续写入默认分支的缓存。非默认分支的范围(如 pull_request 和 release)也保持读写,因为攻击者无法利用它们的缓存范围来影响默认分支。
无需 opt-in。也没有开关可以切换。下次触发器激活时,这个变更就生效了。
对大多数团队来说,坏掉的是那些通过评论或 fork 触发的工作流——它们以前把填充缓存当作运行时的副作用。现在这个副作用没了。GitHub 的指导是把缓存保存移到本身由可信触发器(通常是 push)驱动的工作流中,让不可信的工作流读取可信工作流写入的内容。
最小化的拆分大概长这样:
# .github/workflows/cache-warm.yml
on: push: branches: [main]
jobs: warm: runs-on: ubuntu-latest steps: - uses: actions/checkout@<full-40-char-sha> - uses: actions/cache/save@<full-40-char-sha> with: key: deps-${{ hashFiles('package-lock.json') }} path: node_modules
然后 PR 目标工作流调用 actions/cache/restore@... 而不是 actions/cache@...。在可信的一侧写入。在不可信的一侧读取。fork 载荷做的任何事都不应该决定下周默认分支构建里会出现什么。
把缓存当作信任边界的问题并非 GitHub 独有。其他平台处理这个问题的方式各有不同。
policy: pull 设置可以把 Job 变成只读消费者。信任分离需要你自己在受保护分支和合并请求策略里接线。只读是地板,不是天花板。可信触发器内部的缓存污染仍然可能发生。键名仍然可以通过构建自身产生的文件内容被攻击者影响。一个不做验证就恢复缓存的工作流实际上是在运行 blob 里的任何东西。令牌上的最小权限是容易的那一半。验证取出来的东西仍然是你自己的责任。
信任缓存的程度,不要超过写它的那个人。