必须用sp_executesql,不能用exec(@sql);因sp_executesql在编译阶段分离代码与数据,参数值不参与语法解析,可彻底防注入,而exec直接执行拼接字符串,无隔离层,用户输入混入即执行恶意代码。

必须用 sp_executesql,且参数定义和传值必须严格分离——否则和拼字符串没区别。
为什么 EXEC(@sql) 不能碰
它把整个字符串当命令执行,用户输入一旦混入,就直接进执行队列。比如 @sql = 'SELECT * FROM users WHERE id = ' + @user_id,传入 @user_id = '1; DROP TABLE users; --',结果就是删表。这不是“可能被绕过”,是必然执行。
-
EXEC不支持参数绑定,所有值都得靠字符串拼接,天然高危 - 执行计划无法复用,每次都要重新编译,性能差
-
QUOTENAME()或REPLACE(@input, '''', '''''')都拦不住分号、括号、注释符等组合攻击
sp_executesql 的三个硬性写法要求
安全不是加个 sp_executesql 就完事,它有三道不可跳过的坎:
-
@stmt字符串里只允许出现占位符(如@name),绝对不能出现任何用户输入的字面量,包括拼接进来的变量值 -
@params必须显式声明类型和长度,例如N'@status TINYINT',不能写N'@status SQL_VARIANT'或漏掉长度(NVARCHAR不带长度会默认为 1) - 传参必须用
@param = @value形式,不能省略参数名,也不能把变量值直接塞进@params字符串里(比如N'@p nvarchar(50) = ''' + @input + ''' '是典型错误)
表名/列名等动态结构怎么处理
这些语法成分无法参数化,sp_executesql 对它们完全无效。硬拼就是开后门,必须前置校验:
- 查系统视图确认存在且归属预期 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) - 白名单映射比
QUOTENAME()更可靠:CASE @sort_col WHEN 'created_at' THEN 'created_at' WHEN 'status' THEN 'status' ELSE THROW 50000, 'Invalid sort column', 1 END - 禁止用
OBJECT_ID(@table_name)单独判断——它不校验 schema,恶意输入如'users; DROP TABLE x--'可能被截断后误判成功
容易被忽略的隐性风险点
很多人盯着 sp_executesql 和 EXEC,却栽在更隐蔽的地方:
-
CONTEXT_INFO:在触发器或函数里从上下文取值再拼 SQL,等于二次拼接 - 临时表字段名或索引名:如果用变量构造
CREATE TABLE #t (@col_name VARCHAR(10)),语法不合法但类似逻辑常出现在动态建表脚本中 -
OPENROWSET连接字符串:远程查询若含用户输入,一样可注入,且绕过常规审计 - 参数类型太宽泛:声明
@id SQL_VARIANT或@name NVARCHAR(MAX),等于放弃第一道类型过滤,上游传'1 OR 1=1'时 SQL Server 可能隐式转成 1,掩盖问题而非解决










