我们的 AI 引擎 Neutron 在加州大学伯克利分校的 CyberGym 基准测试中取得了 96.75% 的成绩。 了解更多

安全

安全

AI 智能体能否在运行时证实 SAST 发现?

静态分析能标记可能存在的缺陷,却无法证明它们。运行时验证证实了 Langflow 中一个需身份验证的 RCE,并纠正了一个 libxml2 释放后使用漏洞所声称的触发条件。

一个自主 AI 智能体拿到了一条针对 Langflow v1.7.3 的 SAST(静态应用安全测试)发现,提交了一个构造函数会休眠 10 秒的自定义组件,结果响应在 40.50 秒后才返回,而不是 10 秒。服务器会针对每一次验证请求把提交的代码运行 4 次。任何静态分析工具都看不到这个 40.50 秒的响应,因为它只在代码运行时才存在。第二个案例是 libxml2 中的一个释放后使用(UAF)漏洞,它展示了运行时测试如何纠正发现本身:报告声称必需的 6 个条件中,有 3 个实际上并不需要。

运行时验证能为 SAST 报告补充什么

什么是运行时验证?
运行时验证是指针对正在运行的应用测试一条静态分析发现,以确认它是否真的会触发、严重程度如何,以及它真正需要哪些条件。它要么把一个可能的缺陷变成一个经过证实的缺陷,要么将其排除。

静态分析工具扫描源代码,并标记看起来存在漏洞的代码行。它们从不运行代码,因此无法告诉您被标记的缺陷是否真实存在、有多严重,或者会影响谁。团队要花费数小时从误报中甄别真实漏洞,而一个充斥着噪声的队列会让开发人员习惯于把每张工单都当作噪声——包括那些最终会出现在事件报告中的少数工单。

我们对两条发现进行了运行时验证:

  • Langflow 需身份验证的远程代码执行(RCE)。 一个简单的计时测试证明,发送到服务器的代码确实会运行,而且每一次验证请求都会运行 4 次。
  • libxml2 释放后使用。 测试表明,原始报告中列出的 6 个条件中有 3 个并不需要。只要解析过程中有一次内存分配失败,该缺陷在解析器默认设置下就会触发。

在这两个案例中,运行中的系统都告诉了我们仅靠代码无法得知的信息。第一个缺陷的运行次数比代码所暗示的更多。第二个缺陷影响的配置比报告声称的更多。这类发现是真实的,但描述有误,因此会以错误的优先级被修复。

以往,用这种方式核查每一条发现大约要花一位专家一个下午的时间。这里的 AI 智能体是一种自主扫描器,它会规划探测、针对在线目标运行这些探测,并在无需逐步人工输入的情况下记录证据。如今,对于每一条附带可运行目标、可重放产物和可观测信号的发现,它都可以自动运行这些测试。


这项研究是如何验证的?

本文中的发现和验证由 Ostorlab 安全研究团队完成。所有运行时验证都是在一个受控的内部 Langflow(v1.7.3)实验实例,以及一个在本地使用 AddressSanitizer 重新构建的 libxml2 上进行的。这些发现基于经验性的计时分析、AddressSanitizer(ASan)内存跟踪,以及系统性的前提条件消融(通过逐一移除来测试每一项所声称的要求)。


静态分析能看到什么,又看不到什么

静态分析可以追踪一个值从 source 到 sink 的流向,并表明源代码中存在一条有风险的路径。但它无法告诉您这条路径在已部署的应用中是否可达、输入能否抵达 sink、结果有多严重,或者哪些前提条件是真实的。

SAST 并不是做不好自己的工作。它做的是另一项工作,而不是我们一直要求它去做的那项。

静态分析器针对的是并未运行的代码。在这个范围内,它快速、全面且成本低廉:在一个人喝完咖啡之前,它就能读完一个大型代码仓库中的每一个文件,还能在一个自 2022 年以来无人改动过的辅助模块里,找出您早已忘记的 exec()。

