sp_executesql 是唯一安全选择,因其在编译阶段分离代码与数据,参数不参与语法解析;而exec将整串字符串当t-sql执行,易致注入。

必须用 sp_executesql,不能用 EXEC 拼接字符串——这是防注入的不可妥协底线。哪怕加了 REPLACE(@input, '''', ''''''),攻击者仍可用 1; DROP TABLE users; -- 绕过。
为什么 sp_executesql 是唯一安全选择
sp_executesql 在编译阶段就分离代码与数据,参数值不参与语法解析;而 EXEC 会把整个字符串当 T-SQL 执行,用户输入一旦混入,就是直接执行恶意代码。
- 动态 SQL 字符串里只允许出现静态结构,所有变量位置必须用
@param_name占位(如WHERE id = @id) - 第二个参数(如
N'@id INT, @name NVARCHAR(50)')必须显式声明类型和长度,不能写NVARCHAR(MAX)或漏掉精度 - 第三个及之后的参数必须带
@param_name =前缀,不能只传变量名或裸值
表名、列名等对象名怎么处理
对象名(如 @table、@sort_col)无法参数化,硬拼等于开门揖盗。REPLACE、正则“过滤”、自己加单引号都无效。
- 白名单校验最可靠:比如
IF @sort_col NOT IN ('created_at', 'status', 'amount') RETURN - 查系统视图确认存在且归属预期 schema:
EXISTS (SELECT 1 FROM sys.columns WHERE name = @col AND object_id = OBJECT_ID(@table)) - 拼接前必须套
QUOTENAME(@name)——仅用于对象名,永远不用于数据值
参数声明和输入校验不能省
用了 sp_executesql 不代表高枕无忧。宽泛类型或缺失校验,照样留后门。
- 数字类参数声明为具体类型(
INT、TINYINT),开头加硬约束:IF @user_id 999999 RETURN - 字符串类参数立即检查长度:
IF LEN(@name) = 0 OR LEN(@name) > 50 RETURN - 避免在存储过程里对输入做
CAST/CONVERT——转换失败报错,转换成功可能已失真
别漏掉隐性拼接点
最容易被忽略的地方往往最危险:比如用 CONTEXT_INFO 存用户 ID 后在触发器里拼 SQL,或者用 OPENROWSET 构造远程查询字符串,甚至在日志语句里拼接未过滤的字段值。
只要字符串里混入未经白名单校验或 QUOTENAME() 处理的运行时变量,就构成注入风险——和是否用了 sp_executesql 无关。











