sp_executesql是唯一安全选项,因其在编译阶段严格分离sql字符串与参数值,参数不参与语法解析;而exec(@sql)将整个字符串当t-sql执行,极易引发sql注入。

必须用 sp_executesql,不能用 EXEC(@sql) —— 否则等于主动放行 SQL 注入。
为什么 sp_executesql 是唯一安全选项
根本区别在编译阶段: sp_executesql 把 SQL 字符串和参数值完全分离,参数值不参与语法解析;而 EXEC(@sql) 把整个字符串当 T-SQL 执行,用户输入一旦混入,就是直接执行。
- 常见错误现象:
SET @sql = 'WHERE id = @id'; EXEC(@sql)会报错Msg 137, Level 15, State 2: 必须声明标量变量 "@id"—— 因为EXEC根本不认识外部变量 -
sp_executesql的第二个参数(参数声明)必须是N'@id INT, @name NVARCHAR(50)'这种显式类型+精度的写法,不能写NVARCHAR(MAX)或漏掉长度 - 第三个及之后的参数必须带
@param_name =前缀,比如@id = @user_id,不能只传@user_id - 哪怕加了
REPLACE(@input, '''', ''''''),攻击者仍可用1; DROP TABLE users; --绕过
表名、列名、排序字段这些标识符怎么处理
它们无法参数化,硬拼进字符串就是高危操作。白名单校验 + QUOTENAME() 是唯二靠谱手段。
- 先白名单过滤:
IF @sort_col NOT IN ('created_at', 'status', 'amount') RETURN - 再查系统视图确认存在:
EXISTS (SELECT 1 FROM sys.columns WHERE name = @col AND object_id = OBJECT_ID(@table)) - 最后拼接前必须套
QUOTENAME(@table_name)—— 它自动加方括号并转义非法字符,比如'users; DROP TABLE users--'变成[users; DROP TABLE users--],合法但无害 - 别自己写
'[' + @table_name + ']',漏单引号或特殊字符就炸
动态 WHERE 条件拼接的防错写法
空条件导致语法错误是最常踩的坑,靠一堆 IF 追加 AND 字符串既难读又易错。
- 初始化语句用
N'SELECT * FROM users WHERE 1=1',后续每个条件都以AND开头,SQL Server 优化器能忽略1=1,不影响性能 - 日期范围单独判断:
IF @start_date IS NOT NULL SET @sql += N' AND created_at >= @start_date' - 多个
LIKE条件,统一在参数里加通配符:SET @name_param = N'%' + @name + N'%',不要拼'%' + @name + '%'到字符串里 - 避免拼出
WHERE 1=1 AND结尾多一个AND,虽然逻辑上不报错,但看着难受且难调试
参数声明和输入校验不能省
用了 sp_executesql 不代表高枕无忧。宽泛类型或缺失校验,照样留后门。
- 数字类参数声明为具体类型(
INT、TINYINT),开头加硬约束:IF @user_id 999999 RETURN - 字符串类参数立即检查长度:
IF LEN(@name) = 0 OR LEN(@name) > 50 RETURN - 避免在存储过程里对输入做
CAST/CONVERT—— 转换失败报错,转换成功可能已失真 - 最容易被忽略的是隐性拼接点:比如用
CONTEXT_INFO存用户 ID 后在触发器里拼 SQL,或日志语句里拼接未过滤的字段值 —— 只要字符串里混入未经白名单或QUOTENAME()处理的运行时变量,就构成注入风险
真正危险的地方,往往不是你明明白白写的那句 sp_executesql,而是你以为“只是记录一下”的日志拼接、或者“应该没问题”的上下文传递。只要没走白名单或 QUOTENAME(),就不是安全的动态 SQL。