它做不到的,是回答决定是否有人应该在意的四个问题:

问题 为什么源代码分析无法回答
该路径在已部署的配置中是否可达? 取决于运行时配置、功能开关、反向代理路由、入口过滤器和会话中间件。
输入能否完好无损地抵达 sink? 取决于序列化格式、框架类型强制转换、类型转换、Unicode 规范化和 WAF 检查。
它触发时有多严重? 取决于正在运行的操作系统进程的权限、挂载的密钥和网络隔离,而不是代码仓库的权限。
所声称的前提条件中哪些是真实的? 分析器报告的是它所追踪的那一条抽象路径上的条件,而不是触发该缺陷所需的最小条件集。

最后一行是最容易被低估的,而本文的两个案例都取决于它。一条静态发现描述的是该缺陷可能发生的一种方式。读者乃至工具本身,常常把它误认为是对该缺陷如何发生的唯一描述,进而误判谁会受到影响。

这并不是在抱怨某一类产品,而是这类产品的定义本身。一个针对“死”代码进行推理的工具,无法报告关于活进程的事实,正如一张地图无法告诉您那座桥此刻是否已经封闭。


案例 1:代码对 Langflow 说了什么

Langflow 是一个用于构建 LLM 工作流的可视化工具。用户在画布上组装组件,平台还允许他们用 Python 编写自定义组件。这一功能本身就是产品,这也正是它有意思的地方:危险的行为并非意外,而是产品规格。

针对 v1.7.3 的静态分析得出了一条简短、直白的链路:

  1. 发往 /api/v1/custom_component 的 POST 请求携带用户提供的 Python 源代码。
  2. 代码通过 Python 的动态导入机制(importlib)加载。
  3. 组件类被实例化,以便读取其输入和输出。
  4. 因此,__init__ 会以 Langflow 服务器进程的权限在进程内运行。

没有沙箱,没有允许列表,也没有 AST 检查。其中没有任何技巧,也没有巧妙的 gadget 链:验证就是通过运行这段代码来完成的。

运行 Python 本身就是这项功能,所以真正的问题在于谁可以这样做。在共享的 Langflow 部署中,任何能够登录的账户都会获得服务器进程的全部权限,而不仅仅是访问自己的工作区。这正是这条发现所关注的缺口。

Ostorlab 报告中 Langflow 发现的根本原因与易受攻击的代码流

作为一条静态发现,这已经很有说服力,但它仍然不是证明。上面的一切都是关于源代码的论断。还有四个问题悬而未决,每一个都可能让严重程度朝任一方向翻转:

  • 在已部署的实例上,该端点是否真的接受这种请求,还是会先被路由守卫或反向代理拒绝?
  • 它是否受身份验证保护?发现中说是的,需要 JWT——这决定了它是一个面向互联网的严重漏洞,还是一个登录后的权限问题。
  • __init__ 是否真的会运行,还是 Langflow 在读取该类时并不会将其实例化,而分析器搞错了导入过程?
  • 代码一旦运行,实际上能做什么:休眠、创建进程、读取环境变量?

审查人员可以就这四个问题无休止地争论下去。而运行中的实例大约一分钟就能给出答案。

这条路径是 CVE-2025-3248 的需身份验证版本。后者是 /api/v1/validate/code 中的一个无需身份验证的代码注入漏洞,影响 1.3.0 之前的所有版本,在该漏洞中,对提交代码执行的 exec() 会立即运行装饰器和默认参数。同样的设计决策,不同的入口:/api/v1/custom_component。1.3.0 版本关上了无需身份验证的那扇门,但需身份验证的那扇门在 v1.7.3 上依然敞开。我们于 2026 年 3 月 9 日通过 GitHub Security Advisory(GHSA-8xrc-2jr4-78j7,报告时为非公开状态)向 Langflow 维护者报告了该问题。他们完成了分诊并予以接受,但截至撰写本文时,仍然没有发布补丁版本,也没有 CVE 编号,我们的后续询问也一直未获回复。

