二次注入漏洞源于数据取出时拼接SQL,而非输入时;恶意payload先安全存入数据库,后续从DB读取后未经校验直接拼接新SQL,导致防护措施在模块割裂处失效。

因为防御者容易在“数据取出”环节放松警惕,误以为数据库里的数据天然可信。
二次注入的漏洞触发点不在输入时,而在取数拼接时
普通注入的恶意 payload 一提交就立刻参与 SQL 构建,所有防护逻辑(比如 mysql_real_escape_string()、addslashes())都集中在入口处,扫描和审计也容易覆盖到。而二次注入的 payload 是先被安全存入数据库的——可能用了参数化插入,也可能被转义后存为 admin\'-- 这样的字符串。它看起来无害,日志里不报错,WAF 也放行。真正危险的是后续某段代码从数据库查出这个字段,再用 $_POST['xxx'] 那种方式直接拼进新 SQL:比如 $sql = "UPDATE users SET pass='$old_pass' WHERE username='$username'";,而 $username 正是从 DB 读出来的脏数据。
代码割裂导致防护断层
注册模块用 PDO 参数化,但修改密码模块是老代码,用的是 mysql_query() + 字符串拼接;前端做了输入限制,后端取数逻辑却完全没校验;甚至 ORM 查询里混用了原生 SQL 片段——这些情况都会让防护措施只生效于局部。常见表现包括:
- 同一个变量名(如
$user_name)在不同函数里含义不同:一处是用户刚提交的,另一处是从SELECT * FROM users取出的 - 缓存层绕过校验:从 Redis 读出的用户昵称直接拼 SQL,没走任何过滤逻辑
- 日志/审计模块把数据库字段原样写进查询语句,例如按昵称导出报表:
WHERE nickname LIKE '%$keyword%',而$keyword来自 DB 字段
静态扫描和黑盒测试很难覆盖“存储→取出→再拼接”链路
自动化工具通常只检测单次请求中的输入输出,对跨请求、跨模块的数据流转无感。比如:
- SQLMap 对注册接口扫不出问题,因为它只看 INSERT 是否可注入;但它不会自动跟踪“这个 INSERT 进去的值,会不会在三天后的后台任务里被拿出来拼 UPDATE”
- 代码审计如果只看单个文件,容易漏掉
get_user_by_id()返回的$row['nickname']被传给另一个模块的build_report_sql() - 单元测试往往只 mock 输入,不模拟真实数据库存取闭环,所以带 payload 的记录根本不会出现在测试数据里
最麻烦的地方在于:你修复了所有显式的用户输入点,却忘了问一句——“这段 SQL 里用的变量,到底是谁塞进去的?它上一次出现在哪?”

















