GitHub Actions `pull_request_target` 与 Pwn Request:高权限工作流里的 Fork 代码执行风险(2026)
TL;DR场景:GitHub 在actions/checkout v7中默认拦截pull_request_target/workflow_run事件里常见的 Fork PR Head/Merge Checkout 模式,但组织里的旧工作流、第三方 Action、Artifact 与手工 Git 操作并不在新保护覆盖范围内。结论:Pwn Request 的本质是高权限事件 × 攻击者可控内容 × 执行路径 × 可滥用资源四个条件同时闭合,新保护只关掉了其中一个常见入口,信任边界必须由组织自己重建。产出:一份覆盖事件边界、权限矩阵、版本治理、组织扫描、双阶段重构、Policy 与发布前检查的实战指南,可直接用于 2026-07-20 前后的工作流审计与迁移。版本矩阵功能状态说明actions/checkout v72024-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 和脚本执行。浮动主版本可能自动获得回移固定 SHA、Minor 或 Patch 不会自动获得。固定 SHA 应与持续升级机制组合而不是被简单视为更安全或更危险。最稳妥的模式是双阶段pull_request在只读、无 Secret 环境中构建测试高权限阶段只读取小型、严格验证的元数据不执行 Fork 产物。7 月 20 日后的工作流失败可能是保护正常生效。绕过检查或设置allow-unsafe-pr-checkout: true会恢复原风险必须经过显式安全评审。1. 事件边界GitHub 改变了什么GitHub 在 2026 年 6 月 18 日发布公告并在 7 月 15 日的编辑注中把回移日期调整到 7 月 20 日。公告的核心变化是actions/checkoutv7默认拒绝pull_request_target中常见的 Fork PR Head/Merge Checkout 模式。GitHub 计划把同类保护回移到除 v1 外的其他受支持主版本。使用actions/checkoutv4这类浮动主版本的工作流会在 Tag 更新后解析到新实现。固定到具体 Commit SHA、Minor 或 Patch 的工作流不会自动获得回移。pull_request和同仓库 PR 的常规路径不在此次行为变化范围内。可以使用allow-unsafe-pr-checkout: true显式退出保护但这不是推荐迁移方案。该公告证明的是计划、设计与覆盖范围不证明某一时刻所有 CDN、Tag、Runner 和缓存都已完成传播。发布文章或执行组织审计时应同时记录Action 引用、解析 SHA、Runner 镜像、运行时间、错误文本和仓库策略。2.pull_request与pull_request_target的权限差异维度pull_requestFork PRpull_request_targetWorkflow 来源PR 合并上下文/工作流规则Base 默认分支上的 Workflow默认执行信任低信任基础仓库高信任上下文GITHUB_TOKEN通常只读可能按 Workflow/仓库设置获得写权限仓库 Secret通常不向 Fork PR 暴露可用取决于 Job 与环境配置默认分支 Cache低权限任务不应假设可信可能读写形成跨运行持久化通道典型用途构建、测试不可信贡献Label、评论、指派、元数据自动化是否应执行 Fork 代码可以但必须保持低权限不应直接执行pull_request_target的合理用途是对 PR 元数据做高权限动作添加标签、检查作者、写评论、更新 Project、决定是否允许后续任务。它的错误用途是先进入高权限上下文再把 Fork 的 Head 拉下来构建。3. Pwn Request 的成立条件可以把攻击链写成一个乘法模型高权限基础仓库上下文 × 攻击者可控的 Fork PR 内容 × Checkout / Artifact / 配置进入工作区 × Build、Test、Install、Script 等执行路径 × Token、Secret、Cache、Artifact 或网络出口 可利用的 Pwn RequestCheckout 本身通常只是把文件写入磁盘。真正执行攻击者代码的动作可能是npm install、npm ci的生命周期脚本pip install .、setup.py、PEP 517 Build Backendmake test、Gradle/Maven Plugin、Cargo Build Script读取 PR 中的 Shell、Python、JavaScript 或配置并执行测试框架自动加载 PluginDocker Build 中的RUN从低权限工作流下载 Artifact 后解压并执行从攻击者可影响的 Cache 恢复可执行内容。因此只 Checkout、不执行与Checkout 后运行任意项目命令必须分开判断。4. 一个典型危险工作流name:preview-pron:pull_request_target:types:[opened,synchronize,reopened]permissions:contents:writepull-requests:writejobs:preview:runs-on:ubuntu-lateststeps:-uses:actions/checkoutv4with:ref:${{github.event.pull_request.head.sha}}-uses:actions/setup-nodev4with:node-version:22-run:npm ci-run:npm test-run:./scripts/deploy-preview.shenv:PREVIEW_TOKEN:${{secrets.PREVIEW_TOKEN}}危险点不是pull_request_target单独存在而是Job 处于 Base 高权限上下文ref指向攻击者控制的 Head SHAnpm ci会执行攻击者可修改的依赖和生命周期脚本Job 拥有写 Token 和部署 Secret后续脚本还有网络出口和外部平台权限。新actions/checkout保护可能拒绝此类常见模式。把输入改成手工git fetch并不能消除风险只是绕过一个检测点。5. 版本固定的真实含义引用方式可复现性是否自动得到回移主要风险推荐控制actions/checkoutv4中可能Tag 变化会改变行为记录解析 SHA发布前回归测试v4.2.2较高否安全修复不会自动进入Dependabot/Renovate SLAcommit-sha最高否长期不更新会滞留漏洞SHA Allowlist 自动升级 PRv1低且过旧不在本次回移缺少新保护和新运行时支持迁移到受支持版本固定 SHA解决供应链可复现性不解决版本陈旧。组织应要求第三方 Action 固定 SHA同时设置更新机器人、变更审查和最大滞留时间。6. 组织级扫描方法6.1 快速文本搜索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、表达式和多行字符串最终仍需结构化解析与人工确认。6.2 GitHub Code Search 查询org:YOUR_ORG path:.github/workflows pull_request_target org: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 和默认分支之外的活动分支。6.3 风险评分建议按以下规则评分信号分值触发器包含pull_request_target4Checkout 指向head.sha、head.ref或 Merge Ref5使用gh pr checkout/ 手工拉 Fork5Job 使用仓库或环境 Secret4permissions有写权限3运行项目脚本、Build、Test、Install4读写 Cache2下载并执行 Artifact4使用allow-unsafe-pr-checkout: true6只有 Label/Comment且无 Checkout/执行-4评分只是排序不是漏洞证明。一个curl下载并执行外部脚本的工作流即使未命中规则也可能高危。本内容包提供tools/gh_actions_audit.py用于候选工作流的本地静态扫描。7. 安全重构把不可信计算与高权限动作拆开7.1 第一阶段低权限构建和测试name:pr-cion:pull_request:types:[opened,synchronize,reopened]permissions:contents:readjobs:test:runs-on:ubuntu-lateststeps:-uses:actions/checkoutv4with:persist-credentials:false-uses:actions/setup-nodev4with:node-version:22cache:npm-run:npm ci--ignore-scripts-run:npm test这仍然执行攻击者代码因此不能提供 Secret、写 Token 或高权限环境。--ignore-scripts只能减少一类风险测试本身仍是任意代码执行。7.2 第二阶段只处理可信元数据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。7.3 需要把测试结果反馈到 PR 时安全优先级从高到低使用 GitHub 原生 Check Run/Status由低权限 Job 直接写其允许写入的状态。让高权限 Job 通过 GitHub API 查询低权限 Workflow 的状态而不是下载可执行 Artifact。只传递固定 Schema 的小型 JSON例如{run_id, conclusion, commit_sha}验证来源、Workflow、Repository、Branch、SHA、大小和字段。不把低权限 Job 生成的脚本、二进制、Node Module、Python Package 或压缩包交给高权限 Job 执行。一个可接受的高权限评论 Job 应把run_id当索引再向 GitHub API 获取结论不要信任 Artifact 内自报的成功。8.workflow_run不是自动安全workflow_run可以让一个有 Secret/写 Token 的 Workflow 在低权限 Workflow 完成后运行因此很适合做权限拆分。但它同样可能被误用低权限 Fork Workflow ↓ 上传攻击者控制 Artifact 高权限 workflow_run ↓ 下载、解压、执行 Artifact Secret / Write Token 泄露安全交接必须满足高权限 Workflow 固定在默认分支校验触发 Workflow 的名称、仓库、事件和结论校验head_repository、head_sha与目标 PRArtifact 设大小上限、文件类型 Allowlist、禁止符号链接和路径穿越只解析数据不执行内容需要签名时使用高权限 Workflow 能独立验证、攻击者无法伪造的签名体系不让低权限 Workflow 决定高权限 Job 的命令、路径、Action 版本或环境名称。9. Secret、Token、Cache 与 Artifact 的风险矩阵资源攻击者目标常见失败模式控制GITHUB_TOKEN写代码、Release、Issue、PRWorkflow 默认权限过宽顶层permissions: {}Job 按需开启Repository Secret外部服务接管Secret 直接注入执行不可信代码的 JobFork Job 无 SecretEnvironment 审批OIDC获取云临时凭据id-token: write与不可信代码同 JobSubject/Repo/Branch 条件独立部署 JobCache持久化恶意工具链高权限 Job 恢复低信任 Cache信任域分离 Key高权限 Job 不恢复 Fork CacheArtifact跨 Job 注入高权限 Job 执行低权限 Artifact固定 Schema、小体积、只解析、来源校验Self-hosted Runner横向移动Fork 代码进入长期存活机器不对公共 Fork 开放一次性隔离 RunnerDeployment Token外部平台写权限预览部署脚本由 Fork 控制受信部署器 声明式输入 人工审批10. Fork PR 预览环境的安全方案直接执行 Fork 代码的预览本质上需要不可信计算。可采用Fork PR ↓ 低权限构建 一次性隔离 Runner / 无 Secret / 受限网络 ↓ 生成内容摘要与不可执行构建产物 受信部署服务 ↓ 策略校验、病毒扫描、大小限制、内容类型限制 临时 Preview Namespace ↓ TTL、独立域名、无生产 Cookie、无内网访问 Review URL关键不是Preview 是否自动而是构建和部署凭据不共处于同一信任域。对于静态站点可以让构建 Job 生成纯静态文件并在部署器侧执行严格文件类型检查对于服务端代码应使用隔离 Namespace、短期凭据、网络策略和自动销毁。11. 恶意 Fork 回归测试在隔离仓库执行禁止放入真实 Secret。测试用例Fork 修改package.json加入preinstall只创建标记文件。Fork 修改测试脚本打印权限和环境变量名称不打印值。Fork 尝试写入仓库分支确认 Token 权限被拒绝。Fork 尝试访问一个专用无敏感性的 Canary Secret确认不存在。Fork 尝试污染 Cache确认高权限 Job 不恢复该 Cache。Fork 上传带路径穿越、符号链接、超大文件的 Artifact确认高权限解析器拒绝。对浮动主版本、固定 Patch、固定 SHA 分别运行记录解析 SHA和错误行为。记录模板repository:org/security-sandboxrun_started_at:2026-07-20T00:00:00Zrunner_image:ubuntu-24.04checkout_ref:actions/checkoutv4resolved_sha:...event:pull_request_targetfork:trueexpected:blocked-before-checkoutobserved_status:...error_excerpt:...secrets_present:falsewrite_token_test:denied12. 组织级 Policy最低基线默认permissions: contents: read或显式空权限公共 Fork 不得进入持久化 Self-hosted Runnerpull_request_target需要 CODEOWNERS 安全审查禁止allow-unsafe-pr-checkout除非有到期豁免第三方 Action 固定 SHA并自动更新低信任与高信任 Cache Namespace 分离workflow_run高权限 Job 禁止执行上游 ArtifactOIDC Subject 限制 Repo、Ref、Environment组织级事件/Actor 执行保护用于限制低信任触发器Workflow 变更必须经过专门 Reviewer。示例 Rego 风格策略package actions.security deny[msg] { input.on.pull_request_target some job step : input.jobs[job].steps[_] step.uses actions/checkoutv4 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_target some job input.jobs[job].permissions.contents write msg : sprintf(%s has contents:write under pull_request_target, [job]) }真实实现需要先把 YAML 规范化处理字符串/数组触发器、Anchor、Reusable Workflow 和缺省权限。13. 发布前检查表枚举pull_request_target、workflow_run、issue_comment与 Reusable Workflow。识别所有 Fork Head/Merge Checkout包括手工 Git 和第三方 Action。识别所有 Build、Test、Install、Docker Build、脚本和可执行 Artifact。识别 Job 的 Token、Secret、OIDC、Environment、Cache、Artifact 和网络权限。把不可信计算迁移到pull_request低权限 Job。高权限 Job 只处理固定 Schema 元数据。将顶层权限收紧并在 Job 层按需开放。检查 Checkout 的版本固定和自动升级策略。在隔离仓库执行恶意 Fork 回归测试。记录 7 月 20 日解析 SHA、错误文本和 Runner 版本。对临时豁免设置 Owner、原因、到期日和补偿控制。14. 常见错误错误一把事件从pull_request_target改成pull_request但继续注入 SecretFork PR 默认拿不到仓库 Secret这通常会直接失败。正确做法是重新设计部署与评论流程而不是想办法重新暴露 Secret。错误二设置allow-unsafe-pr-checkout: true这只关闭防护不改变信任模型。除非工作流后续不执行任何攻击者内容并经过逐步证明否则不应使用。错误三把 Checkout 改成git fetch检测消失风险仍在。错误四认为固定 SHA 可以永久不升级固定 SHA需要明确的安全更新 SLA。错误五高权限 Job 只解析Artifact但使用了不安全解压器或反序列化路径穿越、符号链接、压缩炸弹、Pickle/YAML 不安全加载都可能把数据通道重新变成代码通道。15. 限制与待验证本文没有证明2026-07-20 所有actions/checkout受支持 Tag 已在所有环境完成传播GitHub 的默认检测覆盖所有 Pwn Request任意组织的 Token/Secret 行为都与公开仓库默认值相同一次静态扫描可以替代威胁建模和动态测试。实际发布前应按experiments/github-actions-enforcement-test-plan.md完成隔离验证。16. 延伸章节组织级 GitHub Actions Workflow Scanner 与 Policy。Fork PR Preview 的一次性 Runner 和网络隔离。Actions Cache Poisoning 与 Artifact 供应链。Coding Agent 自动生成 Workflow 的安全门禁。Reusable Workflow 的身份、输入与 Secret 继承。参考来源GH-ACT-01GitHub ChangelogSaferpull_request_targetdefaults for GitHub Actions checkout。GH-ACT-02GitHub DocsSecurely usingpull_request_target。GH-ACT-03GitHub DocsEvents that trigger workflows。GH-ACT-04GitHub DocsWorkflow execution protections。GH-ACT-05actions/checkoutRepository。错误速查卡症状根因定位修复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 重新查询,不解析 Artifact7-20 后工作流一直失败,加allow-unsafe-pr-checkout: true后恢复这是新保护在生效,不是配置错误比对加豁免前后的错误走显式安全评审,带 Owner、原因、到期日和补偿控制git fetch之后 Pwn Request 仍可利用actions/checkout拦截的不是 Git 本身,而是常见模式看 Workflow 实际执行了哪些脚本信任边界要按是否执行 Fork 产物判定,而不是按是否用了 checkout Actionnpm ci即使有--ignore-scripts仍然有风险--ignore-scripts只能减少生命周期脚本,不能消除任意代码执行看 Job 是否执行npm test/ 项目脚本把构建/测试放到一次性隔离 Runner,关键步骤才回高权限 Job高权限 Job 只解析Artifact 仍被攻击不安全解压器或反序列化把数据通道变代码通道看依赖、路径、符号链接处理文件类型 Allowlist、路径穿越校验、签名验证、不用 Pickle/YAML.loadOIDC 凭据意外泄露id-token: write与不可信代码同 Job看 Job 是否同时有pull_request触发器和id-token: writeOIDC Subject 限制 Repo/Ref/Environment,部署独立 JobSelf-hosted Runner 被污染公共 Fork 代码进入长期存活机器看 Runner 是否对 Fork 开放Fork 用一次性隔离 Runner,Self-hosted 仅限受信 PR7-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 按需开启