如果您运行着共享的 Langflow 实例,在补丁发布之前,请将 /api/v1/custom_component 和 /api/v1/validate/code 视为仅限管理员访问的端点;不要将它们暴露给租户账户。

所有测试均在我们自己的实验实例上进行。


案例 1:运行中的实例说了什么

智能体登录后获取了 JWT,并在一台实验主机上向 /api/v1/custom_component 提交组件。

第一个载荷并不是漏洞利用代码,而是一个阴性对照:

from langflow.custom import Component
from langflow.io import Output

class Recon(Component):
    display_name = "Recon"

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    outputs = [Output(display_name="o", name="o", method="b")]

    def b(self):
        return ""

智能体通过 HTTP POST 提交该组件:

POST /api/v1/custom_component HTTP/1.1
Host: langflow.example.com:7860
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json

{
  "code": "from langflow.custom import Component\nfrom langflow.io import Output\n\nclass Recon(Component):\n    display_name = \"Recon\"\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n    outputs = [Output(display_name=\"o\", name=\"o\", method=\"b\")]\n    def b(self):\n        return \"\"\n"
}

该请求在 0.43 秒内返回了 HTTP/1.1 200 OK 和 {"message": "Component validated successfully"}。现在有了基线,任何偏离它的情况都意味着某种信息。

测试方法是在构造函数中执行休眠。它不需要任何出站网络连接,不会向磁盘写入任何内容,也不会与错误路径混淆:

import time
from langflow.custom import Component
from langflow.io import Output

class TimingOracle(Component):
    display_name = "TimingOracle"

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        time.sleep(10)  # Injected delay oracle

    outputs = [Output(display_name="o", name="o", method="b")]

    def b(self):
        return ""

不同休眠时长下测得的响应时间:

载荷 预期延迟 实测响应 倍数
基线,无休眠 0 s 0.43 s 不适用
time.sleep(3) 3 s 12.53 s ~4×
time.sleep(5) 5 s 20.45 s ~4×
time.sleep(10) 10 s 40.50 s ~4×

Ostorlab 报告中 Langflow 基于时间的验证与计时分析结果

从这张表中可以得出三点结论,而其中只有一点出现在静态发现中。

1. 代码会运行。 一个只读取类 AST 的服务器,无论构造函数中包含什么,都会在 0.43 秒内返回。延迟随载荷变化,说明构造函数确实运行了。这就是该发现,得到了证实。

2. 它会运行 4 次。 在三种不同的休眠时长下,倍数都保持稳定,这排除了巧合和网络抖动的可能。Langflow 在单次验证请求中会将组件实例化 4 次。计时测试证明的是次数,而不是每次运行发生在何处。通过阅读验证代码,最可能的 4 个步骤如下: - 第一次,在初始动态加载期间,用于读取字段属性。 - 第二次,在输入 schema 反射期间。 - 第三次,在输出参数提取期间。 - 第四次,在模板序列化期间。

阅读源代码的人可能会说“它在验证过程中被实例化”——这话没错,却没什么用。每个请求执行 4 次是一种倍增效应:一次 HTTP 请求换来 4 次运行,因此构造函数中开销较大的工作会被放大 4 倍,任何副作用也都会触发 4 次——如果载荷不适合重复执行,这一点就很重要。

3. 两者呈线性关系。 3 → 12.53s,5 → 20.45s,10 → 40.50s。每个读数都等于休眠时长的 4 倍再加上约 0.5 秒(0.45s 至 0.53s),与 0.43s 的基线接近。如果唯一的变化是休眠运行了 4 次,结果正应如此。

随后,这次运行越过了计时测试,去确认影响,而不仅仅是执行。命令执行和读取环境变量的载荷同样返回了 HTTP 200。这些响应表明,标记文件被写入了 /tmp 之下,敏感变量也被读取;这一部分仅从响应码推断得出,并未通过独立的带外通道确认,我们在发现中也保留了这一说明。到了这一步,问题已不再是代码是否会运行,而是这个平台的租户能从服务器进程内部访问到什么——在没有沙箱的情况下,就是该服务账户所拥有的一切。计时测试仍然是整个链条中最有力的单项证据,因为只有它同时具备阴性对照运行和线性响应。


