GitHub 批准了你的 PR,但 pull_request_target 从来没批准过它——这件事今天把 CI/CD 的账全算清楚了

GitHub 批准了你的 PR,但 pull_request_target 从来没批准过它——这件事今天把 CI/CD 的账全算清楚了

你有没有想过这个问题:CI/CD 流水线的”安全审批”,到底在审批什么?

大多数团队的回答是:我们在保护代码质量和发布流程。但很少有人问:我们的 GitHub Actions workflow,在保护什么?

2026 年 7 月 14 日,一个攻击者用 37 个掩护 PR 打开了这个问题的答案。


一行配置,让你的 CI 变成了攻击入口

GitHub Actions 里有一个触发器叫 pull_request_target。它和普通的 pull_request 长得几乎一样,名字也差不多,但行为有本质区别:

  • pull_request:workflow 在 fork 的上下文里运行,只能读代码,碰不到任何 secrets。
  • pull_request_target:workflow 在主仓库的上下文里运行,有完整的 secrets 访问权限,还有写内容的 GITHUB_TOKEN。

这个设计本来是为了做”对外部 PR 做标注和评论”这类事情。但问题来了:很多 workflow 在 pull_request_target 触发的基础上,还 checkout 了 PR 的代码来执行测试、lint,或者做其他自动化处理。

这就成了安全圈里著名的”pwn request“——外部陌生人的代码,跑在拥有你所有 CI secrets 的上下文里。


7 月 14 日,一个真实项目被这样打穿了

AsyncAPI 是一个流行的开源工具链,GitHub 上有数万星。7 月 14 日,它的 generator 仓库被攻击者静默拿下:

攻击者做的事非常清晰:

  1. 37 个掩护 PR:全部是添加”慈善捐款页面”内容,标题统一用”ci: update build configuration”,和你见过的任何 CI 更新 PR 一模一样。
  2. 第 38 个 PR:夹带了一个 markdown 文件,前面垫了约 1000 字节的空白字符,后面藏了混淆过的 JavaScript。
  3. workflow 执行了这段代码,它扫描 runner 环境,偷走了 asyncapi-bot 这个高权限服务账号的 Personal Access Token。
  4. 拿到 token 后,攻击者推送了一个恶意 commit,触发了 release pipeline,发布了 4 个带后门的 @asyncapi 包——合计每天 14 万次下载。

这个攻击不是突然发生的。

4 月 29 日,也就是在此之前 58 天,这个仓库的一个贡献者就提交了一个 PoC(概念验证),证明了这个漏洞的存在。5 月 17 日,他进一步提交了修复方案。

这个修复,在攻击发生的时候,还没被合并。


批准了你的 PR,不等于批准了 CI 的行为

事后复盘里最值得细品的一句话是:“现代 CI/CD 安全实践,特别是 contributor approval 要求,对高知名度仓库是有效的。”

这是 Wiz 研究员的原话。他们说,在这次攻击里,Sentry、OpenSearch、NixOS 这些项目因为配了 PR 审批要求,成功拦掉了攻击。

但这里有一个反常识的边界:

Contributor approval 审批的是”谁能提交代码”,它从来没有审批过”workflow 在有人提交代码后会做什么”。

只要你的 workflow 用了 pull_request_target 且 checkout 了 PR 分支的代码,攻击者就拿到了所有 secrets——不管谁在提交 PR,不管 PR 是不是被审批过的。

这不是代码漏洞,这是 workflow 配置层面的结构性风险。


下一步:怎么知道自己有没有这个问题

打开你的 .github/workflows 目录,搜索这两个组合:

# 如果你的 workflow 同时满足这两个条件,你就是受害者
on: pull_request_target
# 以及
- uses: actions/checkout@v4
  with:
    ref: ${{ github.event.pull_request.head.sha }}

只要 checkout 语句引用了 PR 的 head ref,外部代码就跑在了有 secrets 的上下文里。


怎么修

最干净的方案:用 pull_request 替代 pull_request_target,只在确实需要主仓库 secrets 时才用 pull_request_target,且绝对不 checkout 外部 PR 的代码。

如果业务确实需要 checkout PR 代码

  • 在 environment 上配 Required Reviewers,确保 workflow 触发前有人审批
  • 在 workflow 文件本身配 pull_request_target 的分支限制,只允许受信任分支
  • 把 secrets 放在 environment 里而非直接放在 repository secrets,environment 的 protection rules 更细粒度
  • pull_request_target 的使用当成高危操作,每次改 workflow 文件都走 Code Owner 审批

这件事真正的账

AsyncAPI 的教训不是”我们被攻击了”。教训是:我们明明知道有问题,修补方案就在那,58 天没人合并

CI/CD 基础设施的安全债务,和应用层代码的安全债务,优先级判断逻辑完全不同。代码漏洞有人管,workflow 配置漏洞没人看。

下一个摊到你的项目,可能不是 58 天,可能更短。

今天就去看一眼你的 .github/workflows

评论区

0 条评论

登录后可评论。