AI 安全报告通过校验,也可能漏洞百出
作者为一个 47 行的 Node 服务编写四条安全发现,其中两条不成立,还有一条引用不存在的文件,却全部通过 Cloudflare 的发现校验器。文章分析其多 Agent 审计技能,说明报告校验与漏洞真实性验证之间的差距。
AI 深度解读
4 条安全发现,2 条是假的,其中一条引用了根本不存在的文件,却全部通过校验。作者用一个 47 行的 Node 服务说明:AI 安全审计里,报告格式正确,离漏洞真实存在还差很远。
他考察的是 Cloudflare 的 security-audit-skill。结论很直接:评估这类工具,应该重点看它如何推翻自己的发现。对接入 AI 审查的开发者来说,这关系到最终收到的是可复现的问题,还是需要维护者逐条排查的误报。
校验器为什么放过了假漏洞
这套 MIT 许可的 skill 将多个 agent 分成侦察、漏洞搜寻和验证等角色。按作者查看时的记录,项目一天获得近 2,000 stars,总计 35,690。Cloudflare 称它是其全量服务漏洞检测流程的起点。
作者没有安装 skill,也没有运行任何 agent。他手工扮演漏洞搜寻者,再用项目自带的校验器检查结果。因此,这次实验检验的是校验器的边界,以及验证流程是否有用,不能代表整套 agent 的实际表现。
测试目标是用 node:http 和 node:sqlite 写的笔记服务,包含虚拟用户 alice 和 bob。下面两段代码最能解释真假漏洞的差别:
function search(user, q) {
const sql = "SELECT id, title FROM notes WHERE owner = '" + user + "' AND title LIKE '%" + q + "%'";
return db.prepare(sql).all();
}
function getNote(user, id) {
return db.prepare('SELECT id, title, body FROM notes WHERE id = ? AND owner = ?').get(id, user);
}
要看的是 SQL 如何接收输入:search 直接拼接字符串;getNote 通过 ? 占位符绑定参数,查询里同时包含笔记归属条件。
作者提交了四条发现:
search存在 SQL 注入,是真的。getNote存在 SQL 注入,被标为confirmed,还附上了读取 bob 笔记的虚构结果。src/users.js第 212 行的findUser存在 SQL 注入,但文件根本不存在。/admin/export依据X-Forwarded-For是否以10.开头放行,实际风险还取决于代理如何处理请求头。
执行 node validate-findings.cjs proposed.json,结果是 PASS: 4 findings valid。
校验器并非没有约束。整体严重性高于影响等级,或者给 needs_validation 记录填写严重性,都会报错。它还检查结构、排序、安全相对路径,以及 confirmed 是否附带可见的观察结果。
但它不接收目标目录,也不打开源码。虚构的观察结果,只要是合规字符串,就能过关。Phase 4 已明确说明:校验成功只证明格式和台账一致性。
格式校验能证明报告写得合规,不能证明报告说的是真的。
真正排除误报,要重建防线并复现攻击
作者随后写了 15 行 Node 代码,检查引用文件和行号是否存在。这立即排除了引用 src/routes/login.js、src/users.js 的虚构发现。按 Cloudflare 博客的描述,其实际流程也会用确定性代码检查文件和路径。
但这抓不住 getNote 的误报:第 24 行存在,查询也存在,错误藏在「攻击确实成功」这个断言里。
作者于是手工执行 Phase 3 的反证要求:第 2 步重建路径上最强的防护,第 3 步独立复现最小结果。前者会发现参数绑定和 owner 条件,后者用实际请求检验结论。
以下输出展示了同一服务面对普通输入、攻击输入和伪造请求头时的区别:
search, normal 200 [{"id":1,"title":"groceries"}]
search, payload 200 [{"id":1,"title":"groceries"},{"id":2,"title":"payroll"}]
note, bob id 200 null
note, payload 200 null
note, payload 2 200 null
export, XFF 10. 200 [... both users' notes ...]
关键是 note 几行:HTTP 200 不等于越权成功,响应内容仍然是 null。反过来,search 使用 ' OR 1=1 -- 后,确实让 alice 获得了 bob 的笔记标题。
导出接口仅靠请求头就能打开,但生产代理是否覆盖该请求头,仓库没有说明。因此它保留为 needs_validation,并附上由负责人核查的事项。
最终是 1 条确认、1 条待验证、2 条拒绝。再次执行 node validate-findings.cjs findings.json,仍然得到 PASS: 4 findings valid。同样的四条指纹、同样的通过结果,漏洞结论已经不同。
作者是在 loopback 上运行自己的虚拟数据服务。skill 对运行他人目标的要求更严格:必须先有操作系统强制执行的沙箱。
多一层怀疑,值不值得多花钱
这套 skill 的六个阶段中,有两个专门负责反证:Phase 3 将候选交给全新 agent,Phase 5 再让另一个全新 agent 复核最终记录;quick 配置合并这两步。
复杂度并不低。作者统计了 15 个 Markdown 文件,共 1,778 行、24,768 词、约 183 KB;按每 token 四字节估算,约 46,000 tokens,运行时按需分段加载。Cloudflare 博客称初版约 450 行。每个 Phase 3 验证提示在读到候选前,就带有约 17.5 KB 固定文本,包括六步反证要求、文件晋升流程和三个 schema 分支。
两个零依赖脚本 validate-findings.cjs、validate-coverage-ledger.cjs 分别有 773 行和 872 行,作者本机运行的 34 项、31 项测试均通过。
这些投入针对的是误报成本。Cloudflare 博客报告了 20,799 条原始候选,约 12,057 条通过验证;早期验证拒绝率为 40%,改善侦察后降至 11%。按 40% 算,每十条候选就可能有四条需要被挡下,否则排查成本会落到维护者身上。
不过,更复杂未必在所有目标上都更划算。Hacker News 用户 jesse_dot_id 报告,一个较小的 FastAPI 项目至少消耗了 150k tokens,随后触及会话限制。
Issue #20 则对一个人工植入漏洞的目标进行了盲测,每组三轮。quick 成本中位数为 $29.95,普通单 agent 为 $2.06,两者召回率中位数同为 0.467。测试者披露自己拥有竞品,这组结果的样本和利益关系都需要放在判断里。
还有一个限制:按该 issue,skill 的验证 agent 继承会话模型;Cloudflare 自己的流程则使用不同模型进行分诊。全新上下文能减少前文干扰,却不能保证摆脱同一模型的判断偏差。
我的判断
我认同作者把反证放在中心位置。它把「看起来危险」推进到了「防护为何失效、攻击是否成功」,这比多生成几条候选更接近可处理的安全工单。
但这次手工实验没有证明多 agent 优于单 agent,也没有测出这套 skill 的实际误报率。Cloudflare 的规模数据属于其自有流程,不能直接归因于公开 skill;Issue #20 的单目标结果,同样不足以否定复杂验证流程。
与普通单 agent 审查相比,多阶段验证增加了交叉检查机会,也增加了成本。与确定性检查相比,模型能理解更复杂的路径,却更难提供稳定保证。更合理的组合是:文件存在性、格式等交给代码,攻击成立与否交给源码分析和复现,部署前提交给掌握环境的人核实。
审核质量取决于证据能否经受反驳,而不是报告里有多少条 confirmed。
能拿来做什么
- 给 Node 服务接入 AI 安全审查时,先加入文件和行号存在性检查,让低成本规则提前清理虚构引用。
- 要求确认的漏洞提供输入、实际输出和成功判据;审查 SQL 注入时,同时检查参数绑定与权限条件,别把 HTTP 200 当成攻击成功。
- 遇到代理、网关或部署配置决定风险的发现,保留
needs_validation,明确待核实事实和负责人,避免模型替生产环境作答。 - 引入多 agent 验证前,用自己项目的代表性目标比较单 agent、
quick与人工复核,记录有效发现和排查成本,再决定采用哪种流程。