案例 2:运行时如何纠正 libxml2 发现

Langflow 案例展示了运行时如何证实一条发现,并为其补充事实。第二个案例不那么令人舒服,却更有价值,因为在这里,运行时测试推翻了我们自己的报告。

这条发现是 libxml2 ID 校验中的一个堆释放后使用漏洞:两个属性最终指向同一个 xmlID 对象,释放一次之后,第二个属性仍然持有已被释放的内存。根本原因分析精确到了行号,而且是正确的。

该缺陷可追溯到 2022 年 2 月针对 CVE-2022-23308 的修复。那次对 ID 表处理的重构使这条别名路径仍然可达,因此在扫描标记它时,它已经在库中存在了大约四年。随该发现一同提交的,还有一份据称触发该缺陷所需的 6 个条件的列表。

libxml2 释放后使用扫描概览:覆盖率热力图、令牌用量以及高风险发现

报告将该缺陷定位到具体的函数和行号。别名始于 xmlAddIDSafe,在那里两个属性最终共享同一个 xmlID 对象。在文档销毁阶段,xmlFreeIDTable 遍历 ID 表,并通过 xmlFreeID 释放该共享对象;由于别名条目仍然指向它,下一次读取该条目时就会落在已释放的内存中。这就是本节末尾 ASan 跟踪中所示的 xmlFreeID 栈帧。

libxml2 易受攻击的代码:xmlAddIDSafe 别名路径,以及释放共享 xmlID 对象的 xmlFreeIDTable 销毁过程

我们使用 AddressSanitizer 重新构建了该库,并以最直接的方式测试每一个条件:移除一个要素,遍历每一个内存分配失败的位置,观察崩溃是否依然出现:

移除的条件 崩溃是否仍可复现? 结论
XML_PARSE_NOENT(实体替换) 是 非必需
XML_PARSE_DTDVALID(DTD 校验) 是 非必需
元素上的第三个非 ID 属性 是 非必需
重复的 ID 值 否 必需
将属性声明为 ID 的 ATTLIST 否 必需
任何一次内存分配失败 否 必需

6 个条件中有 3 个并不是条件。每一个都有听起来合理的理由,而且每一个都是对分析器所见的那一条执行轨迹的真实陈述,只是在没有任何人测试移除它是否会阻止崩溃的情况下,就被当成了必需条件。

关键在于错误的方向。这三个条件全都夸大了要求。其中两个是非默认的解析器标志(XML_PARSE_NOENT 和 XML_PARSE_DTDVALID),因此按原文所写,这条发现等于告诉读者:必须同时设置这两个标志才会受影响。

事实上,该缺陷在默认选项下、在完全不设置任何标志的情况下就会触发。阅读原始发现的团队,可能会合乎情理却错误地认定它与自己无关。

下面是最小的 143 字节输入。在解析过程中注入一次内存分配失败的情况下,它无需任何非默认标志即可触发崩溃:

<!DOCTYPE root [
  <!ELEMENT root (elem)*>
  <!ATTLIST elem id1 ID #REQUIRED id2 ID #REQUIRED>
]>
<root>
  <elem id1="dup" id2="dup"/>
</root>

除了重复的 ID 和 ID ATTLIST 声明之外,表中保留的另一个不那么显而易见的要求是内存分配失败:崩溃需要解析过程中某处的 malloc 失败,而测试框架(模糊测试框架)中的故障注入器会在其内存分配失败遍历期间强制造成这种失败。这是一个内存条件,而不是解析器标志,因此复现仍然是在默认选项下运行的。这也使得该缺陷在生产环境中很难被意外触发,因为它需要解析过程中有一次内存分配失败——真实工作负载只有在内存压力下才会遇到这种情况,而测试框架是通过故障注入来达成的。这项测试的意义并不在于崩溃很容易触发,而在于报告把该缺陷究竟能影响哪些配置弄错了。

