sp_executesql能防注入,因为它强制参数与语句分离:用户输入必须通过@params声明和后续参数列表传入,sql引擎预先区分数据与代码;任何拼接进字符串的值(如'select * from t where x='''+@v+''')均无效,表名列名等结构部分须白名单校验后quotename()处理。

sp_executesql 为什么能防注入?
因为它强制参数化:所有用户输入必须走 @param 占位符,SQL 引擎在执行前就已把参数值和语句结构分开处理,不会把输入当代码解析。
常见错误是“伪参数化”——比如拼接字符串再传给 sp_executesql,例如:'SELECT * FROM users WHERE name = ''' + @name + ''''。这毫无防护作用,跟直接拼接 EXEC 一样危险。
关键点只有一个:参数值必须通过 @params 和后续参数列表传入,不能出现在 SQL 字符串里。
正确写法:三步缺一不可
调用 sp_executesql 必须同时满足:
- SQL 字符串中只出现
@param占位符(如@id、@keyword),不拼接任何变量值 -
@params参数明确声明每个参数的类型和方向(通常是nvarchar(50)而不是笼统的sql_variant) - 后续参数按顺序、类型严格匹配,比如
@id是int,就传整数,别传字符串再指望隐式转换
示例:
DECLARE @sql nvarchar(max) = N'SELECT * FROM orders WHERE status = @status AND amount > @min'; DECLARE @params nvarchar(max) = N'@status nvarchar(20), @min money'; EXEC sp_executesql @sql, @params, @status = N'shipped', @min = 100.0;
容易被忽略的坑:类型不匹配引发隐式转换
如果传入的参数类型和 @params 声明不一致,SQL Server 可能触发隐式转换,导致索引失效或意外结果,甚至绕过部分校验逻辑。
比如表字段 user_id 是 int,但你声明 @uid nvarchar(10) 并传入 '123',虽然能执行,但可能让查询无法使用索引;更严重的是,若业务层对 nvarchar 做了长度截断或编码处理,而数据库层又做了不同规则的转换,边界情况可能暴露漏洞。
建议:
- 参数类型尽量与目标列一致(
int就用int,datetime2就用datetime2) - 字符串参数统一用
nvarchar(不是varchar),避免字符集问题 - 长度宁可略宽,不要硬卡成
nvarchar(1)—— 除非真确定只会是单字符
动态列名/表名怎么办?
sp_executesql 无法参数化对象名(如表名、列名、排序字段)。这时候必须白名单校验 + QUOTENAME()。
错误做法:N'SELECT * FROM ' + @table_name
正确做法:
- 先查白名单:
IF @table_name NOT IN ('orders', 'users', 'products') THROW 50000, 'Invalid table', 1; - 再安全包裹:
N'SELECT * FROM ' + QUOTENAME(@table_name) - 列名同理,
QUOTENAME(@sort_col)+ 白名单比对(如IN ('created_at', 'status'))
记住:对象名不进参数列表,也不进 @params,它们属于 SQL 结构的一部分,必须在拼接前完成可信性验证。











