用大模型做代码审计,最直觉的方式是:
"帮我审计这个项目的安全漏洞"
这在小项目上偶尔有效,但在真实的工程项目中几乎不可用。原因很简单: 代码审计不是搜索,而是决策。 一个 20 万行的 Java 项目,有上百个 Controller、几十个数据源、复杂的认证链路。模型不知道该先看什么,不知道该看多深,不知道什么时候该停。更致命的是——它会「幻觉」:报告一个根本不存在的文件中的漏洞,或者编造一段从未出现过的代码。
在写第一行 Skill 指令之前,我花了很多时间思考一个问题:
AI 做代码审计,最容易犯什么错? 答案是两个字:幻觉 。
这是整个 Skill 的第一原则。具体体现为三条硬性规则:
禁止猜测文件路径——必须用 Glob/Read 验证文件存在
禁止编造代码片段——必须引用 Read 工具实际输出的代码
禁止报告未读文件中的漏洞——没有读过的代码不能出现在报告中
另一个容易出现的问题是确认偏见。模型读了几个文件后,会倾向于「证实」自己已有的判断,而不是系统性地检查所有可能。
禁止 "基于之前的审计经验,我将重点关注..."
禁止因为某个漏洞类型"看起来不太可能"就跳过检查
必须枚举所有敏感操作,然后逐一验证
必须完成完整的检查清单,对每个维度一视同仁
单个 Medium 级别的漏洞可能无关紧要,但当它与另一个 Medium 组合时,可能变成 Critical。
认证绕过(H) + 需认证的 SSRF(M) = 未认证 SSRF(C)
信息泄露(L) + 密码重置逻辑缺陷(M) = 账户接管(C)
整个审计流程分为五个阶段:
Phase 1: Reconnaissance(侦察)
↓
Phase 2A: Vulnerability Hunt(自由审计)
↓
Phase 2B: Coverage Verification(覆盖率自检)
↓
Phase 3: Deep Dive(深度验证)
↓
Phase 4: Report(报告生成)
不急着找漏洞,先画地图。 这个阶段做的事情非常明确:
这是核心阶段。多个 Agent 并行执行,每个 Agent 负责 1-3 个安全维度。 Skill 不预设固定的 Agent 模板,而是根据 Phase 1 的攻击面分析动态决定:
该项目的 R1 Agent 划分(基于攻击面分析动态生成):
Agent 1: SQL/SpEL 注入 (D1) — 追踪用户输入到 SQL Sink
Agent 2: 认证+授权+业务逻辑 (D2+D3+D9) — JWT/Filter/权限/IDOR
Agent 3: 文件操作+SSRF (D5+D6) — 上传下载/路径遍历/JDBC URL
Agent 4: 反序列化+RCE (D4) — JAR 上传/类加载/反射调用
Agent 5: 配置+加密+依赖 (D7+D8+D10) — 硬编码密钥/暴露端点/CVE
这是区别于其他 AI 审计工具的关键机制。 Phase 2A 完成后,不是直接出报告,而是用一个 10 维度覆盖率矩阵 做自检:
---|---|---
D1 | 注入 | 用户输入是否能到达 SQL/Cmd/LDAP/SSTI 执行点?
D2 | 认证 | Token 生成、验证、过期是否完整?
D3 | 授权 | 每个敏感操作是否验证用户归属?
D4 | 反序列化 | 是否存在不受信数据的反序列化?
D5 | 文件操作 | 上传/下载路径是否可控?
D6 | SSRF | 服务端 HTTP 请求的 URL 是否用户可控?
D7 | 加密 | 硬编码密钥?弱算法?
D8 | 配置 | 调试接口暴露?CORS 过宽?
D9 | 业务逻辑 | 竞态条件?流程可跳过?
D10 | 供应链 | 依赖是否有已知 CVE?
这是 Skill 中我花了最多时间优化的部分。
早期的多轮审计策略很粗暴——R2 重新来一遍,只是换个方向。结果:
R1 完成后,主线程会产出一个结构化的「跨轮传递结构」:
COVERED: D1( 10个发现), D2( 6个), D3( 14个), D5( 4个), ...
GAPS: D4( 未覆盖), D9( 未覆盖), D1(️ SQL查询构建层未深入)
CLEAN: [JNDI/XXE/Fastjson — 已搜索确认不存在的攻击面]
HOTSPOTS: [QueryBuilder.java:135 — 字符替换未追踪, PermissionManager.java — 鉴权逻辑待验证]
FILES_READ: [TokenUtils.java:JWT decode无verify, CryptoUtils.java:硬编码AES密钥, ...]
GREP_DONE: [JWT.decode, @Permission, CREATE ALIAS, loadRemoteFile, ...]
R2 不再固定数量,而是由缺口数精确计算:
| 缺口状态 | R2 Agent 数量 |
|---|---|
| 未覆盖 0-1 个 | 1 Agent (20 turns) |
| 未覆盖 2-3 个 | 2 Agent (2×20 turns) |
| 未覆盖 4+ 个 | 3 Agent (3×20 turns) |
多智能体最大的挑战不是「如何启动」,而是「如何约束」。 Skill 定义了一套 Agent 合约(Agent Contract) ,每个 Agent 启动前自动注入:
---Agent Contract---
1. 搜索路径: {paths}。排除: node_modules, .git, build, test, frontend
2. 必须使用 Grep/Glob/Read 工具。禁止 Bash 中 grep/find/cat。
3. 工具调用 ≤50 次,Bash ≤10 次。max_turns: {N}。
4. Turn 预留: turns_used ≥ max_turns-3 时立即停止探索,产出结构化输出。
5. 搜索策略: Grep 定位行号 → Read offset/limit 读上下文(±20行)。
6. 输出: 按结构化模板返回。禁止返回大段原始代码(>3行)。
7. 同类漏洞 ≥5 个合并报告。同 pattern 多文件列清单不逐个深挖。
8. Sink 类别上界: 每维度最多 8 个 Sink 类别,每类最多深追 3 个实例。
9. 数据转换管道追踪: Source → [Transform₁ → ... → Transformₙ] → Sink。
10. 截断防御: HEADER 在最前(≤400字),AGENT_OUTPUT_END 在最后。
---End Contract---
很多安全工具的做法是:给一个检查清单,逐项执行。 我的设计理念正好相反:Checklist 不驱动审计,而是验证覆盖。
LLM 先自由审计(Phase 2A)→ 再用矩阵查漏(Phase 2B)
| 指标 | 数据 |
|---|---|
| 项目 | 某开源 BI 数据可视化平台 |
| 代码量 | ~200K 行 Java |
| 技术栈 | Spring Boot 3.x + Java 21 + MyBatis-Plus + 嵌入式数据库 |
| 模块结构 | core-backend + sdk-common + extensions (多数据源插件) |
| 审计模式 | Standard(2 轮) |
Round 1(侦察 + 广度扫描)
| Agent | 方向 | max_turns | 发现数 |
|---|---|---|---|
| Agent 1 | SQL/SpEL 注入 (D1) | 25 | 8 |
| Agent 2 | 认证+授权 (D2+D3) | 25 | 16 |
| Agent 3 | 文件+SSRF (D5+D6) | 25 | 6 |
| Agent 4 | 反序列化+RCE (D4+D5) | 25 | 10 |
| Agent 5 | 配置+加密+依赖 (D7+D8+D10) | 25 | 11 |
| R1 合计 | 125 turns | 51 findings |
Round 2(增量补漏 + 深度追踪)
| Agent | 方向 | max_turns | 新发现 |
|---|---|---|---|
| R2-Agent 1 | D4+D9(未覆盖维度) | 20 | 4 |
| R2-Agent 2 | 权限注解 AOP + SQL 查询构建层(浅覆盖加深) | 20 | 5 |
| R2-Agent 3 | 跨模块攻击链验证 | 20 | 3 |
| R2 合计 | 60 turns | 12 findings |
12 个 Critical 漏洞 :
---|---|---|---
C-01 | JWT 签名验证完全缺失 | 9.1 | JWT.decode() 无 verify(),可伪造任意用户
C-02 | 权限注解无 AOP 实现 | 9.8 | 社区版鉴权函数永远返回 true
C-03 | 嵌入式数据库存储过程 RCE | 9.0 | JDBC URL 注入 → 执行任意 Java 代码
C-04 | SQL 查询构建层 9+ 注入点 | 8.8 | String.format() 拼接表名/字段名
C-05 | JDBC 连接参数反序列化 | 8.6 | 连接参数绕过黑名单触发反序列化
Skill 支持四种模式,适应不同场景:
| 模式 | 适用场景 | 轮次 | Agent 数 | 特点 |
|---|---|---|---|---|
| Quick | CI/CD 集成、小项目 | 1 轮 | 2-3 | 高风险漏洞 + 密钥泄露 + 依赖 CVE |
| Quick-Diff | PR 审查、增量提交 | 1 轮 | 1-2 | 只审计 git diff 变更的文件 |
| Standard | 常规安全审计 | 1-2 轮 | 3-9 | OWASP Top 10 完整覆盖 |
| Deep | 关键系统、渗透测试前 | 2-4 轮 | 5-15 | 全量覆盖 + 攻击链 + 业务逻辑 |
Skill 的知识不是全部写在一个文件里的。它采用模块化设计,按需加载:
| 模块 | 功能 |
|---|---|
| Anti-Hallucination | 防幻觉规则和验证流程 |
| Taint Analysis | 污点分析和数据流追踪模板 |
| PoC Generation | 验证模板生成 |
| Capability Baseline | 防止能力退化的回归测试框架 |
Skill 对发现的置信度有严格的分级标准:
| 等级 | 条件 | 可报告的最高严重度 |
|---|---|---|
| 已验证 | 完整数据流 + 无有效防护 + 可构造 PoC | Critical |
| 高置信 | 完整数据流 + 无有效防护,但 PoC 需特定环境 | Critical/High |
| 中置信 | 数据流不完整,或防护可绕过性不确定 | Medium |
| 需验证 | 仅 Grep 命中模式,未追踪数据流 | Low/Info |
整个审计过程由一个四状态执行状态机驱动:
PHASE_1_RECON
│ Phase 1 完成
▼
ROUND_N_RUNNING ◄──────────────────────────┐
│ 所有 Agent 完成 │
▼ │
ROUND_N_EVALUATION │
│ │
├── 覆盖率不足 → NEXT_ROUND ─────────────►│
│
└── 覆盖率达标 + 弹性终止条件满足
│
▼
REPORT
早期没有截断防御机制,Agent 在最后一个 turn 还在做 Grep → 输出超长 → 被截断 → HEADER 和发现表格全部丢失 → 主线程拿到空结果。 修复 :Turn 预留规则 + HEADER 前置 + SENTINEL 哨兵 + 截断检测与恢复流程。
R2 Agent 不知道 R1 做了什么,重新搜索相同的模式、重读相同的文件。 修复 :跨轮传递结构(COVERED/GAPS/CLEAN/HOTSPOTS/FILES_READ/GREP_DONE)。
| 维度 | 传统 SAST (Semgrep/SonarQube) | code-audit Skill |
|---|---|---|
| 检测方式 | 固定规则模式匹配 | LLM 语义理解 + 数据流追踪 |
| 数据流追踪 | 有限(通常 1-2 跳) | 深度(3+ 跳,含中间转换层) |
| 业务逻辑漏洞 | 几乎无法检测 | 可检测(D9 维度) |
| 误报率 | 较高(20-40%) | 较低(防幻觉规则约束) |
| 攻击链分析 | 无 | 支持多漏洞串联分析 |
code-audit Skill 不是一个 prompt template,而是一个完整的审计系统。它的核心创新在于: