作者:互联网 时间: 2026-07-21 18:26:03
actions/checkout v7 中默认拦截 pull_request_target / workflow_run 事件里常见的 Fork PR Head/Merge Checkout 模式,但组织里的旧工作流、第三方 Action、Artifact 与手工 Git 操作并不在新保护覆盖范围内。| 功能 | 状态 | 说明 |
|---|---|---|
actions/checkout v7 2024-06-18 默认拦截 pull_request_target / workflow_run 中 Fork Checkout | ✅ 已验证 | 2024-06-18 发布,2026-06-29 腾讯新闻等多源报道确认其针对 Pwn Request 强化 |
allow-unsafe-pr-checkout: true 作为显式豁免 | ✅ 已验证 | v7 更新日志明确:必须显式声明才能跳过拦截 |
浮动主版本(如 @v4)可在 Tag 更新后解析到新实现 | ✅ 已验证 | Renovate 在 2026-06-18 自动升级到 @v7 的 PR 已存在 |
| 固定 Commit SHA、Minor 或 Patch 不会自动获得回移 | ✅ 已验证 | GitHub 官方文档与本文工程推导一致 |
pull_request 与同仓库 PR 的常规路径不在此次行为变化范围 | ✅ 已验证 | GitHub Docs 关于 pull_request_target 安全的说明 |
workflow_run、issue_comment、Artifact、Cache、手工 git fetch 也在 Pwn Request 攻击面内 | ✅ 已验证 | TeamPCP 蠕虫 5-10/5-11 攻击链 + Grafana 5-16 事件均涉及 |
| 2026-07-20 为"7-15 编辑注调整后的回移日期" | ⚠️ 待官方公告核验 | 文章内部标注,未取得 GitHub 官方 changelog 直接确认 |
| v1 不在本次回移范围 | ⚠️ 待验证 | 文章说法,未在官方文档中独立确认 |
| TeamPCP / Shai-Hulud 攻击波及 ~160 个 npm 包,@tanstack/react-router 周下载 ~1200 万 | ✅ 已验证 | Socket 5-11 通报 + TanStack/Mistral AI 复盘文章 |
| Grafana 2026-05-16 被 CoinbaseCartel 窃取 4 个私有仓库 | ✅ 已验证 | Grafana 5-17 X 公告 + CSDN 深度复盘 |
| GitHub 自身约 3800 个内部仓库因恶意 VSCode 扩展被 TeamPCP 窃取 | ✅ 已验证 | 搜狐 2026-05-21 报道,与 Pwn Request 是不同事件 |
npm ci --ignore-scripts 只能减少一类风险 | ✅ 已验证 | 通用供应链最佳实践,本文采用 |
| OIDC Subject 限制 Repo/Ref/Environment 作为防御手段 | ✅ 已验证 | GitHub Docs OIDC 配置通用做法 |
pull_request_target 不是一个"更强的 pull_request"。它运行在基础仓库的可信上下文中,通常可以获得基础仓库的 GITHUB_TOKEN、Secret 和默认分支 Cache。如果工作流又把未经审查的 Fork PR 代码拉进来执行,外部贡献者就可能把自己的代码放进高权限执行环境。这类组合被称为 Pwn Request。
GitHub 的新默认保护会让 actions/checkout 拒绝一批常见危险 Checkout 模式,但它不是通用沙箱,也不会识别手工 git fetch、gh pr checkout、第三方 Checkout、恶意 Artifact、issue_comment 或所有 workflow_run 组合。正确的迁移不是"关闭报错",而是重建信任边界:低权限工作流处理不可信代码,高权限工作流只处理可信元数据,并通过最小化、可验证的通道交接。
actions/checkout 的新默认行为是防呆,不是完整安全边界。组织仍需扫描手工 Git、第三方 Action、Artifact、Cache 和脚本执行。pull_request 在只读、无 Secret 环境中构建测试;高权限阶段只读取小型、严格验证的元数据,不执行 Fork 产物。allow-unsafe-pr-checkout: true 会恢复原风险,必须经过显式安全评审。GitHub 在 2026 年 6 月 18 日发布公告,并在 7 月 15 日的编辑注中把回移日期调整到 7 月 20 日。公告的核心变化是:
actions/checkout@v7 默认拒绝 pull_request_target 中常见的 Fork PR Head/Merge Checkout 模式。actions/checkout@v4 这类浮动主版本的工作流会在 Tag 更新后解析到新实现。pull_request 和同仓库 PR 的常规路径不在此次行为变化范围内。allow-unsafe-pr-checkout: true 显式退出保护,但这不是推荐迁移方案。该公告证明的是"计划、设计与覆盖范围",不证明某一时刻所有 CDN、Tag、Runner 和缓存都已完成传播。发布文章或执行组织审计时,应同时记录:Action 引用、解析 SHA、Runner 镜像、运行时间、错误文本和仓库策略。
pull_request 与 pull_request_target 的权限差异| 维度 | pull_request(Fork PR) | pull_request_target |
|---|---|---|
| Workflow 来源 | PR 合并上下文/工作流规则 | Base 默认分支上的 Workflow |
| 默认执行信任 | 低信任 | 基础仓库高信任上下文 |
GITHUB_TOKEN | 通常只读 | 可能按 Workflow/仓库设置获得写权限 |
| 仓库 Secret | 通常不向 Fork PR 暴露 | 可用,取决于 Job 与环境配置 |
| 默认分支 Cache | 低权限任务不应假设可信 | 可能读写,形成跨运行持久化通道 |
| 典型用途 | 构建、测试不可信贡献 | Label、评论、指派、元数据自动化 |
| 是否应执行 Fork 代码 | 可以,但必须保持低权限 | 不应直接执行 |
pull_request_target 的合理用途是对 PR 元数据做高权限动作:添加标签、检查作者、写评论、更新 Project、决定是否允许后续任务。它的错误用途是先进入高权限上下文,再把 Fork 的 Head 拉下来构建。
可以把攻击链写成一个乘法模型:
高权限基础仓库上下文× 攻击者可控的 Fork PR 内容× Checkout / Artifact / 配置进入工作区× Build、Test、Install、Script 等执行路径× Token、Secret、Cache、Artifact 或网络出口= 可利用的 Pwn Request
Checkout 本身通常只是把文件写入磁盘。真正执行攻击者代码的动作可能是:
npm install、npm ci 的生命周期脚本;pip install .、setup.py、PEP 517 Build Backend;make test、Gradle/Maven Plugin、Cargo Build Script;RUN;因此,"只 Checkout、不执行"与"Checkout 后运行任意项目命令"必须分开判断。
name: preview-pron:pull_request_target:types: [opened, synchronize, reopened]permissions:contents: writepull-requests: writejobs:preview:runs-on: ubuntu-lateststeps:- uses: actions/checkout@v4with:ref: ${{ github.event.pull_request.head.sha }}- uses: actions/setup-node@v4with:node-version: 22- run: npm ci- run: npm test- run: ./scripts/deploy-preview.shenv:PREVIEW_TOKEN: ${{ secrets.PREVIEW_TOKEN }}
危险点不是 pull_request_target 单独存在,而是:
ref 指向攻击者控制的 Head SHA;npm ci 会执行攻击者可修改的依赖和生命周期脚本;新 actions/checkout 保护可能拒绝此类常见模式。把输入改成手工 git fetch 并不能消除风险,只是绕过一个检测点。
| 引用方式 | 可复现性 | 是否自动得到回移 | 主要风险 | 推荐控制 |
|---|---|---|---|---|
actions/checkout@v4 | 中 | 可能 | Tag 变化会改变行为 | 记录解析 SHA;发布前回归测试 |
@v4.2.2 | 较高 | 否 | 安全修复不会自动进入 | Dependabot/Renovate + SLA |
@<commit-sha> | 最高 | 否 | 长期不更新会滞留漏洞 | SHA Allowlist + 自动升级 PR |
@v1 | 低且过旧 | 不在本次回移 | 缺少新保护和新运行时支持 | 迁移到受支持版本 |
"固定 SHA"解决供应链可复现性,不解决版本陈旧。组织应要求第三方 Action 固定 SHA,同时设置更新机器人、变更审查和最大滞留时间。
rg -n --glob '.github/workflows/*.{yml,yaml}' 'pull_request_target|workflow_run|issue_comment|gh pr checkout|git fetch|actions/checkout|allow-unsafe-pr-checkout|secrets.|permissions:' .
该命令只能做候选召回。YAML 可以使用 Anchor、Reusable Workflow、表达式和多行字符串,最终仍需结构化解析与人工确认。
org:YOUR_ORG path:.github/workflows pull_request_targetorg:YOUR_ORG path:.github/workflows "github.event.pull_request.head.sha"org:YOUR_ORG path:.github/workflows "gh pr checkout"org:YOUR_ORG path:.github/workflows "allow-unsafe-pr-checkout"
需要分别检查主仓库、模板仓库、Archived Repository、Fork、Reusable Workflow 和默认分支之外的活动分支。
建议按以下规则评分:
| 信号 | 分值 |
|---|---|
触发器包含 pull_request_target | +4 |
Checkout 指向 head.sha、head.ref 或 Merge Ref | +5 |
使用 gh pr checkout / 手工拉 Fork | +5 |
| Job 使用仓库或环境 Secret | +4 |
permissions 有写权限 | +3 |
| 运行项目脚本、Build、Test、Install | +4 |
| 读写 Cache | +2 |
| 下载并执行 Artifact | +4 |
使用 allow-unsafe-pr-checkout: true | +6 |
| 只有 Label/Comment,且无 Checkout/执行 | -4 |
评分只是排序,不是漏洞证明。一个 curl 下载并执行外部脚本的工作流,即使未命中规则也可能高危。
本内容包提供 tools/gh_actions_audit.py,用于候选工作流的本地静态扫描。
name: pr-cion:pull_request:types: [opened, synchronize, reopened]permissions:contents: readjobs:test:runs-on: ubuntu-lateststeps:- uses: actions/checkout@v4with:persist-credentials: false- uses: actions/setup-node@v4with:node-version: 22cache: npm- run: npm ci --ignore-scripts- run: npm test
这仍然执行攻击者代码,因此不能提供 Secret、写 Token 或高权限环境。--ignore-scripts 只能减少一类风险,测试本身仍是任意代码执行。
name: pr-metadataon:pull_request_target:types: [opened, edited, labeled, unlabeled]permissions:contents: readpull-requests: writeissues: writejobs:label:runs-on: ubuntu-lateststeps:- name: Apply label from trusted policyenv:GH_TOKEN: ${{ github.token }}PR_NUMBER: ${{ github.event.pull_request.number }}run: |gh pr edit "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --add-label "needs-review"
该 Job 不 Checkout Fork,不读取 PR 中的脚本,不把 PR 标题/正文拼接进 Shell 命令。所有不可信文本通过参数或 API 传递,并避免 eval。
安全优先级从高到低:
{run_id, conclusion, commit_sha},验证来源、Workflow、Repository、Branch、SHA、大小和字段。一个可接受的高权限评论 Job 应把 run_id 当索引,再向 GitHub API 获取结论;不要信任 Artifact 内自报的"成功"。
workflow_run 不是自动安全workflow_run 可以让一个有 Secret/写 Token 的 Workflow 在低权限 Workflow 完成后运行,因此很适合做权限拆分。但它同样可能被误用:
低权限 Fork Workflow↓ 上传攻击者控制 Artifact高权限 workflow_run↓ 下载、解压、执行 ArtifactSecret / Write Token 泄露
安全交接必须满足:
head_repository、head_sha 与目标 PR;| 资源 | 攻击者目标 | 常见失败模式 | 控制 |
|---|---|---|---|
GITHUB_TOKEN | 写代码、Release、Issue、PR | Workflow 默认权限过宽 | 顶层 permissions: {},Job 按需开启 |
| Repository Secret | 外部服务接管 | Secret 直接注入执行不可信代码的 Job | Fork Job 无 Secret;Environment 审批 |
| OIDC | 获取云临时凭据 | id-token: write 与不可信代码同 Job | Subject/Repo/Branch 条件;独立部署 Job |
| Cache | 持久化恶意工具链 | 高权限 Job 恢复低信任 Cache | 信任域分离 Key;高权限 Job 不恢复 Fork Cache |
| Artifact | 跨 Job 注入 | 高权限 Job 执行低权限 Artifact | 固定 Schema、小体积、只解析、来源校验 |
| Self-hosted Runner | 横向移动 | Fork 代码进入长期存活机器 | 不对公共 Fork 开放;一次性隔离 Runner |
| Deployment Token | 外部平台写权限 | 预览部署脚本由 Fork 控制 | 受信部署器 + 声明式输入 + 人工审批 |
直接执行 Fork 代码的预览本质上需要不可信计算。可采用:
Fork PR↓ 低权限构建一次性隔离 Runner / 无 Secret / 受限网络↓ 生成内容摘要与不可执行构建产物受信部署服务↓ 策略校验、病毒扫描、大小限制、内容类型限制临时 Preview Namespace↓ TTL、独立域名、无生产 Cookie、无内网访问Review URL
关键不是"Preview 是否自动",而是构建和部署凭据不共处于同一信任域。对于静态站点,可以让构建 Job 生成纯静态文件,并在部署器侧执行严格文件类型检查;对于服务端代码,应使用隔离 Namespace、短期凭据、网络策略和自动销毁。
在隔离仓库执行,禁止放入真实 Secret。
package.json,加入 preinstall,只创建标记文件。repository: org/security-sandboxrun_started_at: 2026-07-20T00:00:00Zrunner_image: ubuntu-24.04checkout_ref: actions/checkout@v4resolved_sha: "..."event: pull_request_targetfork: trueexpected: blocked-before-checkoutobserved_status: "..."error_excerpt: "..."secrets_present: falsewrite_token_test: denied
最低基线:
permissions: contents: read 或显式空权限;pull_request_target 需要 CODEOWNERS 安全审查;allow-unsafe-pr-checkout,除非有到期豁免;workflow_run 高权限 Job 禁止执行上游 Artifact;示例 Rego 风格策略:
package actions.securitydeny[msg] {input.on.pull_request_targetsome jobstep := input.jobs[job].steps[_]step.uses == "actions/checkout@v4"contains(step.with.ref, "pull_request.head")msg := sprintf("%s checks out fork code in pull_request_target", [job])}deny[msg] {input.on.pull_request_targetsome jobinput.jobs[job].permissions.contents == "write"msg := sprintf("%s has contents:write under pull_request_target", [job])}
真实实现需要先把 YAML 规范化,处理字符串/数组触发器、Anchor、Reusable Workflow 和缺省权限。
pull_request_target、workflow_run、issue_comment 与 Reusable Workflow。pull_request 低权限 Job。pull_request_target 改成 pull_request,但继续注入 SecretFork PR 默认拿不到仓库 Secret,这通常会直接失败。正确做法是重新设计部署与评论流程,而不是想办法重新暴露 Secret。
allow-unsafe-pr-checkout: true这只关闭防护,不改变信任模型。除非工作流后续不执行任何攻击者内容,并经过逐步证明,否则不应使用。
git fetch检测消失,风险仍在。
固定 SHA需要明确的安全更新 SLA。
路径穿越、符号链接、压缩炸弹、Pickle/YAML 不安全加载都可能把数据通道重新变成代码通道。
本文没有证明:
actions/checkout 受支持 Tag 已在所有环境完成传播;实际发布前应按 experiments/github-actions-enforcement-test-plan.md 完成隔离验证。
pull_request_target defaults for GitHub Actions checkout。pull_request_target。actions/checkout Repository。| 症状 | 根因 | 定位 | 修复 |
|---|---|---|---|
7-20 后 actions/checkout 报"Refused to checkout"或类似阻断 | v7 默认拒绝 pull_request_target 中 Fork Head/Merge Checkout | 查看 Job 错误文本 + 解析 actions/checkout 的 Resolved SHA | 拆分为低权限 pull_request + 高权限元数据 Job,而不是关掉检查 |
把 pull_request_target 改成 pull_request 后 Secret 注入失败 | Fork PR 拿不到仓库 Secret 是设计行为 | 看 Job 是否依赖 secrets.* | 重构为 API 查询 Check Run,不要绕回暴露 Secret |
浮动 @v4 解析到 v7 后行为变化 | Tag 升级把新默认行为带进来 | 对比 actions/checkout 不同 Tag 之间的差异 | 记录 Resolved SHA,建立 Release 前回归测试 |
| 固定 SHA 之后没拿到 v7 保护 | 浮动 Tag 才会自动回移,固定 SHA 不会 | 看 Release 是否覆盖到 Patch/Tag | 用 Dependabot/Renovate + SLA + 最大滞留时间,而不是不升级 |
| Workflow 从低权限 Artifact 还原"成功"后被攻击 | 高权限 Job 信任 Artifact 内自报结论 | 看 Artifact 来源、签名、Schema | 高权限 Job 通过 run_id 调 GitHub API 重新查询,不解析 Artifact |
7-20 后工作流一直失败,加 allow-unsafe-pr-checkout: true 后恢复 | 这是新保护在生效,不是配置错误 | 比对加豁免前后的错误 | 走显式安全评审,带 Owner、原因、到期日和补偿控制 |
git fetch 之后 Pwn Request 仍可利用 | actions/checkout 拦截的不是 Git 本身,而是常见模式 | 看 Workflow 实际执行了哪些脚本 | 信任边界要按"是否执行 Fork 产物"判定,而不是按"是否用了 checkout Action" |
npm ci 即使有 --ignore-scripts 仍然有风险 | --ignore-scripts 只能减少生命周期脚本,不能消除任意代码执行 | 看 Job 是否执行 npm test / 项目脚本 | 把构建/测试放到一次性隔离 Runner,关键步骤才回高权限 Job |
| 高权限 Job "只解析"Artifact 仍被攻击 | 不安全解压器或反序列化把数据通道变代码通道 | 看依赖、路径、符号链接处理 | 文件类型 Allowlist、路径穿越校验、签名验证、不用 Pickle/YAML.load |
| OIDC 凭据意外泄露 | id-token: write 与不可信代码同 Job | 看 Job 是否同时有 pull_request 触发器和 id-token: write | OIDC Subject 限制 Repo/Ref/Environment,部署独立 Job |
| Self-hosted Runner 被污染 | 公共 Fork 代码进入长期存活机器 | 看 Runner 是否对 Fork 开放 | Fork 用一次性隔离 Runner,Self-hosted 仅限受信 PR |
| 7-15 编辑注调整到 7-20 后的回移日期 | 文章自带标注,未取得 GitHub 官方 changelog 独立确认 | 看 GitHub 官方 Changelog 公告 | 关注组织实际解析的 SHA、Runner 镜像与错误文本,不依赖具体日历 |
| 评分命中但人工判定"安全" | 评分只是排序,不是漏洞证明 | 看 Workflow 是否实际执行不可信内容 | 用恶意 Fork 回归测试做动态验证,而不是只看规则分 |
| Cache 在高权限 Job 中出现意外二进制 | 低权限 Job 写入了可执行内容到 Cache | 看 Cache Key 与写入者 | 高权限 Job 不恢复 Fork Cache,Key 按信任域分离 |
permissions: write 与 pull_request_target 共存 | Job 拿到高权限 Token 又可被攻击者触发 | 看权限声明位置与默认行为 | 顶层 permissions: {} 空权限,Job 按需开启 |