存储过程本身不免疫sql注入,关键在于是否使用动态sql拼接用户输入;正确做法是严格白名单校验元数据、仅对值参数化、杜绝字符串拼接,且输出数据需在应用层重新验证。

存储过程里用 EXEC 或 sp_executesql 拼接字符串就危险
存储过程本身不是“免疫”SQL注入的银弹。只要内部用了动态SQL,并且把用户输入直接拼进 EXEC('... ' + @input) 或未参数化的 sp_executesql,风险就和应用层拼接一模一样。
常见错误场景包括:按字段名排序(@sortColumn)、按条件动态加 WHERE 子句、构建表名或列名等元数据操作。
- 错误示例:
EXEC('SELECT * FROM users ORDER BY ' + @sortColumn)—— 攻击者传入'name; DROP TABLE users--'就能触发删除 - 正确做法:对这类元数据使用白名单校验,比如只允许
@sortColumn IN ('name', 'email', 'created_at') - 若必须动态,用
sp_executesql配合参数化,但仅限值(@value),不能用于对象名(@tableName)
sp_executesql 不等于安全,关键看怎么用
sp_executesql 是 SQL Server 提供的安全执行接口,但它只是工具——传参方式决定是否真安全。很多人误以为用了它就万事大吉,结果还是拼字符串。
- 危险写法:
sp_executesql N'SELECT * FROM ' + @tableName + ' WHERE id = ' + @id——@tableName和@id全部被拼进去,毫无防护 - 安全写法:
sp_executesql N'SELECT * FROM users WHERE status = @status', N'@status NVARCHAR(10)', @status = @inputStatus—— 只有@status是参数,其余都是硬编码或白名单控制 - 注意:
sp_executesql的第二个参数是参数定义字符串,第三个及之后才是对应值;漏掉定义或类型不匹配,可能绕过类型检查
参数声明 ≠ 输入过滤,类型宽松照样出事
即使存储过程声明了 @username NVARCHAR(50),也不代表它自动过滤了单引号或分号。SQL Server 不会对参数内容做语义解析或关键字拦截,只做长度和类型约束。
- 攻击者仍可传入
'admin'' OR ''1''=''1',如果后续逻辑又把它拼进动态SQL,照样生效 - 例如:
DECLARE @sql NVARCHAR(MAX) = 'SELECT * FROM users WHERE name = ''' + @username + ''''; EXEC(@sql)—— 参数再“安全”,拼进去就废了 - 真正起作用的是“不拼”:所有用户可控值,只作为参数出现在最终执行语句的
@param占位位置,不参与字符串拼接
容易被忽略的盲点:输出参数和返回值也能带毒
存储过程常通过输出参数或 SELECT 返回结果集给上层应用。如果这些值来自不可信来源(比如从日志表查出的原始用户输入、未清洗的配置项),而调用方又把它二次拼进新SQL,就形成“注入接力”。
- 典型链路:Web 层 → 存储过程A(查出
@raw_input)→ 应用层拼成新SQL → 执行 → 中招 - 解决方案不是靠存储过程“更干净”,而是明确边界:存储过程只负责查/写,不负责“信任传递”;应用层拿到任何输出,都要当作新输入重新验证或参数化
- 尤其警惕
INSERT ... EXEC或临时表中存入未经处理的用户数据,后续再读取拼SQL时极易遗漏
真正卡住风险的地方,从来不是“用了存储过程”,而是“有没有让任意输入穿过字符串拼接这道闸门”。哪怕只有一行 + 或一个没加引号的变量名,整条链路就失效了。