下面是在默认解析器标志下、注入内存分配失败后产生的 AddressSanitizer 崩溃跟踪:

=================================================================
==18492==ERROR: AddressSanitizer: heap-use-after-free on address 0x608000000420 at pc 0x7f81ab281a4b bp 0x7ffd19b3a1a0 sp 0x7ffd19b3a198
READ of size 8 at 0x608000000420 thread T0
    #0 0x7f81ab281a4a in xmlFreeID /libxml2/valid.c:2892:12
    #1 0x7f81ab280ef1 in xmlFreeIDTable /libxml2/valid.c:2914:5
    #2 0x7f81ab251208 in xmlFreeDoc /libxml2/tree.c:1240:5
    #3 0x55dc1820491a in main /libxml2/xmllint.c:3812:9

0x608000000420 is located 32 bytes inside of 48-byte region [0x608000000400,0x608000000430)
freed by thread T0 here:
    #0 0x7f81ab708f30 in free (/usr/lib/x86_64-linux-gnu/libasan.so.6+0xaaf30)
    #1 0x7f81ab281a4a in xmlFreeID /libxml2/valid.c:2892:12
    #2 0x7f81ab280ef1 in xmlFreeIDTable /libxml2/valid.c:2914:5
=================================================================

libxml2 漏洞利用证据:静态确认与动态 AddressSanitizer 崩溃复现

解决这个问题的实验大约只用了二十分钟:一个 143 字节文件的 6 个变体,加上一次遍历。困难的部分——在七千行校验代码中,找到一条隐藏在受保护的销毁路径背后的释放后使用漏洞——早已完成。而成本低廉的那部分,恰恰改变了答案。


运行中的系统知道哪些源代码无法知道的事?

有四项事实(可达性、执行次数、必要性和影响范围)只能通过运行系统来确认。这两个案例看上去毫无相似之处。一个是 Python Web 服务,另一个是 C 语言解析库;一个源于设计决策,另一个是存在了四年的回归缺陷。它们以同样的方式失效,因为两条发现都是关于源代码的论断,而只有运行中的系统才能证实或推翻源代码层面的论断。

1. 可达性。 源代码分析能证明代码中存在一条路径,却无法证明这条路径在部署中是活跃的。Langflow 的端点会响应、接受载荷并运行它:这必须亲眼看到。

2. 执行次数。 危险操作在每个请求中实际发生了多少次?静态报告中没有任何内容会标出这个次数,而人工阅读代码时也很容易忽略。它往往决定着严重程度,因为它会把一个功能性缺陷变成倍增器。

3. 必要性。 静态分析报告的是它所追踪路径上的条件。这些是充分条件,却被当作必要条件呈现。只有通过移除来测试——去掉一个再重试——才能区分两者,而这种区别就是整个暴露面评估的全部所在。在 libxml2 案例中,6 个条件里有 3 个根本不是必需的。

4. 影响范围。 代码在运行时能访问到什么,取决于进程而不是代码仓库:它的用户 ID、环境变量、挂载的密钥以及所处的网络位置。一个作用于容器范围的 exec() 和一个作用于 root 范围的同类调用,会产生相同的源代码层面的发现,却会导致截然不同的安全事件。

请注意,四项中有三项与发现是否属于误报无关。人们普遍把运行时验证定位为减少误报的手段,它确实能做到这一点,但更大的损失在于那些正确却描述不当的发现。它们不会在分级处理中被剔除,而是会以错误的优先级被修复,或者因为一个最终被证明不成立的理由而被搁置;而且与误报不同,在安全事件发生之前,没有人会发现这个错误。


什么才算是漏洞的证明?

