针对代理工作流注入(CVE-2026-44246)的可复现漏洞与修复后的 GitHub Actions 测试夹具,附带实测检测器覆盖率和缓解指南。
针对以 CVE-2026-44246 发布的这一类漏洞(nnU-Net,agentic 工作流注入,CVSS 7.2,CWE-1427),提供可复现的易受攻击/已修复测试夹具,以及三个检测器针对这些夹具的实测输出。
它之所以存在,是因为此前没有任何东西可以用来测试检测器。为这一类漏洞编写规则意味着要手工编写一个夹具,并希望它是忠实的;而已发布的修订版本只有在你知道该看哪个提交时才能获取,而这恰恰成了有趣的部分。
一个 GitHub Actions 工作流将不受信任的文本交给一个持有仓库凭据的 AI agent。
四个条件,缺一不可:
issues、issue_comment、pull_request_review —— 任何拥有 GitHub 账户的人都可以撰写其文本的事件。issues: write、contents: write 等等。claude-code-action 和 codex-action 会拒绝没有写入权限的运行 actor,除非某个输入(allowed_non_write_users、allow-users)显式将其纳入。没有该输入,不受信任的作者永远无法到达 agent,此时该工作流是安全配置,而非本漏洞配置。prompt: 内的 ${{ github.event.issue.body }}),要么在运行时用作业交出的 token 获取(gh issue view)。这不是模板注入。没有任何东西被当作 shell 求值;载荷是散文,而解释器是模型。这就是为什么通常的建议——给变量加引号、不要 eval——并不适用,也是为什么下面的修复关乎 agent 能触及什么,而非转义输入。
nnU-Net 在两个提交中加固了此工作流,而一个将“之前”和“之后”视为二元的检测器会把中间那个搞错。
那个所有人都会称之为修复的提交——其提交信息是 “hardened issue and PR agents”——移除了 gh issue edit 并将标签路由至 .github/scripts/safe-label.sh,但把 Bash(gh issue comment:*) 留给了 agent。agent 仍可被引导去发表评论,而允许列表模式 gh issue comment:* 并未限定于触发该事件的 issue,因此目标由模型自行选择。只有第三个提交关闭了它:agent 现在写入 /tmp/issue-comment.md,随后一个非 agent 的步骤用取自事件的 ISSUE: ${{ github.event.issue.number }} 发布它。
由此得出两点,也正是本仓库不只是两个文件的原因:
“已修复”是关于某个修订版本的主张,而非关于某个版本。 v2.4.1 被引为已修复的发布版本,但其 .github/workflows/ 只包含 codespell.yml——agent 工作流根本不在该标签中。你无法从标签验证修复;你必须固定提交。
权限块从未改变。 issues: write 在所有三个修订版本中都存在且正确,包括最后一个,因为后续步骤需要它。一个仅以 permissions 为关键的检测器无法区分修订版本 1 和修订版本 3。区分它们的是 agent 可以调用什么。
三个检测器,针对两个夹具和全部三个真实修订版本运行。完整原始输出和工具版本见 results.md。
这里没有检测器是简单地“错误”的——它们回答的是不同的问题:
ai-action-prompt-injection 在易受攻击的修订版本上触发,那里 issue 正文被插值进 prompt:,并在插值消失后停止。正确。它在后续修订版本上的发现值得在采取行动前阅读——ai-action-excessive-tools 标记了 Write,而此工作流有意使用它来写入后续步骤所发布的文件;而 ai-action-execution-order 希望 agent 放在最后,而这恰恰是本设计所避免的,因为特权步骤在模型回合结束后才进行。permissions 来区分修订版本 2 和修订版本 3 的工具。它的 ci-agent-missing-author-association 发现在后续修订版本上被报告为 critical,其自身的 README 将其描述为过于严重而非误报:auto-triage 本就旨在让任何人都能触及。这里的每个工具都有一个发现,报告在某个修订版本上,而该修订版本的作者已经就那个确切条件进行过推理。这是启发式方法的常态,也是覆盖率表比通过/失败列更有用的原因。
git clone https://github.com/sushant-me/agentic-workflow-injection
cd agentic-workflow-injection
bash scripts/fetch-revisions.sh # the three real revisions, pinned by SHA
bash scripts/benchmark.sh # runs every detector that is installed
benchmark.sh 将缺失的检测器报告为缺失,而非静默跳过,因为一张悄悄省略某个工具的覆盖率表无法证明关于它的任何事。
夹具是为本仓库编写的精简形态,与其余部分一样采用 MIT 许可。它们是机制忠实的,而非副本:fixtures/vulnerable.yml 承载四个条件,别无其他;fixtures/fixed.yml 是同一文件,带有六个关键变更,每个都有注释。如果你要添加规则,这两个文件是它必须正确处理的最小输入。
两个接口怪癖,已在脚本中处理,但值得了解:
not found 并以 3 退出。无人看管时,这看起来与一次干净的扫描完全相同。path 是 basename,因此对同名文件进行目录扫描会产生歧义。检测是容易的一半。docs/mitigations.md 涵盖另一半:如何约束 agent 的权限,各层按一个问题排序——该控制是否依赖于模型选择遵从?
这个排序就是全部要点。提示中的指令是输入,而非守卫子句;一个指明其目标的工具模式,或一个 agent 从未持有的 token,才是一种约束。该指南从“模型无法做错事”向下讲到“模型被要求不要做错事”,并附有检查清单和 nnU-Net 的演进作为实例。
fixtures/fixed.yml 为其六个变更中的每一个都标注了它所实现的层,以及它是否承重——其中两个不承重,并被如此标注,以免读者高估该文件。
在运行时按提交 SHA 获取,从不内嵌:
此处不再分发 nnU-Net 配置的任何副本;请重新运行获取,并自行将字节与上述 blob 进行比对。
这是一个检测工程产物。它不包含任何漏洞利用,此处也没有任何东西针对实时工作流进行过测试——易受攻击的修订版本是静态文件,已在 nnU-Net 的历史中公开,并由一份已发布的公告引用。夹具被标记为 do not deploy,因为它们的存在是为了被检测,而非被复制。
如果你维护针对这一类漏洞的检测器,并希望在表中占一行,基准脚本就是接口:添加一个部分,在 targets 上运行你的工具并打印其发现,夹具和固定修订版本会完成其余工作。
MIT。
| 修订版本 | 提交 | 日期 | agent 的 --allowedTools | 判定 |
|---|
| 易受攻击 | 94300b49e716 | 2026-04-13 | gh issue comment、gh issue edit | 可触及的写入 |
| “修复” | 4e4770b0b0e6 | 2026-04-24 | 仅 gh issue comment;打标签移至包装脚本 | 仍可触及写入 |
| 后续 | 11bd8746fc06 | 2026-04-27 | 两者皆无;后续步骤从 agent 写入的文件发布 | 不可触及 |
| 修订版本 | agentbound 0.1.3 | sisakulint v0.3.7 | zizmor 1.30.1 |
|---|
1-vulnerable | HIGH write-scope、HIGH untrusted-content | ai-action-prompt-injection | — |
2-fix-commit | HIGH write-scope、CRITICAL author-association | —(仅通用) | — |
3-later | LOW write-scope、CRITICAL author-association | ai-action-excessive-tools、ai-action-execution-order | — |
| 文件 | 仓库 | 提交 | 路径 |
|---|
real/1-vulnerable.yml | MIC-DKFZ/nnUNet | 94300b49e716 | .github/workflows/issue-triage.yml |
real/2-fix-commit.yml | MIC-DKFZ/nnUNet | 4e4770b0b0e6 | .github/workflows/issue-agent.yml |
real/3-later.yml | MIC-DKFZ/nnUNet | 11bd8746fc06 | .github/workflows/issue-agent.yml |