JavaLYG

GitHub Actions 工作流执行保护 GA:pull_request_target 默认拦截与 11 月 2 日前的迁移清单

如果你的公开仓库里有 pull_request_target 触发的工作流,2026 年 11 月 2 日之后它可能直接跑不起来:GitHub 会给公开仓库启用一条默认策略,拦掉这个事件。9 月 17 日"工作流执行保护"正式 GA,配套的默认规则已经在评估模式下运行——你能提前在 Insights 里看到哪些运行会被拦。这篇给一份迁移清单:先自查,再决定是换事件、显式放行还是接受拦截,然后动手配置和加固。

一、这次 GA 发布了什么

"工作流执行保护"(workflow execution protections)是一套 allowlist:actor 规则管"谁能触发",event 规则管"哪些事件能触发",两个条件都在运行前评估。策略可以配在企业、组织、仓库三级,和 ruleset 一样层层叠加。被拦下的运行会失败,日志里能看到类似这样的报错:

Event 'workflow_dispatch' is not allowed to trigger Actions workflows.
Workflow file: '.github/workflows/0-welcome.yml'.

从 public preview 转正式 GA 的同时补了三块能力:把规则限定到具体工作流文件(比如只锁 deploy.yml,而不是整个仓库);Policy insights,审计"拦了谁、会拦谁";以及 REST API,把策略当代码批量下发——对几百个仓库的组织来说,最后一条比点鼠标靠谱得多。

还有个默认值要放在心上:actor 规则如果不配,所有对仓库有写权限的人都能触发工作流。收范围之前,先想清楚"写代码的人"和"跑流水线的人"是不是该是同一批人。

二、为什么默认盯上了 pull_request_target

要理解这条默认规则,先分清两种 PR 触发器的信任差异。

pull_request:从 fork 来的 PR,工作流文件取 PR 的 merge commit,运行环境只给只读的 GITHUB_TOKEN、不给 secrets,还有 fork 审批策略兜底。不受信任的代码在低权限环境里跑,这是安全的默认。

pull_request_target:工作流和"未指定 ref 的 checkout"都取自 base 仓库的默认分支,并且被授予 secrets 和可写 token——这是给"需要权限的 PR 自动化"(打标签、发状态检查)准备的。设计前提是:只跑默认分支上的可信代码。

危险出现在你打破这个前提的时候:把 checkout 指向 fork 的 head 或 merge commit,再去执行它。官方文档把这类脆弱写法总结得很清楚:

  • ref: ${{ github.event.pull_request.head.sha }},或者 ref: refs/pull/${{ github.event.pull_request.number }}/merge
  • repository: ${{ github.event.pull_request.head.repo.full_name }},直接拉 fork 的分支;
  • 绕过 checkout,用 git fetchgh pr checkout,或者把 fork 跑出来的 artifact 下载回来再执行。

注意一个容易误判的点:checkout 本身不执行代码,真正引爆的是后面那条把检出内容当代码跑的步骤——make testnpm install(会触发 postinstall 脚本)、构建命令都算。"pwn request"这个叫法来自 GitHub Security Lab,它不是 pull_request_target 独有:issue_commentworkflow_run 工作流如果把 fork 内容当可信数据执行,同样中招。

三、时间线:四个月里的三层防护

把最近的官方动作连起来看,脉络很清楚:

时间动作
2026-06-18actions/checkout v7 发布:默认拒绝在 pull_request_target(及由 PR 触发的 workflow_run)中拉取 fork PR 代码,命中不安全输入直接报错
2026-07-20该防护 backport 到所有受支持的大版本;v1 除外;把版本锁死在 SHA/小版本的工作流不受影响,需要自行升级
2026-09-17工作流执行保护 GA(原 public preview):新增工作流文件定向、Insights、REST API
2026-11-02公开仓库默认策略从评估转为强制执行,拦截 pull_request_target

覆盖范围要说准:默认规则只加在"没有已配置适用 event policy 的公开仓库"上,私有和内部仓库不受影响,你自己配过的策略也不会被覆盖。当前处于评估模式——运行照旧,但 Insights 里能看出强制执行后会拦谁。

GitHub Actions pull_request_target 默认拦截的四个关键日期与覆盖范围
图一:从 checkout 加固到默认拦截,四个日期里只有 11 月 2 日是"必须做决定"的截止点。

四、自查:找到会被拦的工作流

第一步,在本地把所有用了这个触发器的工作流列出来:

grep -rn "pull_request_target" .github/workflows/

再扫一遍高危写法,命中任何一条,都值得请那个工作流的负责人看两眼:

grep -rnE "refs/pull/|pull_request\.head\.sha|pull_request\.head\.repo" .github/workflows/

线上对照两个入口。一个是 Policy insights 页面(在仓库、组织或企业的 Actions 策略页下方),能看到评估模式下"将会被拦"的运行清单;另一个是用 REST API 直接读策略,只读、可审计:

curl -sS \
  -H "Authorization: Bearer $GITHUB_TOKEN" \
  -H "Accept: application/vnd.github+json" \
  "https://api.github.com/repos/OWNER/REPO/actions/policies"

组织级把路径换成 /orgs/ORG/actions/policies 即可。接口需要对应层级的管理权限;本文命令依据官方文档核对,未在任何真实仓库执行过。

然后对每个命中工作流回答两个问题:它真的需要 secrets 或可写 token 吗?它会执行来自 PR 的代码吗?两个答案都是"是",才轮到考虑保留。

