Cloudflare 开源:把安全审计拆成六个阶段
一个自己写的服务跑起来之后,最怕的不是页面丑,是某天发现 /api/articles 不校验身份就能删数据。白物集上线后做过一次安全自查,落点很朴素:把构建产物当外部代码再看一遍,顺手 grep 一遍密钥名、.env 路径、不该出现的内网地址。
这类检查有用、便宜、必须留着。但它有个明确天花板:关键词清单只能查出你事先想到的东西。
Cloudflare 开源的 security-audit 处理的就是这个天花板。仓库 22,000 stars,MIT 许可,JavaScript 写的,源头是 Cloudflare 内部漏洞挖掘管线的第一版:一个约 450 行的 Skill,后来长成了覆盖 128 个仓库的扫描系统。
六个阶段,每一段都在防同一件事
这个 Skill 把一次审计拆成固定六段:
- 侦察 —— 画架构、信任边界、输入面,产出
architecture.md和一份覆盖账本coverage-ledger.json - 按覆盖驱动的狩猎 —— 从账本里取「区域 × 攻击类」格子派给隔离的 hunter,另派 critic 专门找没覆盖到的空格
- 候选验证 —— 每个候选交给一个全新的 verifier,它的任务是证伪
- 结构化输出 —— 三类判定写进
findings.json,再用report-schema.json校验 - 独立复核 —— 新 Agent 复核最终结论引用的源码;结论若被替换,再换一个独立复核者
- 中立报告 —— 从已核实的记录推导出
REPORT.md、FINDINGS-DETAIL.md、NEEDS-VALIDATION.md
流程本身不新鲜,值得看的是它把三个判断写成了硬规则。
核对漏洞的 Agent,永远不是发现它的那个
原话是:The agent that checks a finding is never the agent that found it。同一个 Agent 自查等于自己给自己判卷,同一个模型换个提示词也还是同一套偏见。更狠的是它把「机械校验」和「对抗验证」分开:validate-findings.cjs、validate-coverage-ledger.cjs 这两个零依赖 Node 脚本负责查格式、行号、路径对不对,模型只负责推理。格式错误不需要花 token 去判断。
三档判定,拒绝把猜测包装成漏洞
这是整套设计里最实用的一条:
confirmed—— 有完整源码链路,且有一个边界清楚的复现结果needs_validation—— 必须写出那条没解决的具体事实,而且不允许给严重级rejected—— 候选被证伪,留档
AI 做安全审计最常见的产物,就是一份看着唬人、实际无法复现的问题清单。needs_validation 这一档直接把这类东西按住了:你说不出卡在哪,就不给级别、不写成漏洞。同一套克制还体现在严重级的算法上——严重性 = 可能性 × 影响,不是偏离清单的条数;纵深防御缺口只记 hardening note,不记漏洞。
覆盖账本是可累积的资产
账本按「区域 × 攻击类」记账,跑完一次就标格子。重复运行是叠加的,不是从头再来。Cloudflare 自己给的实测数据很诚实:单次运行只能找到多次运行总量的约一半,而且偏向简单、不隐蔽的那一类。他们由此得出一个判断——如果你的流程已经变成「跑十遍再人工 diff」,说明该上真正的 harness 了。
仓库里更有价值的资产是那十几个攻击类清单文件:客户端注入、协议与认证、供应链与发布、云与部署、资源耗尽、数据隔离与生命周期、桌面与本地 IPC。这些是「格子」的定义本身,也是它比通用代码审查工具更针对性的原因。
白物集这边实际对应的是什么
我们目前的安全动作集中在构建产物这一环。网站构建完之后跑一个检查脚本,扫 .vercel/output/static 和 dist/client 两个目录,一旦命中可疑模式就非零退出,直接掐掉部署——白物集现在跑的是 website/scripts/check-build.js。它挡的是「Astro 模板插值没编译进产物」这类确定性错误(源码里 {redirectToJson} 到了产物里变成 undefined),这类错误人肉 review 很容易漏。
再加上前面说的习惯性 grep,这就是我们全部的家底。它的边界很清楚:正则能挡住的是确定性错误,查不出逻辑漏洞。我们自己的 API 有没有越权,anon key 能不能被拿去读别人的数据,Express 路由有没有把请求参数直接拼进查询,支付相关的回调有没有校验签名——这些问题,关键词清单一个都查不出来,因为它们的形态不是某个字符串。
security-audit 补的正是这一段:把「想到什么查什么」换成「格子填满为止」。它有 15 个攻击类清单文件,等于把「一个懂安全的人会从哪些角度下手」写成了可执行的枚举。
还有一条要提前说清楚:我们没有跑过它的完整流程。但它的默认行为值得单独讲——加载这个 Skill 不等于授权跑全套审计。默认是 guidance 模式,问安全问题、只看单个文件时,它只取相关章节,不会建目录、不会写审计产物;只有明确说「审计整个代码库」「渗透测试」「要报告文件」,才进完整模式。在动不动就自动写一堆文件的 Agent 生态里,这个保守默认很少见,也是它敢直接用在生产仓库的前提。
适合什么,不适合什么
适合:有真实用户和真实数据的自建服务——API、数据库、前端构建产物都在自己手里的那种;上线前的自查;接手一个陌生代码库时的摸底。
不适合:纯静态页面、没有信任边界的玩具项目;以及没有沙箱就别硬跑全套。它的要求里有硬条件:目标代码只能在 OS 级沙箱里执行——禁外网、环境变量白名单、CPU/内存/进程/磁盘限额、只能写分配的 scratch 目录。少一条控制,流程就不执行目标代码,把结论保留成 needs_validation。个人机器上如果没有容器隔离,实际只能跑到「读源码 + 推理」这一层,价值打折,但不为零。
快速上手
# 1. 装 Skill(加 --global 装到用户级)
npx skills add https://github.com/cloudflare/security-audit-skill \
--skill security-audit
# 2. 在待审代码库目录里启动你的 coding agent
# 3. 直接说触发语
# security audit this codebase
# 或者只想问问题:「这个接口的身份校验有问题吗」→ 走 guidance 模式
# 4. 验证输出
node validate-coverage-ledger.cjs
node validate-findings.cjs
输出默认落在 ~/security-audit-skill/<repo-name>/run-<N>,<N> 取下一个没被占用的整数——同一个仓库重复跑不会互相覆盖,因为多次运行本来就是叠加用的。
跑完留下的三份文件里,NEEDS-VALIDATION.md 比 REPORT.md 更值得读:报告写的是已经确认的事实,而那份文件写的是「我们还不知道的那部分」——对个人开发者来说,后者才决定下一次该往哪查。