把审查结论强制绑定到代码行后,AI 的 SQL 注入误报会不会消失?

41 阅读5分钟

上一篇做完“AI 先写,再让它自己审”的实验后,我留下了一个问题:

如果不再允许模型泛泛地说“可能有风险”,而是要求每条结论必须绑定到代码行、引用原始代码,它的 SQL 注入误报会不会消失?

我拿同一段参数化查询代码做了三组对照。结论是:只绑定行号,误报不会消失;再加机器可检查的证据门禁,误报标签才会消失,但模型仍可能在证据字段上填错。

cover-1920x1080.png

实验代码

前面实验里,模型生成的登录接口使用了参数化查询:

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 组:强制绑定行号和代码证据

第二组不再让模型自由总结,而是要求:

  1. 每条结论必须绑定明确行号;
  2. 必须引用该行原始代码;
  3. 区分输入来源、SQL 组装方式、执行位置和参数绑定方式;
  4. 只有看到用户输入被拼接进 SQL 字符串时,才能标记 confirmed;
  5. 如果没有拼接证据,必须标记 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 注入”,它可能套用模式。

把规则改成“是否出现拼接符”“是否存在占位符和参数元组”,结论会稳定一些。

但最可靠的方式仍然是:

  1. 模型只负责提取证据;
  2. 判定规则由代码或人工执行;
  3. 关键字段独立复核。

实际审查流程应该怎么改

这次实验之后,我会这样处理 AI 安全审查:

第一步:先让扫描器找可疑位置

code-audit app --format json --output scan.json

扫描器负责定位模式,不负责最终判决。

第二步:让 AI 只做证据提取

要求它输出:

  • 文件;
  • 行号;
  • 原始代码;
  • 输入来源;
  • 数据流向;
  • 危险点;
  • 反证。

不要让它直接写“存在 SQL 注入”这种最终结论。

第三步:用规则或人做最终判定

例如 SQL 注入至少要看清楚:

SQL 字符串里有没有拼接用户输入?
还是用户输入作为参数单独传给了 execute?

这两件事不能混为一谈。

第四步:修复后重新验证

不要只看模型说“已修复”。

重新跑扫描,回到真实调用路径,再用测试确认行为。

回到最初的问题

把审查结论强制绑定到代码行后,AI 的 SQL 注入误报会不会消失?

答案分三层:

  1. 只绑定行号:不会,模型仍可能误报;
  2. 加入机器可检查的证据门禁:最终误报标签可以消失;
  3. 想得到可靠结论:还要验证模型提取的证据字段,不能把最终标签当成理解正确的证明。

AI 可以参与代码审查,但更适合负责“找线索、提证据、生成待核项”。

最终判断仍然应该由确定性的规则、扫描器或人来完成。

公开规则、示例报告和扫描命令可以在这里核对:

github.com/yuan1521913…

仓库只放规则和示例,不包含完整源码;自动扫描结果和 AI 审查结论都需要人工确认。