五、三条路线,选一条

官方给的处置路径其实就是三选一:

  • 换事件:不需要 secrets 的,改成 pull_request,保留默认防护,成本最低。
  • 显式放行:确实需要高权限的,创建或更新一条适用的 event policy,在允许事件里显式包含 pull_request_target;新版本支持把范围限定到具体工作流文件,别一放就是一整个仓库。
  • 接受拦截:确认不再需要的,就让默认策略生效,11 月 2 日之后该事件被拦。

三个选项没有中间地带。最忌讳的是"先放着,等流水线红了再说"——那时候你是在压力下做安全决策。

pull_request_target 风险链路与三种处置路线:换事件、显式放行、接受拦截
图二:左边是风险链路,右边是三条路线;放行不是终点,最小范围加加固才是。

六、动手:UI 与 REST API 两条路

策略是分层叠加的:企业级配不可协商的兜底,组织与仓库级往上补细节。官方建议拆成多条边界清晰的策略,而不是每个账号一条大而全的规则——出问题时才能定位到底是哪一层拦的。

UI 路径:仓库或组织里进 Settings → Actions → Policies;企业则从 Policies 标签进 Actions → Policies。新建策略时依次选:名称、enforcement(disabled / active / evaluate)、目标范围(仓库或工作流文件)、actor 规则(谁能触发)与 event 规则(哪些事件)。其中评估模式(evaluate)目前仅 GitHub Enterprise 可选,其余情况建议先拿测试仓库小范围验证。

要批量管理,就走 REST API。创建策略的请求体结构和官方示例一致,下面把规则换成事件限制、并显式允许 pull_request_target;执行前请按自己仓库的真实需要调整字段:

curl -sS -X POST \
  -H "Authorization: Bearer $GITHUB_TOKEN" \
  -H "Accept: application/vnd.github+json" \
  "https://api.github.com/orgs/ORG/actions/policies" \
  -d '{
    "name": "restrict-actions-events",
    "enforcement": "active",
    "rules": [
      {
        "type": "restrict_action_events",
        "parameters": {
          "allowed_events": ["push", "pull_request", "pull_request_target", "workflow_dispatch"]
        }
      }
    ]
  }'

仓库级同形:/repos/{owner}/{repo}/actions/policies;更新用 PUT .../{policy_id},删除用 DELETE。enforcement 选 evaluate 时只评估、不改行为,适合上线前先看一遍会拦到谁;active 才真正生效。

七、必须保留 pull_request_target 的话,四件事做掉

  • 收权限:把 GITHUB_TOKENpermissions 收到最小,只挂真的需要的 secrets,别用"全都要"的默认值。
  • 认清缓存:pull_request_target 工作流对默认分支缓存只有读权限,写缓存会被拒(保存失败只报 warning,步骤继续)——需要写缓存的活儿挪到 push 工作流去做。
  • 隔离 runner:自托管 runner 要临时、隔离,别和内部资源混在一起,也别跨运行复用。
  • 别碰开关allow-unsafe-pr-checkout: true 这个选项取名就是让你在代码评审里一眼看见——只有确认"检出的代码只被当数据处理、绝不执行"时才考虑。

另外两件顺手的事:给 Actions 工作流开 CodeQL,能查出注入一类的通用问题;如果自动化里用到 dependabot[bot] 这类机器人身份,记得把它加进允许的 actor,否则它触发的流程会先被拦。

八、排错表:六个常见现象

现象优先检查结论边界
11/2 后 CI 报 Event 'pull_request_target' is not allowed…该工作流是否真需要此事件;是否需要显式放行报错是策略生效,不代表仓库被限制
Insights 显示将拦截,但运行照常确认处于 evaluate 评估阶段评估期不改行为,别当成失效
私有仓库没看到这条默认规则规则只针对公开仓库不是配置丢了
已放行仍被拦策略层级、条件范围、工作流文件定向是否命中放行范围没覆盖,等于没放行
checkout 步骤直接失败(拒绝拉取)这是 6 月起 actions/checkout 的默认防护,与 11/2 默认策略是两层两层日志要对号入座,别混为一谈
dependabot 触发的自动化被拦allowed actors 是否包含对应机器人身份GitHub 内置流程有豁免,不等于你自定义的也豁免

九、上线前的验收清单

  • 已用 grep 加 Insights 双核对,列出所有使用 pull_request_target 的工作流和负责人。
  • 每个工作流都有明确结论:迁移 / 定向放行 / 接受拦截,没有"再看看"。
  • 放行范围已最小化:限定到具体工作流文件和必要事件,不做全仓库放行。
  • 策略先在评估模式或测试仓库验证过,再切 active
  • 加固四件事(权限、缓存、runner、敏感输入审查)已完成,机器人身份已按需加入。
  • 演练做在 11 月 2 日之前,而不是等 CI 变红来提醒你。

参考来源:GitHub 9 月 17 日 GA 公告Actions policies 官方概念文档控制工作流执行官方操作文档Securely using pull_request_targetactions/checkout 默认防护公告GitHub Security Lab:Preventing pwn requests。资料核对日期:2026 年 9 月 18 日。

11 月 2 日不是一个遥远的日期,离现在只剩六周多。花半小时做上面的自查:要么把这个高权限触发器换成更安全的事件,要么把它锁进最小范围。总比某个早上盯着突然变红的流水线,现场做安全决策强。

🔕 评论已关闭