“已验证”是安全报告中被大量使用的一个词,而它往往几乎毫无意义。以下是我们对自己的发现所坚持的标准,它也是一个可以套用到任何安全报告上的实用标准:

  1. 可复现的产物。 确切的请求、载荷或输入文件,以他人可以重放的形式提供,而不是对攻击的文字描述。例如那个 143 字节的 XML 文件、带有请求头的 HTTP 请求、自定义组件的源代码。
  2. 一个预言机(oracle)。 某种可观测的信号,能够把“漏洞被触发了”与“发生了某件事”区分开来。比如随载荷呈直线变化的计时延迟、一次 AddressSanitizer 中止、一个标记文件、一次带外 DNS 回调。没有预言机,您得到的只是一个异常现象,而不是结果。
  3. 阴性对照。 使用载荷无害版本的同一请求(在案例 1 中,即不含休眠的 Recon 组件),表明信号随之消失。在那张表中,0.43 秒的基线与 40.50 秒的读数同样重要,因为正是它把一次缓慢的响应变成了证据。跳过对照的发现,往往会在厂商的回复中站不住脚。
  4. 经过刻画、并通过移除测试的前提条件。 对于每一项所声称的要求,都有证据表明移除它会使效果消失。这是几乎所有人都会跳过的一步,而恰恰是这一步决定了谁需要采取行动。
  5. 诚实的边界。 哪些是被证明的,哪些是推断的。我们的 Langflow 发现通过受控的计时测试证明了代码执行;它根据 HTTP 200 响应推断出凭据泄露,并在报告中如实说明,而不是笼统地升级为“已证明完全攻陷”。

满足这五项标准的发现,不是一张需要工程师去调查的工单,而是一张需要他们去修复的工单,而且这张工单自带回归测试:打补丁后重新运行同一请求,预期结果为基线。


自主智能体究竟改变了什么?

这里没有任何新想法。每一个步骤都是资深渗透测试人员拿到一份 SAST 报告和一个预发布环境后会做的事。它之所以没有成为标准做法,原因只是简单的算术:一名测试人员、一个下午、一条发现,而待分级处理的积压工单数量远远超过团队所拥有的下午。

自主智能体改变的是这个循环的成本,而不是它的逻辑:

智能体任务时间线:验证过程中依次运行的 read、grep、bash 和 python 步骤

┌────────────────────────────────────────────────────────────────────────────────────────┐
│                      AUTONOMOUS RUNTIME VALIDATION PIPELINE                            │
├────────────────────────────────────────────────────────────────────────────────────────┤
│  1. Static Pass      ──► 2. Hypothesis      ──► 3. Design Oracle & Negative Control    │
│     (Path to Sink)          (Evidence Criteria)     (Timing / Memory Abort / DNS)      │
│                                                              │                         │
│  ┌───────────────────────────────────────────────────────────┘                         │
│  ▼                                                                                     │
│  4. Live System Probe ──► 5. Observable Fires?                                         │
│                                 │                                                      │
│        ┌────────────────────────┴────────────────────────┐                             │
│        ▼ (No)                                            ▼ (Yes)                       │
│     Refute & Log Negative Result                      6. Ablate Claimed Preconditions  │
│     (Record the result)                                  │                             │
│                                                          ▼                             │
│                                                       7. Actionable Proven Finding     │
│                                                          (PoC + Regression Test)       │
└────────────────────────────────────────────────────────────────────────────────────────┘

从被推翻的猜测返回的那条回路才是关键所在,也是大多数自动化方案所跳过的部分。在我们的 libxml2 运行中,关于触发条件的前三个猜测都是错误的,而每一个都被明确证伪,而不是被悄悄丢弃;第四个猜测才是第一个站得住脚的。一个只报告成功结果的系统并不是在做漏洞研究:它只是在不断采样,直到有东西崩溃为止。

智能体在这里擅长的事情范围狭窄、重复性强,却实实在在:生成载荷变体、把握计时运算、执行那种没人有耐心去做的逐一移除遍历,并把阴性结果记录下来。那次 libxml2 运行从源代码到可用的概念验证(PoC)用了 1 小时 54 分钟,其中 6 个变体的消融遍历大约用了二十分钟。

