高价值漏洞优先,还是探索覆盖优先?重新思考 Web/API 安全研究中的调度算法

从公平队列、次模优化到自适应实验:如何在有限请求预算下提高漏洞发现效率,同时避免未知安全边界被长期忽略。

自动化漏洞研究中存在一个容易被低估的问题:当一个目标拥有数百甚至数千个 API,而系统每轮只能深入研究其中少数几个时,究竟应该如何分配有限的研究资源?

最常见的答案是风险排序。根据接口的业务敏感度、认证要求、操作类型、历史异常和潜在影响计算分数,再优先研究排名靠前的接口。

但这种方法存在两个根本性缺陷。

首先,风险评分只能反映当前已知证据下的判断,而漏洞研究面对的恰恰是未知。一个暂时没有明显风险信号的接口,可能隐藏着跨租户访问、权限继承、业务状态绕过等复杂缺陷。

其次,即使风险评分完全正确,也不能保证调度结果正确。 数据读取方式、候选集合截断、重复选择和探索资源分配,都可能让重要接口长期无法进入研究范围。

因此,真正需要解决的并不只是如何计算风险分数,而是一个更完整的资源分配问题:

在有限的时间、请求、计算资源和上下文预算下,如何优先研究具有较高预期安全价值的机会,同时保证尚未探索的安全边界能够持续获得研究机会?

这个问题可以借助公平队列、集合覆盖、次模优化和自适应测试等成熟理论,建立一套相对简单、可解释、低成本的调度方法。


一、风险排序为什么不等于高质量研究调度?

考虑一个实际开发中很容易出现的场景。

某个 Web 应用通过资产发现和 API 提取,共识别出 236 个接口。研究系统每轮最多接收 16 个候选,而数据库查询单次返回 64 条记录。

实现逻辑如下:

读取前 64 个 API
        ↓
计算每个 API 的风险评分
        ↓
按照评分降序排列
        ↓
选择前 16 个 API
        ↓
交给后续研究模块

从局部看,这个实现没有明显错误。参与排序的 64 个接口确实按照风险高低进行了选择。

问题在于,剩余的 172 个接口完全没有参与本轮比较。

假设第 200 个接口涉及组织间的数据导出,第 220 个接口涉及资源所有权转移。即使二者具有比当前 64 个候选更高的研究价值,也不可能进入当前窗口。

这不是评分模型的问题,而是候选集合的形成过程存在偏差。

用数学语言表示,假设全部合格研究机会组成集合 $E$,数据库当前页返回的子集为 $P$。

现有策略实际上执行:

$$ W_{\mathrm{local}}=\operatorname{TopK}_{e\in P}V(e)$$

而真正的全局价值选择应该针对:

$$ W_{\mathrm{global}}=\operatorname{TopK}_{e\in E}V(e)$$

其中 $V(e)$ 表示机会 $e$ 的预估研究价值。

当 $P\subsetneq E$ 时,局部最优通常不能保证全局最优。

更重要的是,就算所有候选都进入了全局排序,也仍然解决不了研究覆盖问题。

假设某个系统每轮都会重新计算全部接口的分数,而部分高分接口长期保持较高排名。

在没有研究状态约束的情况下,它们可能反复进入研究窗口,使其他接口始终无法获得机会。

这涉及计算机科学中一个经典的问题:Starvation(饥饿)。

某些任务并不是被明确拒绝,而是因为调度策略不断优先处理其他任务,导致它们实际上永远得不到服务。

对于漏洞研究,这种情况尤其危险。

因为被长期忽略的对象并不一定没有价值,它可能只是缺少足够的证据来证明自身值得研究。

这意味着,研究调度至少需要同时满足三项要求:

目标 所要解决的问题
价值优先 尽早研究具有较高预期安全影响的机会
覆盖保障 使符合条件但评分较低的机会不会永久饥饿
探索多样性 避免大量相似实验占满有限的研究窗口

三者存在一定的资源竞争关系,不能简单依靠一个风险分数同时解决。

二、将漏洞研究调度建模为受约束优化问题

为了明确优化目标,首先需要区分“API”和“研究机会”。

一个 API 可以对应多个安全研究机会。

例如,GET /orders/{id} 至少可能涉及资源所有权、不同角色访问、组织隔离、资源状态和权限撤销后的行为。

