工程阅读约 9 分钟

对抗式补丁循环如何判定漏洞“真正被封堵”

多数安全工具止步于“修复建议”。接下来的问题——这个补丁真的挡住攻击了吗——被推回给人。对抗式补丁循环就是为回答这个问题而生的,而答案不是一个通过/失败的标记,而是可复现的证据。

作者 FloatFactory 安全工程团队

FlawDetector 引擎 · 研究

重新扫描不是证明

静态分析的标准流程通常这样收尾:发现缺陷、给出修复建议、开发者合入补丁、下一次扫描确认缺陷消失、关闭工单。问题在于最后一步什么都没有证明。生成缺陷的判断和宣布缺陷消失的判断,来自同一个引擎、同一套规则、同一个视角。等于出题的人给自己的答卷打分。

我们在客户代码库中反复观察到三种失败模式。第一种是绕开特征:补丁只改动了规则匹配的字符串模式,扫描器安静了,攻击路径依旧敞开。第二种是部分缓解:只堵住了最显眼的入口,共享同一弱点的第二个处理函数原封不动。第三种是位置转移:校验逻辑上移到更高层,途中反而开出新的绕过路径。这三种情况下,重新扫描的结果都是干净的。

扫描器安静下来和攻击失败,是两件不同的事。多数流水线测量的是前者,报告的却是后者。

循环的五个阶段

对抗式补丁循环在 CI 内部让两个智能体正面对抗。AI 红队拿到一个已确认的缺陷并真实尝试利用,AI 蓝队则写出能让这次尝试失效的最小补丁。一个回合由以下五个阶段构成,每个阶段都会留下供下一阶段消费的产物。

  1. 01

    攻击 — 红队制定利用方案

    接收缺陷位置、调用图和入口点清单,构造多步攻击序列。产物是可执行的请求序列,以及判定成功的观测条件:响应体、状态码、副作用。

  2. 02

    入侵探测 — 在隔离环境中执行

    把该提交构建到一次性容器中并执行序列。观测条件被满足时,该缺陷就从“理论风险”升级为“已复现的利用”,同时保存完整轨迹。

  3. 03

    生成补丁 — 蓝队做最小改动

    蓝队的输入是利用轨迹,而不是缺陷代码。目标是“让这条轨迹失效的最小改动”,重构和风格调整被明确排除在外。

  4. 04

    再攻击 — 重放原始攻击与变异攻击

    对打过补丁的代码树重放原始序列,随后投放变异套件。只要还有一个能打穿,结果就成为下一回合蓝队的输入。

  5. 05

    封堵验证 — 确认功能没有被破坏

    通过安全关卡的补丁,还要过既有测试套件和正常输入的响应比对。用破坏功能的方式挡住攻击,不算封堵。

对抗式补丁循环的三道封堵关卡

「已封堵」不是宣传用语,而是三个布尔条件同时为真时才会贴上的标签。任何一个为假,状态就不是封堵,判定日志会记录卡在哪道关卡、原因是什么。

封堵判定关卡
关卡通过条件未通过时
原始利用复现第一回合中成功的攻击序列重放三次全部失败补丁没有触及攻击路径。附上轨迹要求蓝队重写
变异套件编码、上下文迁移、链路重排三个族系派生的 12 个变异全部失败只挡住了特征。把打穿的变异放入上下文进入下一回合
回归与行为一致性既有测试全部通过,且正常输入的响应与打补丁前一致安全达标但功能损坏。收窄改动范围重试

三道关卡按顺序评估,前一道未通过时不会执行后面的关卡。

变异利用是怎么生成的

变异套件只有一个任务:分辨蓝队挡住的是字符串还是路径。以成功的利用轨迹为种子,派生三个族系的变体。

  • 编码族 — URL 双重编码、Unicode 归一化差异、空字节插入、大小写混用,即同样的语义换一种字节表达。基于正则的过滤大多在这里崩塌。
  • 上下文迁移族 — 同一载荷改从其他入口投放:另一条路由、批量 API、Webhook 接收端、仅管理员可见的处理函数。这是抓部分缓解的关卡。
  • 链路重排族 — 调换多阶段攻击的顺序,或用其他手段替换中间环节。例如把「认证绕过 → 读文件」拉长为「认证绕过 → 路径穿越 → 读文件」。

默认生成 12 个变异,权重随缺陷类型变化:注入类缺陷中编码变异占比更高,授权类缺陷中上下文迁移变异超过一半。生成过程在固定种子下是确定性的,同一个缺陷永远重建同一套变异——在审计场景中,必须能重新展示“当时到底投了什么”。

没能封堵时 — 缓解与升级

循环不会无限运行。默认上限为 4 个回合,每个回合蓝队都会继承上一次失败的完整上下文。到达上限时,缺陷会落到三种终止状态之一。

  1. sealed(已封堵) — 三道关卡全部通过。补丁 diff 与判定日志一起以 PR 形式打开。
  2. mitigated(已缓解) — 原始攻击失效,但仍有部分变异能打穿。存活变异的复现步骤会原样附上。
  3. escalated(已升级) — 最小补丁无法解决的结构性缺陷,连同“需要设计变更”的明确判断交给人。认证模型本身有问题是典型案例。