其局限同样真实,而第二个案例恰恰以我们自己为代价展示了这些局限。

智能体给出了正确的根本原因、可用的概念验证,以及一份信心十足却错误的前提条件列表。那并不是凭空捏造的答案:每一条论断都是对它所见的那一条执行轨迹的真实陈述。失败之处在于,它从单一轨迹出发进行泛化,却没有对这种泛化加以测试——这是一种已知的失效模式,也是可以通过流水线设计来捕获的。通过移除进行测试并不是最后一道可有可无的润色步骤,而是让其余一切变得可信的那一步。


经过运行时验证后,开发人员队列里会收到什么?

实际的转变体现在开发人员队列中收到的内容上:

指标 未经证实的发现(原始 SAST) 经过证实的发现(智能体驱动的运行时验证)
提出的第一个问题 “这是真的吗?” “加沙箱还是加身份验证检查?”
谁来花时间 先是安全团队,然后是开发人员,再回到安全团队 开发人员,一次即可
工单中的证据 一条源代码路径、文件行号和一个通用 CWE 一个可执行的请求、经过验证的响应和对照运行
回归测试 以后再写,或者永远不写 证明产物本身,在修复后重新运行
严重程度评分 基于代码论证的理论严重程度 针对实际部署检验过的实测严重程度

右侧一栏,也正是让一条发现在面对开发团队或开源维护者时站得住脚的原因。

“您的验证端点会运行提交的代码”会引发一场关于设计意图的争论。

“这是一个构造函数会休眠 10 秒的组件,这是您的服务器花了 40.50 秒才作出响应,而这是去掉休眠后同一请求在 0.43 秒内返回的结果”则会终结这场争论。


团队如何让安全发现更值得信任?

无论您的组织使用什么工具,以下三种做法都能立即提升发现的质量:

  1. 要求进行阴性对照运行。 把“完全相同的请求在不带载荷时表现如何?”设为每一条高危发现的必填字段。它只需几秒钟,就能清除由慢速端点、代理超时或环境延迟造成的误报。
  2. 把前提条件列表视为未经测试的论断。 一条声称“仅影响启用了设置 X 的部署”的发现,在有人关闭 X 并重新运行探测之前,只不过是一种猜测。这句话决定着您的团队是否采取行动;它理应接受与漏洞本身同等的测试。
  3. 保留阴性结果。 一个被证伪的猜测并不是一次白费的运行。正是因为它,下一次扫描才不会再次发出同样的告警;它也是一条审计轨迹,表明您的工具是在推理而不是在猜测。

为什么运行时验证会改变分级处理队列

一条静态发现是一个好问题,而不是答案。它告诉您该往哪里看。只有运行中的系统才能告诉您这个缺陷是否真实、触发频率如何、真正需要什么条件,以及影响范围有多广。

这两个案例展示了这一点的两个方面。在 Langflow 中,运行时测试证实了发现,并补充了一个任何人都无法从代码中读出的事实:每个请求执行 4 次。在 libxml2 中,它的作用恰恰相反,纠正了发现,表明所声称的前提条件中有一半并不需要,而且该缺陷能影响的配置比报告所暗示的多得多。

这两项结果都不需要新的想法。每一项需要的都是一次实验:一个清晰的信号、一次对照运行,以及对每一项所声称条件的测试。这些工作一直都是可行的,却也一直太慢,无法以人工方式大规模开展。智能体让它的成本足够低,可以针对每一条具备可运行目标和可观测信号的发现执行——正是这一点,把一个充满“可能”的嘈杂队列,变成一份经过证实、可以立即修复的简短缺陷清单。

Ostorlab 在一次流程中完成全部工作:静态分析找出候选路径,Ostorlab Agentic Deep Scan 将它们直接带到在线实例上,去设计、探测并测试证明。逐一移除的步骤会得出真实的前提条件列表,而不是沿用来自“死”源代码的假设。本文讨论的两条发现都直接来自这条流水线。