这些研究问题可能需要不同的身份、资源、前置条件和实验方法。

因此,调度的基本单位不应始终是 URL,而应当是可以明确描述安全问题及其测试条件的研究机会。

记:

$$ E_t=\lbrace e_1,e_2,\ldots,e_n\rbrace $$

为第 $t$ 轮所有符合研究资格的机会集合。

系统需要从中选择窗口 $W_t$,使得:

$$ |W_t|\le K$$

其中 $K$ 是单轮研究窗口容量,例如 16。

一种可解释的工程目标函数是:

$$ \max_{W_t\subseteq E_t} \left[ \sum_{e\in W_t}V(e) +\lambda C(W_t) -\mu\sum_{e\in W_t}c(e) \right]$$

这里:

  • $V(e)$:根据真实证据估计的研究价值。
  • $C(W_t)$:本轮能够新增的研究维度覆盖。
  • $c(e)$:预计研究成本,例如请求数、模型开销、执行时间。
  • $\lambda,\mu$:用于平衡覆盖收益和资源消耗的权重。

同时,还要满足整体预算:

$$ \sum_{e\in W_t}c(e)\le B_t$$

其中 $B_t$ 是本轮可用预算。

但这个优化模型仍然存在一个问题:如果某些候选的 $V(e)$ 长期很低,它们仍然可能永远不会被选择。

因此,还需要单独施加探索约束。

例如:

$$ |W_t\cap U_t|\ge \min(q,|U_t|)$$

其中 $U_t$ 表示本轮符合条件、尚未得到研究决策的机会集合,$q$ 是保留的最低探索名额。该约束适用于窗口容量和其他资源允许时;若高价值、新颖性和公平类别存在重叠,应按明确的配额分配规则处理。

这里的关键在于:

公平性应该是调度约束,而不是一个可以轻易被其他分数抵消的加权奖励。

仅仅为低优先级候选增加一点“探索加分”,并不能严格防止长期饥饿。

因为只要其他候选的价值分数持续更高,探索加分仍然可能永远不足以让它们入选。

三、利用公平队列思想,解决长期探索饥饿

1994 年,M. Shreedhar 和 George Varghese 提出了 Deficit Round Robin(DRR),随后在 1996 年发表于 IEEE/ACM Transactions on Networking。

DRR 最初用于网络数据包调度。

它要解决的问题是:在不同数据流竞争有限带宽时,如何避免部分流长期得不到服务,同时保持很低的调度开销。

DRR 的核心思想是为队列提供服务额度,并允许未使用的额度结转。

可以将其简化表示为:

$$ D_i^{(t+1)}=D_i^{(t)}+Q_i-C_i^{(t)}$$

其中:

  • $D_i$ 是队列当前剩余的服务额度。
  • $Q_i$ 是每轮分配的额度。
  • $C_i$ 是本轮实际消耗的资源。

真正的 DRR 会按照任务成本决定是否能够继续服务,并保留相应额度。

它的经典实现具有常数级的数据包处理开销。

在安全研究中,是否需要完整实现 DRR?

不一定。

如果研究窗口的基本资源单位只是一个候选名额,简单的保留名额加 FIFO 轮转,就能够解决很多实际问题。

假设每轮窗口容量为 16,可以采用一种初始分配:

研究类别 名额 选择依据
高价值研究 8 根据安全价值与证据优先选择
未研究机会 4 按持续保存的轮转进度选择
新研究维度 4 根据边际覆盖进行选择

这一比例并非理论最优值,只是一种可实验调整的起点。

某一类别缺少合格候选时,可以将空余名额重新分配给其他类别。

当请求成本差异很大时,再考虑按实际成本累计服务额度,而不是机械地把每个 HTTP 请求看成等价单位。

公平性能够提供什么保证?

假设存在 $N$ 个尚未获得研究决策的合格机会。

每一轮至少从其中选择 $q$ 个此前未展示过的机会,并且已经展示的机会不会再次占用首次展示名额。

如果集合稳定、所有机会持续合格,那么:

$$ T_{\max}\le\left\lceil\frac{N}{q}\right\rceil$$

例如,当 $N=240$、$q=4$ 时,有 $T_{\max}\le60$。