2.3

封堵所需平均回合数

严重与高危缺陷,中位数为 2

88.4%

四回合内封堵率

以进入循环的缺陷为基数

8.1%

以缓解收尾

风险下降但变异仍存活

3.5%

升级给人处理

多为设计与认证模型问题

怎么读判定日志

每一次循环都以一个判定对象收尾。仪表盘展示、门禁判断、审计报告生成,用的都是同一个对象。

verdict.jsonjson
{
  "finding": "FD-2026-07-1183",
  "cwe": "CWE-89",
  "severity": "critical",
  "verdict": "sealed",
  "rounds": 2,
  "gates": {
    "original_exploit": { "reproduced": false, "attempts": 3 },
    "mutation_suite": {
      "passed": 12,
      "failed": 0,
      "families": ["encoding", "context-shift", "chain-reorder"],
      "seed": "fd-1183-a"
    },
    "regression": { "tests": 412, "failed": 0, "behavioural_diff": "none" }
  },
  "patch": { "commit": "a1f7c02", "files": 1, "added": 6, "removed": 2 },
  "sealed_at": "2026-07-09T04:12:38Z"
}
一个 SQL 注入缺陷在两回合内被封堵的判定日志

最该看的字段不是 verdict 而是 gatesverdict 只告诉你通过或失败,gates 才显示这个判断的依据是什么。由于 mutation_suite.seed 被记录下来,半年后的审计仍可重建同一套变异并确认同样的结果。

作为门禁接入 CI

循环真正的价值体现在用作合并门禁时。常见配置是按严重级别要求不同的终止状态:严重和高危要求 sealed,中危以下允许 mitigated。

.github/workflows/security-gate.ymlyaml
name: security-gate
on: [pull_request]

jobs:
  flawdetector:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: floatfactory/flawdetector-action@v1
        with:
          api-key: ${{ secrets.FLAWDETECTOR_API_KEY }}
          mode: loop                # 扫描后继续跑红队对蓝队循环
          max-rounds: 4
          seal-gate: critical,high  # 这些级别必须达到 sealed 才允许合并
          allow-mitigated: medium,low
          comment-pr: true          # 把判定日志发到 PR 评论

# 退出码
#   0  门禁通过
#  12  seal-gate 级别中仍有 mitigated 缺陷
#  13  存在 escalated 缺陷 — 需要人工评审

首次开启门禁时,建议从 seal-gate: critical 起步。一次性把高危也纳入,往往意味着历史债务在第一天就卡死所有合并,随之而来的决定通常是关掉门禁。把债务拆成独立待办、门禁只作用于新代码,留存率高得多。

我们测量什么,不相信什么

我们内部跟踪的指标不是“自动修复了多少个”。代码库越乱这个数字越大,因此它和质量的相关性很弱。我们看另外三个。

  • 封堵前置时间 — 从缺陷确认到 sealed 判定所需时间。这是团队唯一真实感知的速度指标。
  • 重开率 — sealed 判定后 90 天内同一缺陷再次复现的比例。判定标准一旦放松,它最先动。
  • 变异打穿率 — 第一回合补丁被变异套件拦下的比例。可作为“蓝队在挡字符串还是挡路径”的代理指标。

不信任的指标同样明确。补丁采纳率在开发者不读 diff 直接批准时也会上升;平均严重度降幅只要改一改标注策略就能改善。对安全工具的任何指标,第一个要问的是:工具自己能不能操纵它。

小结

循环的设计原则可以压缩成一句话:把判定的一方和修复的一方分开,并让判定依据可复现。拆分红队与蓝队、固定变异种子、坚持把 sealed 和 mitigated 作为不同标签,都出自同一条原则。

安全自动化真正难的部分从来不是写出修复代码,而是拒绝给自己的答卷打分,并在修复不够时明说出来。

常见问题

对抗式补丁循环会攻击线上系统吗?
不会。所有利用尝试只在依据被测提交构建的一次性隔离容器中运行,并阻断对外网络。任何请求都不会到达生产或预发环境。
「已封堵」和「已缓解」有什么区别?
已封堵表示三道关卡全部通过:原始利用不再复现、12 个变异全部失效、回归与行为一致性成立。已缓解表示原始攻击被挡住,但至少还有一个变异能打穿——风险下降,但攻击路径没有关闭。
循环产出的补丁可以自动合并吗?
不建议。默认行为是打开带 diff 和判定日志的 PR,由人批准。sealed 补丁已经通过回归与行为一致性验证,评审需要确认的范围小得多,但合并决定仍由人做出。
跑一次循环需要多长时间?
每个缺陷每回合数十秒量级,封堵平均需要 2.3 个回合。整仓扫描的中位数为 41 秒,循环可以配置成只对已确认的严重与高危缺陷选择性运行。

继续阅读

全部文章

在你自己的仓库上看循环跑一遍

接入仓库即可获得完整扫描、可直接合并的补丁,以及每个严重缺陷的判定日志。无需绑定信用卡。