AI 辅助代码安全审计:从 SAST 到 LLM 增强审查
代码安全审计(Security Audit)的目标,是在漏洞被攻击者利用之前,把注入、越权、敏感信息泄露这类缺陷从代码里挖出来。传统做法依赖 SAST(静态应用安全测试)与 DAST(动态应用安全测试)工具做规模化扫描,而 LLM 的加入带来了一层「能读懂业务语义、能跨文件推理」的增强审查能力。本文先建立漏洞认知框架,再讲清工具之间如何互补,最后给出可直接复用的提示词与一段含漏洞代码的实战修复示例。
一、先建立漏洞认知框架:OWASP Top 10 与 CWE Top 25
在让工具干活之前,得先知道「要找什么」。两个权威清单是最好的地图。
OWASP Top 10:2025
OWASP Top 10 是面向开发者的 Web 应用安全风险共识列表。截至 2025 版本,十大类为:
| 编号 | 类别 | 与本文相关的重点 |
|---|---|---|
| A01 | Broken Access Control(失效的访问控制) | 越权、IDOR 的核心来源 |
| A02 | Security Misconfiguration(安全配置错误) | 默认口令、暴露的调试接口 |
| A03 | Software Supply Chain Failures(软件供应链失效) | 依赖投毒、构建完整性 |
| A04 | Cryptographic Failures(加密机制失效) | 明文存储、弱算法 |
| A05 | Injection(注入) | SQL 注入、命令注入 |
| A06 | Insecure Design(不安全设计) | 缺少威胁建模 |
| A07 | Authentication Failures(认证失效) | 弱凭证、会话固定 |
| A08 | Software or Data Integrity Failures(软件与数据完整性失效) | 不安全的反序列化 |
| A09 | Security Logging and Alerting Failures(安全日志与告警失效) | 无法追溯攻击 |
| A10 | Mishandling of Exceptional Conditions(异常处理失当) | 错误信息泄露内部细节 |
CWE Top 25:2025
CWE(常见弱点枚举)从「具体代码缺陷」层面给漏洞编号,比 OWASP 更细,也更适合作为审计时的排查清单。2025 榜单中与本文三类重点问题高度相关的有:
- CWE-79:跨站脚本(XSS),榜单第 1 位。
- CWE-89:SQL 注入,第 2 位。
- CWE-78:OS 命令注入,第 9 位。
- CWE-94:代码注入,第 10 位。
- CWE-862:缺少授权(Missing Authorization),第 4 位,越权的典型。
- CWE-639:通过用户可控键绕过授权(Authorization Bypass),即常见 IDOR。
- CWE-200:向未授权方暴露敏感信息,第 20 位。
- CWE-502:反序列化不受信任数据,第 15 位。
审计时建议把 OWASP 当「风险大类」做面,把 CWE 当「具体弱点」做点,两者结合才不会漏项。
二、传统 SAST/DAST 与 LLM 增强审计如何互补
三者不是替代关系,而是分层的组合拳。
| 维度 | SAST(静态) | DAST(动态) | LLM 增强审查 |
|---|---|---|---|
| 工作时机 | 编码/提交阶段 | 运行阶段 | 编码/审查/复盘阶段 |
| 擅长 | 模式化、确定性的缺陷(危险函数、硬编码密钥) | 运行态真实利用(鉴权绕过、配置错误) | 语义理解、跨文件逻辑、业务越权 |
| 短板 | 误报多、看不懂业务意图 | 覆盖依赖流量、难定位代码行 | 可能幻觉、结果非确定性、需喂上下文 |
| 定位 | 第一道确定性滤网 | 真实利用验证 | 推理层与降噪层 |
互补的典型用法有三类:
- SAST 出清单,LLM 做降噪。SAST 一次报几百条,人看不过来。把告警连同代码片段交给 LLM,让它按「是否真实可达、危害等级、修复成本」排序,先把明显误报滤掉。
- LLM 补 SAST 看不到的盲区。注入、硬编码密钥 SAST 很强,但「这个接口是否校验了当前用户是否有权查看这条记录」这种业务逻辑,SAST 基本无能为力,LLM 结合上下文能判断。
- LLM 当 DAST 的辅助解释器。DAST 发现一个越权响应,LLM 可反向定位到对应的鉴权缺失代码,并给出最小修复。
原则:让 SAST/DAST 做「确定性的广撒网」,让 LLM 做「语义化的精加工」。最终是否修复、如何修复,仍由人拍板。
三、提示策略:让 LLM 发现注入、越权、敏感信息泄露
LLM 审计的效果高度依赖提示词的结构。笼统地说「帮我看看有没有安全问题」会换来一堆泛泛而谈。有效的提示策略包含五点:
- 定角色与基准:明确「你是资深应用安全工程师,熟悉 OWASP Top 10:2025 与 CWE Top 25」,让模型套用专业框架。
- 明确排查清单:把要找的问题枚举出来(注入 / 越权 / 敏感信息泄露),而不是开放式的「安全问题」。
- 要求带证据:强制输出「CWE 编号 + 行号 + 风险等级 + 成因」,没有定位的结论一律视为无效。
- 要求修复而非改代码:让模型只出「修复建议」,不要让它直接重写整段,避免引入新变量或悄悄改变行为。
- 分批聚焦:一次只盯一类问题(例如先注入、再越权),比一次性全查更准,也便于人工逐条核对。
可复用的审计提示词模板:
你是资深应用安全工程师,熟悉 OWASP Top 10:2025 与 CWE Top 25。
请审计下面这段代码,本次只聚焦三类问题:
1. 注入:用户输入是否未经转义就进入 SQL、命令、HTML 或反序列化路径。
2. 越权:是否校验了当前调用者对该资源拥有访问权限(Broken Access Control / IDOR)。
3. 敏感信息泄露:是否向调用方、日志或报错中暴露密钥、token、SSN 等数据。
逐条输出:对应 CWE 编号、文件:行号、风险等级(高/中/低)、成因、修复建议。
不要修改代码,只输出结构化审计结论。
针对「敏感信息泄露」可单独加一句:「检查异常分支是否把堆栈、SQL、内部路径回传给客户端,这类信息会协助攻击者构造利用」。
四、实战示例:含漏洞代码 + 审计 Prompt + 修复
下面以一段 Flask 接口为例,它同时踩中注入、越权、敏感信息泄露三个坑。
有漏洞的代码
from flask import Flask, request, jsonify
import sqlite3
DB_PATH = "app.db"
SECRET_KEY = "sk_live_1234567890abcdef" # 硬编码密钥,应移入环境变量(CWE-798)
app = Flask(__name__)
@app.route("/api/user")
def api_user():
uid = request.args.get("uid", "") # 用户输入,未校验、未鉴权
# 字符串拼接构造 SQL,存在 SQL 注入(CWE-89)
sql = "SELECT id, name, email, ssn FROM users WHERE id = " + uid
conn = sqlite3.connect(DB_PATH)
row = conn.execute(sql).fetchone()
if not row:
return jsonify({"error": "not found"}), 404
# 直接回传 ssn 等敏感字段,且未校验当前用户是否有权查看(越权 + 信息泄露 CWE-200/862)
return jsonify({
"id": row[0],
"name": row[1],
"email": row[2],
"ssn": row[3],
})
把上面这段代码连同第三节的提示词交给 LLM,它会给出类似结论:
- CWE-89(高):第 13 行字符串拼接
uid进 SQL,攻击者可构造0 OR 1=1拖库。 - CWE-862(高):接口未校验调用者身份,任意
uid都能查,构成越权。 - CWE-200(中):响应回传
ssn,违反最小暴露原则。 - CWE-798(中):
SECRET_KEY硬编码,应进环境变量或密钥管理。
修复后的代码
from flask import Flask, request, jsonify, g
import sqlite3
import os
DB_PATH = os.environ.get("DB_PATH", "app.db") # 配置从环境读取,不再硬编码密钥
app = Flask(__name__)
def current_user_id():
# 从已校验的会话中取当前用户标识(实际项目由鉴权中间件写入 g)
return g.get("user_id")
@app.route("/api/user")
def api_user():
uid = request.args.get("uid", "")
if not uid.isdigit(): # 先做输入类型校验
return jsonify({"error": "invalid id"}), 400
conn = sqlite3.connect(DB_PATH)
try:
# 使用参数化查询,杜绝 SQL 注入(CWE-89)
row = conn.execute(
"SELECT id, name, email FROM users WHERE id = ?",
(int(uid),),
).fetchone()
finally:
conn.close()
if not row:
return jsonify({"error": "not found"}), 404
# 越权校验:只允许查看属于自己的记录(CWE-862)
if int(uid) != current_user_id():
return jsonify({"error": "forbidden"}), 403
# 不再返回 ssn 等敏感字段(CWE-200)
return jsonify({"id": row[0], "name": row[1], "email": row[2]})
修复要点:参数化查询消除注入;current_user_id 比对消除越权;移除 ssn 字段收敛信息暴露;密钥改走环境变量。
五、工具链概览(仅能力描述)
| 工具 | 能力定位 |
|---|---|
| CodeQL | GitHub 推出的语义化代码分析引擎,核心理念是「把代码当数据来查询」。它用专门的 QL 查询语言编写规则,可跨函数、跨文件追踪数据流,官方覆盖了大量 CWE;深度集成 GitHub code scanning,能在 PR 上自动出安全告警,并支持自定义查询套件。 |
| Semgrep | 轻量、快速的 SAST 工具,规则可用类代码语法编写,覆盖 C/Go/Java/Python/JS/TS 等数十种语言,并提供 SAST、SCA(依赖可达性分析)与 Secrets(630 多种凭据识别)三类扫描。适合在本地与 CI 里做高频、低延迟的检查。 |
| CodeRabbit | 面向 PR 的 AI 代码审查助手,会在每次提交时给出带上下文的审查评论,其中含安全维度的提示,适合作为人工审查前的第一道语义滤网。 |
| GitHub Copilot Security | 依托 GitHub 安全能力的 AI 助手,可在 code scanning 告警旁提供解释与AI 自动修复建议(Autofix),帮助开发者在熟悉的流程里更快地理解并消解漏洞。 |
这些工具与 LLM 通用审计是互补的:前者提供确定性、可重复的基线扫描,后者提供灵活的业务语义推理。生产环境里通常把它们串成「提交即扫、AI 降噪、人工决断」的流水线。
小结
- 用 OWASP Top 10:2025 划「风险大类」,用 CWE Top 25:2025 定「具体弱点编号」,审计才不会漏项。
- SAST/DAST 做确定性的广撒网,LLM 做语义化的精加工与降噪,二者互补而非替代。
- 提示词要定角色、枚举排查项、强制带 CWE 编号与行号、只给修复建议不改代码、按问题分批聚焦。
- 实战中注入靠参数化查询消除,越权靠调用者身份比对消除,敏感信息泄露靠收敛返回字段消除。
- CodeQL、Semgrep、CodeRabbit、GitHub Copilot Security 提供确定性基线,通用 LLM 提供灵活推理,组合成流水线最稳。
- 无论工具多强,修复优先级与最终决策始终留在人手里,AI 是放大器不是裁判。
参考与延伸阅读
- OWASP Top 10:2025(风险大类清单与说明):https://owasp.org/Top10/
- Semgrep 官方文档(语言支持、SAST/SCA/Secrets 能力):https://semgrep.dev/docs/
- CodeQL 官方文档(QL 查询语言、CWE 覆盖、GitHub code scanning 集成):https://codeql.github.com/docs/
- CWE Top 25:2025(具体弱点编号与排名,含 CWE-79/89/862/200 等):https://cwe.mitre.org/top25/archive/2025/2025_cwe_top25.html
- GitHub code scanning 与 CodeQL 集成指南(把语义化扫描接进 PR 流程):https://docs.github.com/en/code-security/code-scanning