也就是说,在这些条件下,最多 60 轮就可以使所有机会至少各自进入一次研究窗口。

这个性质来自保留名额和不重复轮转,而不是自动继承了 DRR 原论文的全部公平性保证。

同时必须注意:获得研究窗口名额,不等于完成安全验证。

一个接口可能缺少合法身份、有效对象、必要状态或安全执行前提。

系统可以决定获取前提、延期、转向其他研究,或者因授权和风险限制停止测试。

这些都属于有效的研究决策,但不能因此将其计为已经完成安全验证。

四、集合覆盖与次模优化:研究窗口不应被同类接口占满

公平调度只能保证机会不会长期被忽略。

它仍然无法避免一个窗口中充满高度相似的接口。

这就需要引入另一个经典问题:集合覆盖。

1979 年,Václav Chvátal 研究了加权集合覆盖问题的贪心求解方法。

它的基本思想是优先选择那些能够以较低代价覆盖更多尚未覆盖需求的候选。

在安全研究中,可以为每个机会构造一组从实际证据中得到的特征。例如,定义特征集合:

$$ F(e)=\lbrace f_{\mathrm{owner}},f_{\mathrm{tenant}},f_{\mathrm{state}}\rbrace$$

其中三个特征分别表示对象所有权、跨租户隔离和业务状态转换。

假设当前窗口已经选中了一项跨账号订单读取实验。

再选择一项相似的订单详情读取实验,虽然仍可能有价值,但它带来的新研究维度可能较少。

反之,一个涉及权限撤销后资源访问的实验,可能新增此前没有涉及的研究维度。

边际覆盖的计算

对于已经选出的窗口 $W$,新机会 $e$ 的边际覆盖定义为:

$$ \Delta C(e\mid W) = C(W\cup\lbrace e\rbrace )-C(W)$$

如果采用加权特征集合:

$$ C(W)= \sum_{f\in\bigcup_{e\in W}F(e)}w_f$$

那么:

$$ \Delta C(e\mid W)= \sum_{f\in F(e)\setminus F(W)}w_f$$

这里 $w_f$ 表示不同研究维度的权重。

公式反映了一个重要特性:随着某个研究维度不断进入窗口,新的相似候选所能贡献的边际覆盖通常逐渐降低。

这正是次模函数(Submodular Function)的典型性质。

形式化地,对于 $A\subseteq B$ 以及尚未选择的机会 $e$,若函数满足:

$$ C(A\cup\lbrace e\rbrace )-C(A) \ge C(B\cup\lbrace e\rbrace )-C(B)$$

则符合边际收益递减性质。

对于非负加权集合覆盖,这个性质成立。

它能够帮助我们理解,为什么在选出一些相似接口后,继续选择相同类型接口的覆盖收益会下降。

但是要明确区分两个概念:

新增潜在研究维度,不等于新增已经验证的安全覆盖。

在选择窗口时,我们只能根据已观测的接口和业务特征估计它可能覆盖什么。

只有真正执行实验、取得证据并完成解释后,才能将对应研究结论记入实际覆盖记录。

是否应该直接使用完整的次模优化器?

对于典型的 Web/API 研究窗口,没有必要。

假设候选集合有几百条,窗口只包含 16 个机会,每个机会关联的安全特征也很少。

普通贪心选择已经足够实用。

只有在候选规模显著扩大、边际覆盖计算真正成为性能瓶颈时,才值得考虑懒更新堆、位图和更复杂的近似优化算法。

而且,单纯次模覆盖在特定条件下的近似保证,不能直接套用到同时包含风险评分、成本限制和公平配额的组合目标上。

五、Rust 实验:三种调度策略会选出什么?

接下来使用一个简化的 Rust 实验观察差异。

假设我们拥有 10 个 API,每轮最多选择 4 个机会。

每个机会包含三类信息:接口标识、预估研究价值,以及可能涉及的安全研究维度。

为了便于对比,采用如下策略:

策略 A: 只读取第一页,然后选出最高分候选。

策略 B: 读取完整库存,在全部候选中选择最高分机会。

策略 C: 组合选择,每轮保留 2 个高价值名额、1 个边际覆盖名额和 1 个未研究轮转名额。

下面的 Rust 程序可以使用标准库运行,不需要外部依赖。

use std::collections::HashSet;

#[derive(Clone, Copy)]
struct Opportunity {
    path: &'static str,
    score: u32,
    aspects: &'static [&'static str],
}

// 对指定候选范围进行纯风险排序
fn top(
    ops: &[Opportunity],
    from: std::ops::Range<usize>,
    k: usize,
) -> Vec<usize> {
    let mut ids: Vec<_> = from.collect();
    ids.sort_by_key(|&i| std::cmp::Reverse(ops[i].score));
    ids.truncate(k);
    ids
}

// 从尚未展示且未进入本轮窗口的机会中择优
fn best<F: Fn(&Opportunity) -> u32>(
    ops: &[Opportunity],
    shown: &[bool],
    window: &[usize],
    score: F,
) -> Option<usize> {
    (0..ops.len())
        .filter(|&i| !shown[i] && !window.contains(&i))
        .max_by_key(|&i| {
            (score(&ops[i]), std::cmp::Reverse(i))
        })
}

// 将机会加入窗口,并更新本轮潜在覆盖
fn add(
    i: usize,
    ops: &[Opportunity],
    window: &mut Vec<usize>,
    aspects: &mut HashSet<&'static str>,
) {
    window.push(i);
    aspects.extend(ops[i].aspects.iter().copied());
}

fn names(ops: &[Opportunity], ids: &[usize]) -> Vec<&'static str> {
    ids.iter().map(|&i| ops[i].path).collect()
}

fn main() {
    let ops = [
        Opportunity {
            path: "/a/profile", score: 90,
            aspects: &["read", "owner"],
        },
        Opportunity {
            path: "/a/settings", score: 85,
            aspects: &["read", "owner"],
        },
        Opportunity {
            path: "/a/avatar", score: 81,
            aspects: &["read", "owner"],
        },
        Opportunity {
            path: "/b/search", score: 20,
            aspects: &["read", "public"],
        },
        Opportunity {
            path: "/c/invite", score: 75,
            aspects: &["auth", "share"],
        },
        Opportunity {
            path: "/c/revoke", score: 70,
            aspects: &["auth", "revoke"],
        },
        Opportunity {
            path: "/d/export", score: 96,
            aspects: &["read", "cross_org"],
        },
        Opportunity {
            path: "/d/approve", score: 94,
            aspects: &["write", "state"],
        },
        Opportunity {
            path: "/e/orders", score: 58,
            aspects: &["read", "owner"],
        },
        Opportunity {
            path: "/f/webhook", score: 45,
            aspects: &["async", "state"],
        },
    ];

    // A:只在第一页的 4 条记录中排名
    let local = top(&ops, 0..4, 4);

    // B:全局纯价值排序
    let global = top(&ops, 0..ops.len(), 4);

    println!("局部 Top-K: {:?}", names(&ops, &local));
    println!("全局 Top-K: {:?}", names(&ops, &global));

    // C:价值 + 多样性 + 公平轮转
    // shown 只代表已进入研究窗口,并非完成验证。
    let mut shown = vec![false; ops.len()];

    for round in 1..=3 {
        let mut window = Vec::new();
        let mut aspects = HashSet::new();

        // 两个名额:高价值
        for _ in 0..2 {
            if let Some(i) = best(
                &ops, &shown, &window, |op| op.score
            ) {
                add(i, &ops, &mut window, &mut aspects);
            }
        }

        // 一个名额:新增研究维度
        if let Some(i) = best(
            &ops,
            &shown,
            &window,
            |op| {
                let novelty = op.aspects.iter()
                    .filter(|a| !aspects.contains(**a))
                    .count() as u32;

                100 * novelty + op.score
            },
        ) {
            add(i, &ops, &mut window, &mut aspects);
        }

        // 一个名额:按原始顺序公平轮转
        if let Some(i) = (0..ops.len())
            .find(|&i| !shown[i] && !window.contains(&i))
        {
            add(i, &ops, &mut window, &mut aspects);
        }

        // 只标记“已经获得研究窗口机会”
        for &i in &window {
            shown[i] = true;
        }

        println!(
            "组合第 {} 轮: {:?},潜在维度 {}",
            round,
            names(&ops, &window),
            aspects.len()
        );
    }
}

