二次SQL注入本质是“存进去安全、取出来危险”,修复须覆盖写入、存储、读取、拼接四环节;定位关键在追踪同一字段(如username)跨请求的处理差异,而非孤立查INSERT或SELECT。

二次SQL注入导致的越权访问,本质是数据“存进去安全、取出来危险”——修复必须覆盖写入、存储、读取、拼接四个环节,不能只改某一处。
怎么定位二阶注入的存储-触发链
关键不是找单个 INSERT 或 SELECT,而是追踪同一字段在不同请求中的处理差异。比如用户注册时存入 username,后台导出报表时又用它拼进统计 SQL,这就构成完整攻击链。
- 先全局搜索所有写入点:
INSERT INTO.*users.*username、UPDATE.*SET.*username - 再搜索所有读取后拼接的地方:
WHERE username = '.*' \+ username \+ '.*'、"username = '" + username + "'" - 特别注意 ORM 混用场景:比如用
User::find()查出对象,再手动提取$user->username去拼原生DB::statement()—— 这里参数化保护已断开 - 缓存层也得查:从 Redis 或 Memcached 读出的值若直接拼 SQL,和从数据库读没区别
为什么 Replace 单引号或正则过滤完全无效
二阶注入的 payload 是持久化在数据库里的,比如存入 admin'-- 或 1 OR 1=1,后续查询时根本不会经过前端输入过滤逻辑。任何运行时字符串替换(如 Replace("'", "''"))在读取阶段早已失效。
- 宽字节注入(如
%df%27)可绕过简单转义 - 数字型字段不带引号,
id=1 OR 1=1完全不受单引号过滤影响 - 存储过程里用
EXEC(@sql)动态执行,等于把注入点搬进数据库内部
Legacy ASP.NET 中必须改的三类代码位置
旧项目里最常漏掉的是中间环节的信任假设,修复要直击这三处:
-
ExecuteNonQuery()和ExecuteReader()调用前的 SQL 字符串:只要含+拼接用户变量(哪怕来自数据库),就必须替换成SqlParameter - 存储过程内部:禁止
EXEC(@dynamic_sql),改用sp_executesql并显式传参,且参数必须声明类型(@username NVARCHAR(50)) - 后台脚本/定时任务:导出报表、日志归档、批量同步等代码常被忽略,它们从 DB 读出数据后若拼 SQL,就是典型的二阶入口
最容易被跳过的其实是“读取即信任”这个动作——只要数据源不是当前请求的原始输入,就默认不可信。哪怕是从自己数据库里 SELECT 出来的值,只要下一步要拼进新 SQL,就必须走 SqlParameter,没有例外。











