EXEC(@sql)是硬性红线,因其将整个字符串当代码执行,用户输入含分号、注释符或闭合括号即触发恶意语句;必须禁用,改用sp_executesql并严格参数化,结构部分走白名单或QUOTENAME()。

不能靠“拼得小心”防注入,必须从第一行就分清哪里是模板、哪里是数据——结构部分走白名单或 QUOTENAME(),运行时值一律走 sp_executesql 参数化,EXEC(@sql) 必须禁用。
为什么 EXEC(@sql) 是硬性红线
只要用户输入进了 @sql 字符串,哪怕只多一个分号、一个注释符或一个闭合括号,SQL Server 就会把它当代码执行。比如传入 @table = 'users; DROP TABLE logs; --',拼成 'SELECT * FROM ' + @table 后实际执行的就是完整恶意语句。
-
REPLACE(@input, '''', '''''')挡不住]; DROP TABLE x; --这类绕过 -
CAST/CONVERT转类型不解决根本问题,拼接发生在转换之前 - 前端 JS 校验、应用层过滤,后端照样能绕过——数据库才是最后一道防线
sp_executesql 的参数化必须写对
声明错、传参错、拼接错,三者任一出问题,参数化就形同虚设。关键细节全在参数声明和调用里:
-
@params字符串必须显式写全类型和长度:N'@status TINYINT, @since DATETIME2(3), @name NVARCHAR(50)',不能只写@status或用NVARCHAR(MAX) -
@sql字符串里只能出现参数占位符(如WHERE id = @id),绝不能出现CAST(@status AS VARCHAR)或字符串拼接 - 传参必须用
@param_name = @variable形式:@status = @input_status,不能只写@input_status - 所有存储过程输入参数本身也应强类型、窄范围,比如用
@id INT而非@id SQL_VARIANT
表名、列名、排序字段这些“结构”怎么处理
它们不是值,不能参数化,但也不能裸拼。安全路径只有两条,且必须二选一:
- 用
QUOTENAME()包裹,且必须先拆再包:QUOTENAME(@schema) + '.' + QUOTENAME(@table),而不是QUOTENAME(@schema + '.' + @table) - 更推荐硬白名单校验:
IF @table NOT IN ('users', 'orders', 'products') THROW 50000, 'Invalid table name', 1; - 排序字段(
@order_by)必须映射到固定列:CASE @order_by WHEN 'name' THEN 'user_name' WHEN 'id' THEN 'user_id' END,再拼进 SQL - 排序方向(
ASC/DESC)同样不能直拼,需用CASE映射或白名单校验
最容易被忽略的隐性拼接点
真正危险的往往不是主逻辑里的 sp_executesql,而是那些你以为“只是日志”“只是上下文”的地方:
- 用
CONTEXT_INFO存用户 ID 后,在触发器里拼 SQL - 用
OPENROWSET构造远程查询字符串 - 在
PRINT或错误日志里拼接未过滤的字段值(比如PRINT 'Failed for user: ' + @username) - 动态构建临时表名、索引名、分区名时漏掉
QUOTENAME()或白名单
只要字符串里混入未经白名单校验或 QUOTENAME() 处理的运行时变量,就构成注入风险——和是否用了 sp_executesql 无关。

