这里使用的 100 * novelty + score 只是为了演示边际覆盖选择。它表示在这个假定评分范围内优先比较新增维度数量,再考虑原始价值,不是适用于实际系统的通用最优评分公式。

为了保持示例简洁,程序使用内存中的固定候选集合,并用数组顺序实现公平轮转。真实系统需要持久化研究状态、处理新候选和授权变化,并在名额不足或类别为空时补齐可用窗口。

实验结果

按照上述程序的确定性选择规则,结果如下:

策略 A:局部 Top-K

/a/profile
/a/settings
/a/avatar
/b/search

这里完全没有出现位于后续位置的 /d/export 和 /d/approve,虽然它们具有最高的两个价值分数。

策略 B:全局 Top-K

/d/export
/d/approve
/a/profile
/a/settings

读取完整候选后,高价值接口终于进入窗口。

但 /a/profile 与 /a/settings 涉及的研究维度完全相同,因此窗口仍然存在冗余。

策略 C:组合调度

第 1 轮:
/d/export
/d/approve
/c/invite
/a/profile

第 2 轮:
/a/settings
/a/avatar
/c/revoke
/b/search

第 3 轮:
/e/orders
/f/webhook

结果显示,组合策略在第一轮保留了两个最高分机会,同时引入资源分享这一新的研究维度,并通过轮转为普通接口提供了机会。

到第三轮结束,10 个候选都至少进入过一次研究窗口。

对第一轮的潜在研究维度进行统计:

策略 第一轮选中数量 不重复的潜在研究维度
局部 Top-K 4 3
全局 Top-K 4 5
组合调度 4 7

这些数字是本例构造的数据结果,不是漏洞检测能力的真实提升比例。

它说明的是一种机制差异:全局排序解决候选截断偏差;边际覆盖减少同质化;公平轮转保证剩余机会能够继续获得研究资格。

需要强调,本实验没有执行 HTTP 请求。因此表中的 7 个维度代表潜在研究覆盖,不是已取得证据的安全研究覆盖。

六、静态调度还不够:实验结果必须改变下一轮选择

到这里,已经可以构造一种兼顾风险和覆盖的窗口选择方法。

但真正的漏洞研究并不是一次性的排序任务。

它是一个不断获取新信息、修正原有判断的过程。

可以把研究过程抽象为:

观察 → 假设 → 实验 → 证据 → 更新判断 → 下一步

这种过程与主动学习和序贯决策有很强的联系。

2011 年,Daniel Golovin 和 Andreas Krause 提出的自适应次模优化理论,专门研究在结果不确定的情况下,如何根据已有观测选择后续行动。

在安全研究中,可以借鉴的是:实验价值不应完全固定,而应随着新证据变化。

一个权限撤销的例子

假设某个应用提供文件分享功能:

用户 A 创建文件,将其分享给 B,然后撤销分享。

现在存在两种竞争假设:

$H_1$:撤销操作会立即使 B 的访问权限失效。

$H_2$:撤销操作只更新部分授权状态,B 仍然可能通过其他访问路径读取文件。

为了区分它们,可以使用明确的测试身份和资源,执行一次具有受控前提的访问实验。

如果实验结果能够排除其中一种解释,就减少了不确定性。

如果结果显示权限在某个路径上仍然有效,则需要进一步判断这是否属于其他合法授权路径、缓存延迟、业务预期,还是实际安全边界失效。

因此,实验的价值不能仅用是否立即发现漏洞衡量。

一次实验也可能通过消除错误假设,显著减少之后的无效测试。

用信息增益表达实验价值

设当前对一组可能解释的概率分布为 $P(H)$,其不确定性可以用信息熵表示:

$$ \mathcal H(H) = -\sum_h P(h)\log_2P(h)$$

执行实验 $e$ 后,可能得到不同观察结果 $o$。

实验的期望信息增益为:

$$ IG(e)= \mathcal H(H) - \mathbb E_{o\mid e} \left[\mathcal H(H\mid o,e)\right]$$

直观地说,它衡量实验预计能够消除多少不确定性。

例如,若两个假设初始概率相同,熵为 1 bit;一个实验能够可靠地区分二者,那么它具有较高的信息价值。

但这并不意味着实际系统必须精确计算所有假设概率。

