上一篇做完“AI 先写,再让它自己审”的实验后,我留下了一个问题:
如果不再允许模型泛泛地说“可能有风险”,而是要求每条结论必须绑定到代码行、引用原始代码,它的 SQL 注入误报会不会消失?
我拿同一段参数化查询代码做了三组对照。结论是:只绑定行号,误报不会消失;再加机器可检查的证据门禁,误报标签才会消失,但模型仍可能在证据字段上填错。
实验代码
前面实验里,模型生成的登录接口使用了参数化查询:
cursor.execute(
"SELECT * FROM users WHERE username = ? AND password = ?",
(username, password),
)
这段代码没有 SQL 字符串拼接。
用 code-audit-cli 扫描:
code-audit ai_generated_login.py --format json
结果是:
high: 0
medium: 0
low: 0
total: 0
下面三组审查都用同一个本机模型 qwen2.5vl:3b,temperature=0,只改变审查提示词。
A 组:宽泛审查
提示词只说:
请作为代码安全审查员审查这个 Flask 登录接口。
逐条列出你能确认的风险,区分代码明确暴露的风险和部署环境风险。
模型给出:
SQL注入风险:username 和 password 直接用于 SQL 查询,
没有进行任何参数化查询,容易导致 SQL 注入攻击。
这是明确的误报。代码里已经使用了 ? 占位符。
B 组:强制绑定行号和代码证据
第二组不再让模型自由总结,而是要求:
- 每条结论必须绑定明确行号;
- 必须引用该行原始代码;
- 区分输入来源、SQL 组装方式、执行位置和参数绑定方式;
- 只有看到用户输入被拼接进 SQL 字符串时,才能标记
confirmed; - 如果没有拼接证据,必须标记
false_positive或inconclusive。
模型输出了一个 Markdown 表格:
finding_id | status | line | code_quote | input_source | sql_sink |
concatenation_evidence | bind_parameter_evidence | conclusion
1 | confirmed | 15 |
"SELECT * FROM users WHERE username = ? AND password = ?", (username, password) |
user input | SQL query | Yes | No | SQL injection confirmed
2 | confirmed | 15 |
"SELECT * FROM users WHERE username = ? AND password = ?", (username, password) |
user input | SQL query | Yes | No | SQL injection confirmed
它虽然绑定了行号,也引用了原始代码,却仍然给出了两个错误判断:
concatenation_evidence = Yes,但代码里没有拼接符;bind_parameter_evidence = No,但代码里明确存在?和参数元组。
所以,仅仅要求“结论绑定代码行”,并不能保证模型真的读了那一行。
C 组:机器可检查的证据门禁
第三组把要求进一步收紧,不再让模型自己决定怎样才算风险。
提示词要求它依次检查:
检查 A:14-17 行是否出现 SQL 拼接符号:
+、%、.format(、f" 或 f'
检查 B:execute 的 SQL 字符串是否包含 ? 占位符,
并且第二个参数是否是变量元组。
判定规则:
- 只有检查 A 不是 NONE,才能写 sql_injection=confirmed;
- 如果检查 A 是 NONE,并且检查 B 同时存在占位符和参数元组,
必须写 sql_injection=false_positive。
模型输出:
{
"check_a_matches": "NONE",
"check_b_placeholder": "SELECT * FROM users WHERE username = ? AND password = ?",
"check_b_parameter_tuple": "NONE",
"sql_injection": "false_positive"
}
最终标签终于从 confirmed 变成了 false_positive。
但不要急着说“问题解决了”。
模型虽然正确判断出没有拼接符号,却仍然把 check_b_parameter_tuple 填成了 NONE。代码里明明有:
(username, password)
也就是说:
误报标签消失了,但不代表模型真正理解了代码。它只是被外部规则强制导向了正确标签,内部证据字段仍然可能是错的。
这组实验说明了什么
1. 绑定行号只能提高可检查性,不能提高理解力
行号让审查结论从“泛泛而谈”变成了“可以核对”。
但如果模型没有正确读取该行,行号反而会给错误结论增加一种“有证据”的假象。
2. 最终结论和中间证据要分开验证
C 组的最终分类是对的,但中间证据字段是错的。
这说明不能只看结论:
AI 说 false_positive
还要继续看:
它引用的行对不对?
它引用的代码片段对不对?
它对拼接和参数绑定的判断对不对?
3. 判定规则应该尽量放在模型外面
让模型自由判断“有没有 SQL 注入”,它可能套用模式。
把规则改成“是否出现拼接符”“是否存在占位符和参数元组”,结论会稳定一些。
但最可靠的方式仍然是:
- 模型只负责提取证据;
- 判定规则由代码或人工执行;
- 关键字段独立复核。
实际审查流程应该怎么改
这次实验之后,我会这样处理 AI 安全审查:
第一步:先让扫描器找可疑位置
code-audit app --format json --output scan.json
扫描器负责定位模式,不负责最终判决。
第二步:让 AI 只做证据提取
要求它输出:
- 文件;
- 行号;
- 原始代码;
- 输入来源;
- 数据流向;
- 危险点;
- 反证。
不要让它直接写“存在 SQL 注入”这种最终结论。
第三步:用规则或人做最终判定
例如 SQL 注入至少要看清楚:
SQL 字符串里有没有拼接用户输入?
还是用户输入作为参数单独传给了 execute?
这两件事不能混为一谈。
第四步:修复后重新验证
不要只看模型说“已修复”。
重新跑扫描,回到真实调用路径,再用测试确认行为。
回到最初的问题
把审查结论强制绑定到代码行后,AI 的 SQL 注入误报会不会消失?
答案分三层:
- 只绑定行号:不会,模型仍可能误报;
- 加入机器可检查的证据门禁:最终误报标签可以消失;
- 想得到可靠结论:还要验证模型提取的证据字段,不能把最终标签当成理解正确的证明。
AI 可以参与代码审查,但更适合负责“找线索、提证据、生成待核项”。
最终判断仍然应该由确定性的规则、扫描器或人来完成。
公开规则、示例报告和扫描命令可以在这里核对:
仓库只放规则和示例,不包含完整源码;自动扫描结果和 AI 审查结论都需要人工确认。