必须用 sp_executesql 防注入,因其支持参数化执行,而 exec(@sql) 会将拼接字符串整体解析为代码,导致注入;sp_executesql 要求三段式严格调用:占位符sql、类型化参数声明、@param=value赋值;表名列名等需白名单校验+quotename()封装。

必须用 sp_executesql,不能用 EXEC(@sql) —— 后者等于把用户输入当代码执行,防注入无从谈起。
为什么 EXEC(@sql) 一拼就炸
常见错误是写成 SET @sql = 'SELECT * FROM users WHERE id = ' + @id; EXEC(@sql);。只要 @id 是 '1; DROP TABLE logs; --',就会直接删表。REPLACE 单引号、加引号包裹、甚至用 QUOTENAME() 包裹 @id 都没用——因为 @id 已经被提前拼进字符串,SQL Server 在解析阶段就把整条语句当代码了。
根本问题在于:拼接发生在语法解析前,而注入载荷在运行时才生效。防御必须卡在解析与执行之间,只有 sp_executesql 能做到这点。
-
EXEC(@sql)每次都重新编译,无法复用执行计划,性能差 - 它不区分“结构”和“数据”,所有内容一律当 SQL 文本处理
- 哪怕只拼一个数字,
CAST(@id AS NVARCHAR)进字符串也是拼接,不是参数化
sp_executesql 的三段式写法必须对齐
正确调用不是“用了就行”,而是三部分缺一不可、顺序和类型必须严丝合缝:
- 第一段:
@sql字符串里只出现占位符,如N'SELECT * FROM users WHERE status = @status AND name LIKE @name'—— 里面不能有+、CONCAT或任何变量值 - 第二段:参数签名必须显式声明类型和长度,如
N'@status TINYINT, @name NVARCHAR(50)'—— 写成N'@status INT'而实际传的是字符串,可能隐式转换失败;写成N'@name NVARCHAR(MAX)'则扩大攻击面 - 第三段及之后:每个参数必须带
@param = value形式,如@status = 1, @name = N'John'—— 不能只写@status, @name,也不能漏掉@param =前缀
错一位,比如把 @name 的值传给了 @status 的位置,轻则查不到数据,重则类型不匹配报错中断流程。
表名、列名这些没法参数化的部分怎么保命
字段名、排序字段、表名、schema 名……这些属于语法结构,sp_executesql 不支持参数化。硬拼就是高危操作,但“不能参数化”不等于“可以乱拼”。安全底线是:先校验,再 QUOTENAME(),最后拼。
- 白名单优先:比如排序字段只允许
'created_at'、'status'、'amount',用IF @sort_col NOT IN ('created_at', 'status', 'amount') THROW 50000, 'Invalid sort column', 1; - 系统视图校验:查
sys.tables确认表真实存在且归属指定 schema:IF NOT EXISTS (SELECT 1 FROM sys.tables t JOIN sys.schemas s ON t.schema_id = s.schema_id WHERE s.name = 'dbo' AND t.name = @table_name) RETURN; -
QUOTENAME()只用于拼接前的最后一道封装,如QUOTENAME(@table_name)→[users],或QUOTENAME(@schema) + '.' + QUOTENAME(@table)→[dbo].[users]—— 它不负责校验,只负责转义,所以必须在校验通过后才调用
别用 OBJECT_ID(@table_name) 单独判断——它不检查 schema,'malicious].[table' 可能被截断后误判为合法。
参数声明和输入校验不是可选项
用了 sp_executesql 不代表万事大吉。如果参数类型太宽、没做范围检查,攻击面依然敞开。
- 数字类参数必须声明具体类型:
@user_id INT,而不是SQL_VARIANT或NVARCHAR(MAX);开头加硬约束:IF @user_id 999999 RETURN - 字符串类参数立即检查长度和内容:
IF LEN(@name) = 0 OR LEN(@name) > 50 OR @name NOT LIKE '[a-zA-Z0-9_]%' RETURN - 禁止在存储过程中对输入做
CAST或CONVERT—— 转换失败会报错,转换成功可能已失真(比如把'1 OR 1=1'强转成INT得到1,掩盖了非法输入)
最容易被忽略的是隐性拼接点:比如用 CONTEXT_INFO 存用户 ID 后在触发器里拼 SQL,或者用 OPENROWSET 构造远程查询字符串——这些地方不走 sp_executesql,也不受参数化保护,得单独审计。