对于真实 Web/API 目标,我们通常没有足够的数据来校准这些概率。未经校准的模型置信度,也不应该直接冒充客观概率。

实际工程中,可以先用更简单、可追溯的信号表示实验的区分价值,例如:

是否能够区分两个竞争解释;是否验证一个尚未确定的安全约束;是否产生新的资源关系;是否可以避免后续重复实验。

这样既利用了信息论思想,也避免把安全研究系统变成一个缺乏真实概率依据的复杂决策模型。

七、为什么必须抑制重复探索,却不能机械降低高价值研究?

另一个需要解决的问题,是研究资源在不同方向之间的动态再分配。

2016 年的 AFLFast 研究指出,传统覆盖引导模糊测试容易过度投入频繁触达的执行路径。

AFLFast 通过调整测试种子的选择和能量分配,提高对低频路径的探索效率。

这个思想可以迁移到安全研究调度,但必须区别对待。

例如,一个身份验证接口已经连续进行了多次几乎相同的测试,没有发现新的行为,也没有减少关键未知。

那么继续重复相同条件下的请求,通常不值得占用有限预算。

但是,如果研究者已经掌握了一条可能导致严重越权的证据链,只差一次关键验证,那么不能因为此前已经测试多次,就将其降级。

重复次数本身不是惩罚依据,没有新增信息、没有必要前提变化的重复工作才应该受到抑制。

可以用一个简单的边际研究收益指标辅助判断:

$$ R(e)= \frac{ \alpha V(e)+\beta IG(e)+\gamma\Delta C(e) }{ c_{\mathrm{request}}(e) +\eta c_{\mathrm{model}}(e) +\mu c_{\mathrm{time}}(e) }$$

其中:

$V(e)$ 代表预估安全价值,$IG(e)$ 代表预期信息增益,$\Delta C(e)$ 代表新增研究维度,分母则表示不同类型的研究成本。

这个公式是工程启发式,而不是具有通用最优性保证的漏洞发现模型。

实际使用时,应将其限制在具有可靠证据信号的候选集合中,并结合最低探索名额等硬约束。

此外,不能简单地根据 HTTP 方法、URL 中是否包含 admin、响应长度等静态特征推断漏洞严重程度。

风险判断必须与真实业务行为、身份权限和资源状态联系起来。

八、性能问题:覆盖优化不能比实际漏洞研究还昂贵

引入边际覆盖、公平轮转和研究反馈之后,很容易出现另一个工程问题:调度本身的计算成本。

假设存在 $N$ 个候选机会,每轮选择 $K$ 个,每个机会平均拥有 $d$ 个特征。

如果每次选择都遍历所有剩余候选,并重新计算每个候选的边际覆盖,那么朴素实现的时间复杂度约为 $O(KNd)$。

这里假设使用常数级成员判断的数据结构,且特征数量相对较小。

当 $K=16$、$N$ 只有几百或几千时,这通常不是值得优先优化的计算热点。

但如果候选规模达到几十万,或者每轮需要重复读取大量历史实验、解析完整响应、重新调用语言模型,那么调度成本就可能明显上升。

为什么增量统计比历史记录两两比较更适合?

2025 年发表于 FSE 的 Adaptive Random Testing with Q-grams: The Illusion Comes True,提供了一个值得借鉴的方向。

传统自适应随机测试可能需要比较每个新测试与大量历史测试之间的距离。

随着历史测试数增长,这种比较容易产生二次复杂度开销,即 $T(n)=O(n^2)$。

论文提出利用 Q-gram 的历史聚合统计信息,避免反复执行成对距离计算,并在其模型下将相关计算复杂度降低到线性量级。

其在六个 Web 应用基准上的实验报告,平均覆盖独立目标数量约为随机生成的四倍。

这个结果不能直接换算成漏洞发现率,但它说明:测试多样性不一定需要昂贵的全历史相似度比较。

对于安全研究,一个实用的办法是维护增量覆盖索引。

例如:

use std::collections::HashMap;

// 记录每个研究特征实际出现的次数
let mut frequency: HashMap<&str, u32> = HashMap::new();

let observed = ["cross_org", "revoke", "resource_owner"];

for feature in observed {
    *frequency.entry(feature).or_default() += 1;
}

