sp_executesql是防SQL注入的硬性底线,因其参数在语法解析后绑定、被当纯数据处理;EXEC(@sql)则直接执行拼接字符串,使用户输入成为可执行代码。

sp_executesql 是 SQL Server 中唯一能真正隔离数据与代码的动态执行机制。用它代替 EXEC(@sql),不是“更推荐”,而是防注入的硬性底线——不这么做,就等于把用户输入直接喂给查询引擎。
为什么 sp_executesql 能防注入,而 EXEC 不能
根本区别在于参数绑定时机:sp_executesql 的参数值在语法解析之后、执行之前才代入,SQL Server 把它们当纯数据处理,不会参与语句结构构建;EXEC(@sql) 执行的是完整字符串,只要 @sql 里混了用户输入,就等同于把攻击载荷写进源码里。
常见错误现象:
- 前端传
admin' OR 1=1 --,拼进@sql = 'SELECT * FROM users WHERE name = ''' + @input + '''',结果查出全表 - 用
REPLACE(@input, '''', '''''')“转义”,但攻击者改用CHAR(0x27)或大小写注释--绕过 - 误以为加了单引号就安全,却忽略分号、括号、Unicode 控制字符等执行分隔符
sp_executesql 的正确调用姿势
关键不是“用了没”,而是参数是否全程脱离字符串拼接。下面这种写法仍是危险的:
DECLARE @sql NVARCHAR(MAX) = N'SELECT * FROM users WHERE id = ' + CAST(@id AS NVARCHAR(10)); EXEC sp_executesql @sql;
因为 @id 已在字符串拼接阶段被转成文本,没走参数通道。正确方式必须满足三点:
-
@sql字符串里只出现占位符(如@p1),不出现任何变量值 -
@params参数声明必须显式、完整,例如N'@p1 INT, @p2 NVARCHAR(50)',不能漏类型或长度 - 实际参数值通过第三个及后续参数传入,顺序和声明严格对应,例如
sp_executesql @sql, @params, @p1 = 123, @p2 = N'john'
哪些地方不能参数化?怎么安全处理
表名、列名、排序字段、数据库名这些属于 SQL 语法结构,sp_executesql 不支持参数化。硬拼进去就是漏洞入口,必须换思路:
- 优先拒绝动态对象名:能用静态 SQL 就不用动态,比如
WHERE (@status IS NULL OR status = @status)替代拼AND status = @status - 非用不可时,先白名单校验再
QUOTENAME():查sys.tables确认表真实存在且归属预期 schema,再用QUOTENAME(@table_name)包裹,生成[users]这类带方括号的安全标识符 - 禁用
REPLACE(@name, '''', '''''')这类手动“消毒”:它对 Unicode 异常字符、嵌套注释完全无效,且QUOTENAME()本身也不保万无一失,必须前置校验
容易被忽略的隐性拼接点
很多人只盯着 EXEC 和 sp_executesql,却在其他地方埋雷:
-
CONTEXT_INFO:在触发器或存储过程中读取并拼进 SQL,一样会中招 -
OPENROWSET或OPENDATASOURCE:构造连接字符串时若含用户输入,未过滤即执行 - 临时表名或列名动态生成:比如
SELECT * INTO #tmp_' + @suffix,@suffix必须白名单约束 - 权限配置本身:哪怕存储过程写得再严,执行账号是
db_owner,一次成功的注入就能跨库删表
真正安全的存储过程,不是靠某一行代码兜底,而是从调用层 SqlParameter、到过程内静态 SQL、再到极少数动态分支的 QUOTENAME + 白名单 + 权限最小化,整条链路没有一处把用户输入当代码使。

















