必须用sp_executesql而非EXEC(@sql),因后者将用户输入直接当代码执行,导致注入;前者通过编译期分离SQL结构与参数值实现安全,但需严格遵循三段式写法、精确类型声明及白名单校验表名列名。

必须用 sp_executesql,但光用它不等于安全——参数声明、类型精度、值校验三者缺一不可。
为什么不能只写 EXEC(@sql) 就完事
因为 EXEC(@sql) 是把整个字符串当代码执行,用户输入一旦混进字符串,就直接参与语法解析。比如传入 @name = N"'; DROP TABLE users; --",拼进去就是 SELECT * FROM customers WHERE name = ''; DROP TABLE users; --',删表就发生了。
sp_executesql 的安全前提是“分离”:SQL 文本是常量,参数是独立变量,引擎在编译阶段就确认了结构,运行时只替换值。但这个前提只在你严格按三段式写法时才成立。
-
@stmt必须是N'...'Unicode 字符串或NVARCHAR变量,VARCHAR会隐式转换失败或截断 -
@params字符串里必须显式声明每个参数的完整类型,包括长度:N'@id INT, @name NVARCHAR(50)',不能写NVARCHAR(MAX)或漏掉长度 - 参数传递必须带
@param = value形式,顺序错一位,类型和值就错位,可能把字符串当数字用
sp_executesql 的三段式写法不能省略任何一段
常见错误是把参数列表写成 N'@id INT' 却只传 @id,没写 @id = @id;或者把 @params 写成 '@id int'(缺 N 前缀),导致报错 Must declare the scalar variable "@id"。
正确写法只有这一种结构:
EXEC sp_executesql @stmt = N'SELECT * FROM SalesLT.Customer WHERE CustomerID = @id AND CompanyName LIKE @name', @params = N'@id INT, @name NVARCHAR(128)', @id = 123, @name = N'%Bike%';
-
@stmt和@params都必须加N前缀,否则非 Unicode 字符串会被截断或乱码 -
@params中的类型必须和实际变量完全一致,INT对INT,NVARCHAR(50)对NVARCHAR(50) - 传参必须显式写出
@id = 123,不能只写123,也不能靠位置匹配
表名、列名这些“语法成分”根本不能参数化
你无法写 N'SELECT * FROM @table' —— SQL Server 不允许参数化对象名。硬拼是唯一办法,但必须加白名单校验,QUOTENAME() 只是拼接工具,不是校验手段。
错误做法:
DECLARE @table SYSNAME = N'Customer'; SET @sql = N'SELECT * FROM ' + QUOTENAME(@table); -- ❌ 没校验,QUOTENAME 不防注入
正确做法:
IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = @table AND schema_id = SCHEMA_ID('dbo'))
THROW 50000, 'Invalid table name', 1;
SET @sql = N'SELECT * FROM ' + QUOTENAME(@table); -- ✅ 先查系统视图,再拼- 白名单优先于
QUOTENAME():先确认存在且合法,再用QUOTENAME()包裹 - 排序字段、分组字段、动态列别名等同理,必须进硬编码白名单或系统视图验证
- 禁止对用户输入做
REPLACE(@input, '''', '''''')后直接拼接,这防不住]; DROP TABLE x; --
最容易被忽略的隐性拼接点
很多漏洞不出现在主查询里,而在你没意识到的地方:比如用 CONTEXT_INFO 存用户 ID 后,在触发器里拼 SQL;或者用 OPENROWSET 构造远程查询字符串;甚至是在存储过程里调用另一个用 EXEC(@sql) 的子过程。
这些地方往往绕过 sp_executesql 的防护,也没有白名单校验。只要字符串里含用户可控内容,就等于开了后门。
真正安全的动态 SQL 不是“用了 sp_executesql”,而是每处拼接都回答清楚三个问题:这个值是否来自用户?它出现在语法位置还是数据位置?如果必须拼,有没有双重校验(存在性 + 内容规则)?

