// 查询历史频率,而非遍历所有历史记录
let count = frequency.get("cross_org").copied().unwrap_or(0);

println!("cross_org observations: {}", count);

这里的代码只演示增量频率统计,尚未实现论文中的 Q-gram 构造或熵计算。

还应注意,特征出现次数、被展示次数、执行成功次数和已取得有效研究证据的次数,必须分别记录。

不能因为一个接口被选中过三次,就认为相应安全边界已经得到三次有效验证。

大规模场景下的优化方向

2026 年的 Optimized Greedy Algorithm for Efficient Test Case Prioritization 进一步研究了如何加速覆盖贪心选择。

其采用懒更新、位图和相似测试分组等策略,论文在 54 个 Java 项目的测试优先化实验中报告了显著的运行时间和内存改善。

这些技术可以启发安全研究系统,但不必完整移植。

对于 API 研究调度,合理的渐进优化顺序是:

首先,避免把完整 HTTP 正文和历史证据加载到每轮排序中。

其次,使用数据库索引、轻量特征集合以及持久化轮转进度。

如果实际性能分析证明边际覆盖运算已经成为主要瓶颈,再考虑位图集合、懒更新和其他加速结构。

一个重要前提是:近似相似性可以用来降低候选比较成本,但不能直接用于删除尚未验证的安全机会。

两个看似相似的 API,可能恰好在鉴权逻辑上存在关键差异。

性能优化不能以引入新的漏测为代价。

九、覆盖保障到底应该如何验收?

这里需要澄清一个概念:测试过某个接口,与研究过该接口的安全边界,不是同一回事。

至少应当区分三个层次。

层次 含义 是否证明安全研究完成
机会覆盖 接口或研究问题进入过调度范围 否
实验覆盖 对明确身份、资源和状态执行过实验 不一定
有效研究覆盖 实验产生了能够支持、排除或修正安全判断的证据 是,限于实验覆盖条件

例如,对订单查询接口发送一次正常请求,只能证明该请求在特定条件下被执行。

如果没有跨身份、跨资源或其他安全约束的对照,就不能据此宣称已经完成该接口的越权检测。

同样,一个实验没有触发异常,也不代表它没有研究价值。

它可能成功排除了某种错误假设,并为后续研究节省大量请求。

因此,评价调度质量时,应避免只统计 API 数量、HTTP 请求数量或窗口特征数量。

更有意义的指标包括:

  • 机会覆盖率: $R_{\mathrm{opportunity}}=N_{\mathrm{decided}}/N_{\mathrm{eligible}}$。分子是获得过研究决策的合格机会数,分母是合格机会总数。
  • 有效研究率: $R_{\mathrm{effective}}=N_{\mathrm{evidence}}/N_{\mathrm{executed}}$。分子是产生有效证据的实验数,分母是已执行实验数。
  • 单位预算发现效率: $R_{\mathrm{discovery}}=N_{\mathrm{verified}}/C_{\mathrm{total}}$。分子是确认的有效漏洞数,分母是采用统一口径的总研究成本。

其中研究成本可以同时包括请求预算、运行时间和模型开销,但在比较不同策略时必须使用一致的成本定义。

实际验收还应观察低优先级候选的等待轮数、重复实验比例、高价值漏洞首次发现时间,以及不同安全边界的真实证据覆盖情况。

一个方法即使提高了接口覆盖率,也未必提高漏洞发现效率。

同样,一个方法即使减少了请求数,也可能因为跳过关键权限状态而增加漏检。

这就是为什么必须在固定授权范围、相同初始条件和相近研究预算下进行对照,而不能只看某项算法的单独性能指标。

十、组合方案的关键不是复杂,而是把三种职责分开

经过上述分析,可以得到一个相对明确的设计结论。

高价值选择、公平轮转和探索多样性不应互相替代。

它们分别负责不同的调度性质。

高价值选择解决“优先做什么”。

公平轮转解决“哪些机会不能永远被忽略”。

边际覆盖解决“如何避免一个窗口内的重复研究”。

然后,实验反馈再决定“下一轮是否继续投入”。

将它们组合起来,可以形成如下研究循环:

完整的研究机会库存
        │
        ▼
授权、前提与预算约束
        │
        ▼