运行 Agentic Deep Scan,把您的静态发现转化为经过证实、可以立即修复的结果。


常见问题(FAQ)

SAST 与运行时验证有什么区别?

SAST(静态应用安全测试)读取并未运行的源代码,并报告哪里存在有风险的路径。运行时验证则运行目标,检查该路径是否真的会触发、程度如何以及在什么条件下触发。SAST 找出候选项;运行时验证则把候选项变成证明,或将其排除。

为什么 10 秒的休眠会让 Langflow 请求耗时 40.50 秒?

Langflow 在一次验证请求中大约会将提交的组件创建 4 次,因此构造函数及其中的 time.sleep(10) 会运行 4 次。4 次 10 秒的休眠加上少量开销,大约就是 40.5 秒。在 3 秒和 5 秒时也出现了同样的 4 倍模式,这就是我们确信它是真实现象而非网络噪声的原因。

Langflow 的这个问题与 CVE-2025-3248 相同吗?

不相同,但两者是近亲。CVE-2025-3248 是 /api/v1/validate/code 中一个无需身份验证的代码注入漏洞,已在 1.3.0 中修复。这里的路径是位于 /api/v1/custom_component 上的需身份验证版本。同样的底层设计决策,不同的入口。无需身份验证的那一个已在 1.3.0 中修复;而在我们测试时,需身份验证的那一个在 v1.7.3 上仍然存在。我们于 2026 年 3 月 9 日向 Langflow 维护者报告了该问题(安全公告 GHSA-8xrc-2jr4-78j7);截至撰写本文时,它既没有补丁,也没有 CVE 编号。

什么是计时预言机,为什么可以信任它?

计时预言机是一种载荷,它唯一的作用是让服务器多花一段可测量、可预测的额外时间,在这里就是构造函数中的 sleep。您之所以可以信任它,是因为它配有阴性对照(不带休眠的同一请求在 0.43 秒内返回)和线性响应(3 秒、5 秒和 10 秒都按相同的倍数放大)。两者结合,排除了偶然因素和网络抖动。

对前提条件进行“通过移除来测试”(消融)是什么意思?

它指的是拿掉一项所声称的要求,然后重新运行测试。如果缺陷仍然触发,说明这项要求从来就不是必需的。在 libxml2 案例中,6 个所声称的条件中有 3 个被证明是可选的,而且该缺陷在默认解析器设置下就会触发(仍然需要注入一次内存分配失败),这与原始分析文章所暗示的恰恰相反。

运行时验证会取代 SAST 吗?

不会。它建立在 SAST 之上。SAST 能够快速、全面地在整个代码库中找出候选路径。运行时验证则用来证明这些候选项中哪些是真实的,并对其作出正确的描述。两者您都需要。

这些测试是针对真实的生产系统进行的吗?

不是。两条发现都是在我们自己搭建并掌控的实验环境和测试目标上验证的。本文的重点在于方法,而不在于访问任何他人的系统。


来源与参考资料

来源 参考与背景
Langflow 项目代码仓库 github.com/langflow-ai/langflow:自定义 Python 组件架构与验证端点。
CVE-2025-3248 NVD:CVE-2025-3248:/api/v1/validate/code 中无需身份验证的代码注入漏洞,影响 1.3.0 之前的 Langflow 版本。
libxml2 CVE-2022-23308 修复 GNOME libxml2 提交 652dd12:针对 CVE-2022-23308(ID 和 IDREF 属性的释放后使用漏洞)的上游修复(2022 年 2 月)。它对 ID 表处理的重构使上文所述的别名路径仍然可达。
AI 渗透测试的可信度与证据 Ostorlab:《AI 可以发起攻击,但您能信任结果吗?》:针对自主智能体的最低证据门槛与验证控制。
自主渗透测试的范围漂移 Ostorlab:《事后复盘:自主 AI 智能体为何会越出范围》:遏制架构与运营防护措施。