Wiz 分析:GitHub Actions 2026 防护更新后 Pwn request 攻击面仍存
Wiz 分析 GitHub Actions 2026 防护更新后仍可利用的 Pwn request 攻击面
Wiz 研究显示,GitHub 2026 年针对 Pwn request 的防护(checkout 检出保护、只读 cache token、事件策略)让默认利用显著变难,但未消除该漏洞类别。
Wiz:研究· Shay Berkovich·2026-10-26 21:24· 1 小时前AI 评分53
Pwn Request 清算:哪些攻击对 GitHub Actions 仍然有效
AI 导读
Wiz 发布 GitHub Actions 系列研究第三部分,评估 GitHub 2026 年针对 Pwn request 的平台防护效果:checkout 检出保护、只读 cache token 和事件策略让默认利用显著变难,12 月 2 日起公共仓库将默认阻止无显式事件策略的 pull_request_target。
正文 · 原文
多年来,pull_request_target 一直是典型的 Pwn request 攻击向量:这一工作流触发器会以仓库密钥运行,却可被任何从 fork 提交拉取请求的人触发。2026 年,GitHub 通过平台变更做出回应,使默认场景下的利用难度显著提高。
这些变更代表着真正的进步。checkout 保护、只读 cache token 和事件策略封堵了使利用变得轻而易举的路径。但它们并未消除这一漏洞类别,而是将其转移。此前利用任意 pull_request_target 工作流的攻击者,如今需要寻找显式选择加入、过时的 checkout 版本、未受保护的触发器或脚本注入。
在我们 GitHub Actions 研究的第一部分中,我们阐述了威胁模型和核心防御措施,第二部分则涵盖了 AI 驱动的 action。本文针对 2026 年更新该威胁模型:GitHub 的变更实际防护了什么、缺口仍在哪里,以及防御者需要如何调整。文末的审计清单涵盖了应在自身工作流中检查的内容。
2026 年安全大修:变更时间线
认识到 Pwn request 带来的普遍风险,GitHub 在 2026 年通过分阶段推出平台级防护和 action 更新,系统性地弥合了这一缺口。
| 日期 | 里程碑与所采取的行动 |
|---|---|
| 2026 年 6 月 18 日 | 发布 actions/checkout@v7.0.0,在不可信触发器(pull_request_target,以及由拉取请求触发的 workflow_run)中默认阻止检出 fork 代码。 |
| 2026 年 6 月 26 日 | GitHub 在平台层面为不可信触发器引入只读 cache token,防止缓存投毒攻击。 |
| 2026 年 7 月 20 日 | 将 fork 检出保护回溯移植到 actions/checkout 的所有活跃主版本(v2.8.0、v3.7.0、v4.4.0、v5.1.0 和 v6.1.0;v1 除外)。引用可变版本标签(例如 @v4)的工作流会自动继承这些防御措施。 |
| 2026 年 9 月 10 日 | 通过 cache-mode 实现的细粒度缓存控制正式可用(GA)。 |
| 2026 年 9 月 17 日 | 工作流执行保护正式可用(GA),支持细粒度事件策略和参与者规则。 |
| 2026 年 12 月 2 日 | [计划] GitHub 开始强制执行默认事件策略,在公共仓库中阻止未设置显式事件策略的 pull_request_target。该规则自 9 月 17 日起处于评估模式。私有和内部仓库不受影响。 |
残余风险:仍然有效的手段
尽管这一系列平台更新显著增强了 GitHub Actions 的默认安全配置,但它们并未消除所有风险。
12 月 2 日的强制执行是默认行为,而非禁令。那些自动化依赖 pull_request_target 的维护者(打标签、预览部署、带密钥的 fork CI)会添加一条允许该事件的事件策略。而这些恰恰正是以高权限处理不可信输入的工作流。默认策略也不适用于私有和内部仓库,也不适用于 workflow_run。本节剩余部分将介绍这些幸存的工作流如何仍可能被利用。
SHA 固定:安全悖论
我们研究中最令人不安的发现之一是,标准的供应链加固建议——具体来说,将 action 固定到确切的提交 SHA——竟会无意中绕过 GitHub 的平台防御。
# "Security-hardened" workflow: SHA-pinned for supply chain safety
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0由于 GitHub 回移的保护依赖于嵌入在 actions/checkout 本身的逻辑,固定到 2026 年 7 月 20 日回移版本之前的 SHA 值意味着工作流运行的是没有任何安全检查的旧代码。我们在各种工作流触发器上的实证测试证实了这一差异:引用可变标签如 @v4(解析为 v4.4.x)或固定版本如 @v4.4.0 仍受保护,而较旧版本如 @v4.3.0 或指向旧版本的 SHA 固定引用(如 @08eba0b... 解析为 v4.3.0)则不提供任何保护。
为保持保护,使用 SHA 固定的安全团队必须确保其引用更新到匹配或超过最低修补版本:v2.8.0、v3.7.0、v4.4.0、v5.1.0、v6.1.0 或 v7.0.0+。
自定义检出脚本与第三方 action
第二个缺口涉及完全绕过 actions/checkout 的工作流。GitHub 的保护是在该 action 的代码中实现的——任何其他获取代码的方法都不受限制地运行。直接调用 CLI 完全绕过保护:
- env:
GH_TOKEN: ${{ github.token }}
PR_REPO: ${{ github.event.pull_request.head.repo.full_name }}
PR_SHA: ${{ github.event.pull_request.head.sha }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
# Any one of these fetches fork code without actions/checkout's protections
# Bypass 1: Direct git clone
git clone "https://github.com/${PR_REPO}" pr-code && git -C pr-code checkout "${PR_SHA}"
# Bypass 2: gh pr checkout (runs in the base repo's workspace)
gh pr checkout "${PR_NUMBER}"
# Bypass 3: gh repo clone
gh repo clone "${PR_REPO}" fork-code第三方检出 action 实现自己的 git 逻辑,因此它们不会继承这些保护。任何配置了 PR head ref 的此类 action 都将不受限制地检出 fork 代码。
未受保护的特权触发器
GitHub 的检出保护仅覆盖 pull_request_target 和 workflow_run:
在 pull_request_target 之外的其他事件类型(如 issue_comment)中触发的 pwn request 不会被此变更阻止。未来版本可能会探索对更多事件进行进一步加固。
GitHub 变更日志
这造成了一致性上的安全边界:GitHub 的缓存控制明确将 issue_comment 视为低信任触发器,而检出保护则不然。使用 issue_comment 处理机器人命令(/deploy、/test、/cherry-pick)的工作流在 checkout v7 下仍可被完全利用——无需 allow-unsafe-pr-checkout 标志:
on:
issue_comment:
types: [created]
jobs:
deploy:
if: github.event.issue.pull_request && contains(github.event.comment.body, '/deploy')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
ref: refs/pull/${{ github.event.issue.number }}/merge # Checks out fork PR code其他特权触发器也有同样的缺口。repository_dispatch 同样在带密钥的情况下运行,当工作流使用受攻击者影响的负载数据来选择检出 ref 时,它就可被利用。
在 GitHub 扩展检出保护之前,这些工作流必须强制执行自己的授权,并且授权需要从事件触发的那一刻起一直保持到代码运行:
- 验证是谁触发的。对于评论命令,检查评论者实际的仓库权限(getCollaboratorPermissionLevel,write 或以上)。不要依赖 author_association,它不反映当前权限。对于 repository_dispatch,限制谁持有可发送该事件的令牌,并且不要将负载视为发送者身份的证明。
- 固定已批准的版本。refs/pull/N/merge 是可变的,因此攻击者可以在维护者评论 /deploy 之后推送恶意提交。请检出确切的 head SHA,如果 PR 在评论发布后发生更新,则中止操作。
- 将事件负载视为不可信输入。使用严格且锚定的允许列表正则表达式验证评论参数和 dispatch 负载字段,并通过环境变量将它们传递给 shell,绝不要通过 ${{ }} 插值传递。
脚本注入:无需检出即可发起 Pwn Request
一个常见的误解是,pull_request_target 漏洞需要检出 fork 代码。实际上,攻击者无需任何检出步骤,通过脚本注入即可实现代码执行。
当工作流作者使用 ${{ }} 表达式将事件上下文直接插值到 shell 命令中时,GitHub 会在 shell 运行之前将值替换到脚本中,因此攻击者控制的数据会成为命令本身的一部分:
on: pull_request_target
jobs:
greet:
runs-on: ubuntu-latest
steps:
- run: echo "Thanks for PR: ${{ github.event.pull_request.title }}"攻击者只需构造一个带有恶意标题的拉取请求:
"; touch /tmp/pwned; echo INJECTED #
引号闭合了字符串,分号结束了 echo,其后的所有内容都会作为新命令运行,并携带该作业的密钥和令牌。GitHub 2026 年的变更都没有触及这一点:没有可阻止的检出,漏洞在于工作流如何处理不可信输入。
修复方法是绝不将不可信输入插值到脚本中。改为通过环境变量传递,这样 shell 会将其视为数据:
- env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: echo "Thanks for PR: $PR_TITLE"$PR_TITLE 周围的双引号很重要。如果没有它们,shell 仍会对该值进行分词和通配符展开。
持久化数据通道:超越缓存令牌
GitHub 的只读缓存令牌保护 Actions 缓存服务,但工作流仍可通过不受影响的通道交换数据。
制品投毒
upload-artifact 和 download-artifact 操作独立于缓存令牌运行。经典的攻击链使用无特权的 pull_request 工作流上传攻击者控制的构建输出,然后使用有特权的 workflow_run 下载并执行它们:
# .github/workflows/build.yml - unprivileged, runs the fork's code
name: Build
on: pull_request
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- run: npm ci && npm run build # Attacker-controlled
- uses: actions/upload-artifact@v4
with:
name: build-output
path: ./dist/
# .github/workflows/deploy.yml - privileged, has secrets
name: Deploy Preview
on:
workflow_run:
workflows: ["Build"]
types: [completed]
jobs:
deploy:
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: build-output
path: ./dist/
run-id: ${{ github.event.workflow_run.id }} # Fetches the fork run's artifact
github-token: ${{ secrets.GITHUB_TOKEN }}
- run: ./dist/deploy.sh # Executes attacker-controlled file
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }} # Exposed to attacker code这正是 checkout v7 旨在阻止的 workflow_run 场景,而它在这里无济于事。有特权的工作流从不检出 fork 代码;它下载代码。与缓存条目不同,平台没有等效机制将来自不可信运行的制品标记为受污染,因此任何下载并执行来自较低信任运行制品的工作流都会继承该风险。
自托管运行器状态
在非临时性自托管运行器上,文件系统状态会在作业之间持续存在。本地包管理器缓存(~/.npm、~/.cache/pip)、构建目录和已安装工具位于运行器磁盘上,在 Actions 缓存服务之外,因此只读缓存令牌无法覆盖它们。在不可信触发器下运行的攻击者代码可以写入这些位置,并污染同一台机器上计划的下一个可信工作流,包括发布和出版作业。对于任何可由不可信事件触发的工作流,请使用临时运行器(每个作业后会被清除),并将不可信和可信工作流保留在单独的运行器组中。
挥之不去的误解
许多安全团队认为他们还有另一项保障措施:设置 -> Actions -> General 下的“Require approval for all external contributors”设置。如果启用,fork PR 需要维护者批准才能运行工作流。问题解决了,对吧?
不完全是。该设置只管控 pull_request 工作流,也就是运行 fork 自身工作流 YAML 的那些。对于 pull_request_target,GitHub 认为工作流定义是可信的(它来自基础仓库),因此审批设置不适用,运行会立即开始。
讽刺意味很深:pull_request_target 是更危险的触发器(可访问密钥、写令牌),却恰恰是跳过审批要求的那个。如果某个 pull_request_target 工作流需要人工把关,就把特权作业放在带有必需审核者的环境之后,或用 Workflow Execution Protections 的 actor 规则限制谁可以触发它。
显式选择加入:allow-unsafe-pr-checkout
最后,需要与 fork 代码交互的开发者可以通过启用 allow-unsafe-pr-checkout 标志来显式禁用检出保护:
- uses: actions/checkout@v7
with:
ref: ${{ github.event.pull_request.head.sha }}
allow-unsafe-pr-checkout: true # Re-enables untrusted fork checkouts虽然这个参数名故意起得令人警觉,但执着的开发团队或第三方工作流仍可能使用它来维持遗留自动化,实际上重新打开了与之前完全相同的执行途径。安全团队应特别关注该标志的任何使用。
变化之处
总而言之,GitHub 2026 年的安全推广值得肯定。默认事件策略、检出保护和只读缓存令牌让简单的 Pwn request(fork、PR、获利)变得困难得多。从 12 月 2 日起,除非事件策略明确允许,pull_request_target 将不再在公共仓库中运行。通过 Actions 缓存进行的缓存投毒得到缓解。这些是实实在在的胜利。
但这类漏洞并未被消除。它转移了。仍然可被利用的包括:通过 allow-unsafe-pr-checkout 显式选择加入、早于 2026 年 7 月回溯移植的 SHA 固定版本、绕过操作级控制的自定义检出脚本、完全不需要检出的脚本注入、issue_comment 和 repository_dispatch 等未受保护的触发器、从 pull_request 到 workflow_run 的产物投毒链、非临时性自托管运行器上的持久状态,以及默认策略未覆盖的私有或内部仓库。攻击面缩小了,但剩下的部分更难发现。
如何应对
首先审计你已经在运行的内容,然后在 12 月 2 日强制执行之前做好准备。
审计你的工作流
Wiz Code 会自动扫描 GitHub Actions 工作流中的 Pwn request、脚本注入、版本缺口和风险配置,以覆盖这一残余攻击面。或者,使用 zizmor 等开源工具,甚至这些基本 grep 搜索来开始审计:
# Flag risky triggers, unsafe checkouts, outdated checkout versions, artifact downloads,
# self-hosted runners and event data in expressions (env: usage is safe), then review each hit
grep -rnE 'pull_request_target|workflow_run|issue_comment|repository_dispatch|allow-unsafe-pr-checkout|self-hosted|download-artifact|gh (pr checkout|repo clone)|git (clone|fetch)|refs/pull|actions/checkout@v(1|2\.[0-7]|3\.[0-6]|4\.[0-3]|5\.0|6\.0)([^0-9]|$)|\$\{\{[[:space:]]*github\.(event\.|head_ref)' .github/workflows/此外,运行此脚本将每个 SHA 解析为其发布标签,然后与已修补版本进行比较:
tags=$(git ls-remote --tags https://github.com/actions/checkout)
grep -rhoE "actions/checkout@[0-9a-f]{40}" .github/workflows/ | cut -d@ -f2 | sort -u |
while read -r sha; do
tag=$(echo "$tags" | grep "^$sha" | sed 's#.*refs/tags/##; s#\^{}$##' | sort -V | tail -1)
echo "$sha ${tag:-UNKNOWN - not a release tag, check manually}"
done为 12 月 2 日 pull_request_target 强制执行做准备
从 12 月 2 日起,默认事件策略将阻止没有显式事件策略的公共仓库中的 pull_request_target。workflow_run 不受影响,私有或内部仓库也不受影响。在该日期之前:
检查策略洞察,查看哪些 pull_request_target 运行会被阻止。
逐个工作流决定它是否真的需要 pull_request_target。如果不需要,就将其改为 pull_request。如果仍需要特权后续步骤,可以由 workflow_run 工作流处理,但它必须将 PR 运行的产物视为不可信数据,绝不执行它们。
对于确实需要 fork 访问权限的工作流,应创建显式的事件策略,并使用工作流文件定向,仅允许这些文件使用 pull_request_target。将允许列表中的每个文件都视为高风险:在添加之前先通过上述审计,并在文件发生变更时重新审计。
来源:Wiz:Research · wiz.io
来源:RadarAI · aihot.news