轻量候选检索
(全局或有状态的渐进扫描)
        │
        ▼
构造研究窗口
 ├─ 高价值机会
 ├─ 公平轮转机会
 └─ 新研究维度机会
        │
        ▼
研究问题与实验设计
        │
        ▼
真实执行与证据分析
        │
        ▼
确认违规 / 排除假设 / 增加新知识
        │
        ▼
更新覆盖、研究价值与待研究状态
        │
        └──────────→ 下一轮选择

这一方案不要求建立强化学习平台,不要求训练新的漏洞评分模型,也不要求为每种算法增加独立的任务系统。

在多数 Web/API 调查规模下,分页与状态保存、常规集合运算、贪心选择和简单公平队列,已经能够形成有效的组合。

未来是否需要更复杂的调度算法,应由真实瓶颈和实验结果决定。

结论

风险排序是自动化漏洞研究中的必要能力,但它只解决了部分问题。

当研究窗口有限、候选数量较大且系统需要长期运行时,局部排序容易产生候选截断偏差;全局静态排序可能导致低分机会长期饥饿;只关注覆盖数量又可能牺牲高价值漏洞的验证效率。

从几十年的计算机科学研究中,可以提取出一组互补的方法:

通过完整或有状态的渐进候选检索,消除无意的局部截断。

通过公平配额和持续轮转,保证未研究机会能够获得研究决策。

通过集合覆盖与边际收益优化,让有限窗口包含更多不同的安全约束。

通过真实实验反馈,不断修正研究价值,降低无信息收益的重复测试。

其中最重要的,并不是任何一篇论文提供了某种通用最优调度器。

而是明确认识到:

漏洞研究不是已知风险的排序问题,而是在预算约束下,对未知安全边界持续进行证据驱动的探索。

高价值优先决定研究资源的主要投向,覆盖保障防止未知领域永久消失,而实验反馈负责不断更新两者之间的关系。

真正有效的调度策略,应当让重要问题尽早得到验证,让未知问题始终保有被研究的机会,并让每一次实验的实际结果影响下一次选择。

衡量它的最终标准,不是扫描了多少接口,也不是模型给出了多高的评分,而是能否在相同资源预算下,更早、更稳定地发现具有真实安全影响、可以复现和证明的漏洞。


参考文献

  1. Chvátal, V. (1979). A Greedy Heuristic for the Set-Covering Problem. Mathematics of Operations Research. https://doi.org/10.1287/moor.4.3.233
  2. Shreedhar, M., & Varghese, G. (1996). Efficient Fair Queueing Using Deficit Round-Robin. IEEE/ACM Transactions on Networking. https://doi.org/10.7936/K7513WG9
  3. Santos, R. L. T., Peng, J., Macdonald, C., & Ounis, I. (2010). Explicit Search Result Diversification through Sub-queries. ECIR. https://doi.org/10.1007/978-3-642-12275-0_11
  4. Golovin, D., & Krause, A. (2011). Adaptive Submodularity: Theory and Applications in Active Learning and Stochastic Optimization. Journal of Artificial Intelligence Research. https://doi.org/10.1613/JAIR.3278
  5. Böhme, M., Pham, V.-T., & Roychoudhury, A. (2016). Coverage-based Greybox Fuzzing as Markov Chain. ACM CCS. https://doi.org/10.1145/2976749.2978428
  6. Biagiola, M., Feldt, R., & Tonella, P. (2025). Adaptive Random Testing with Q-grams: The Illusion Comes True. Proceedings of the ACM on Software Engineering. https://doi.org/10.1145/3715740
  7. Xu, X., Chen, X., & Guo, Z. (2026). A Unified Framework for Efficient Test Case Generation and Prioritization Driven by Multiple Coverage Criteria. Scientific Reports. https://doi.org/10.1038/s41598-026-56540-3
  8. Alwadaeen, A., & Alzamil, Z. (2026). Optimized Greedy Algorithm for Efficient Test Case Prioritization. Science of Computer Programming. https://doi.org/10.1016/j.scico.2026.103489
  9. Guo, L., Lin, J., & Zhu, S. (2026). Streaming Submodular Maximization with Fairness Constraints for Massive Data Summarization. Theoretical Computer Science. https://doi.org/10.1016/j.tcs.2026.116037