写过 CI 的人都踩过这个坑——缓存投毒年年防年年有,今天 GitHub 用一个配置把这件事彻底原生化了
用过 GitHub Actions 缓存的都踩过这个坑——PR 跑完把缓存写了,下次跑时直接读,结果里面被人塞了恶意内容。听起来像 CVE,其实在 2025-2026 年已经是真实攻击路径了。今天 GitHub 给所有计划的用户全部上线了 cache-mode,让每个 workflow 和 job 按需拿缓存权限,从根上断了这条攻击链。
攻击路径:共享缓存是怎么变成武器的
GitHub Actions 的缓存按 key 和分支共享——默认分支的缓存所有分支都能读。低权限 workflow(比如 PR 里的脚本注入,或者带 prompt injection 的 AI agent)写一个恶意 cache entry,下一个有写权限的 workflow(比如定时构建、发布 job)读进来,执行了攻击者控制的代码。安全研究者 Adnan Khan 在多个会议上演示了这个路径,称为缓存投毒(cache poisoning)。
这就是为什么 2026 年 GitHub Actions 密集推出一系列供应链安全更新——Trusted Publishing OIDC、CI 恶意检测门控、Workflow Execution Protections——而 cache-mode 是运行时访问控制这环的最后一块。
cache-mode 四种模式
GitHub Actions 缓存现在支持四种权限模式,按最小权限原则分配:
read:允许 restore,不允许 save。低可信事件的默认行为,如 pull_request_target、pull_request_review_requested。
write:允许 restore 和 save。可信事件的默认行为,如 push、schedule。
write-only:允许 save,不允许 restore。用于定时预热或清理缓存,但不读取既有 cache——彻底避免投毒风险。
none:禁止所有缓存访问。最高隔离级别,用于高敏感 job。
jobs:
# 只读,防止低权限 job 污染缓存
security-audit:
cache-mode: read
steps:
- uses: actions/checkout@v4
- run: npm audit
# 正常工作流,保留读写
build:
cache-mode: write
steps:
- uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles("**/package-lock.json") }}
# 只写不读,定时预热缓存
cron-warmup:
cache-mode: write-only
steps:
- uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-latest
三个关键细节
Job 级设置覆盖 workflow 级。 在 workflow 顶部声明了 cache-mode: write,某个特定 job 仍然可以单独声明 cache-mode: read 来限制自己。这个粒度让大型 monorepo 可以给不同的 job 分配不同权限。
缓存服务层强制执行。 设置不是靠约定,而是缓存服务直接校验 token 权限——不可绕过。更重要的是:reusable workflow 继承调用者的权限上限,被调用方拿到的权限不会超过调用方声明的值。
对低可信事件显式开 write 会触发警告。 如果你在 pull_request_target 上声明 cache-mode: write,GitHub Actions 会在 workflow 运行页加一条警告 annotation,提醒你这个配置增加了缓存投毒风险。
怎么迁入
已有 workflow 不设置 cache-mode 继续用现有安全默认值(低可信事件 read,可信事件 write)。要迁移只需要在 job 或 workflow 层级加一行 cache-mode 声明,actions/cache@v4 的使用方式完全不变。
# workflow 级默认
permissions:
contents: read
jobs:
untrusted-pr:
permissions:
contents: read
cache-mode: read
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles("**/package-lock.json") }}
restore-keys: |
${{ runner.os }}-npm-
trusted-push:
cache-mode: write
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles("**/package-lock.json") }}
为什么这件事值得认真对待
缓存投毒不是理论风险。攻击路径很直接:恶意 PR 提交者写一个含恶意代码的 lock 文件,触发 CI 写缓存;下一个有写权限的定时构建或发布流程读进来,代码就执行了。整个过程不需要任何凭证泄露——靠的是共享缓存的信任传递。
在 AI coding agent 大规模使用的背景下,这个风险被放大了。Agent 在 PR 里跑 npm install 写缓存,本身就是低可信事件——prompt injection 可以让 agent 悄悄写入任意内容。cache-mode: read 把这条路彻底堵死。
下一步
如果你的 repo 还没配 cache-mode:先给所有 pull_request_target job 加上 cache-mode: read;然后把高敏感 job(发布、定时构建)声明 cache-mode: write;最后评估有没有需要 write-only 的预热 job。
GitHub Actions 供应链安全的拼图正在一块块补全。从触发前拦截、到运行时检测、再到访问控制——每一块都在缩小攻击面。
评论区
登录后可评论。