存储过程本身不能自动防SQL注入,关键在于使用sp_executesql配合参数化查询;其通过分离SQL模板与参数值,避免用户输入参与语法解析,从而阻断注入。

存储过程本身不能自动防SQL注入,关键在怎么写。直接拼接字符串再执行,哪怕在存储过程里,照样中招;而用 sp_executesql 配合参数化方式,才能切断用户输入与SQL逻辑的耦合。
为什么 sp_executesql 比 exec 更安全
exec 只接受一个字符串,所有内容(包括用户传入值)都混在 SQL 文本里;sp_executesql 则明确分离「SQL模板」和「参数值」,数据库引擎在执行前就已固定语句结构,参数只作为数据传入,不参与语法解析。
常见错误现象:exec('SELECT * FROM users WHERE name = ''' + @name + '''') —— 一旦 @name 是 O'Brien'; DROP TABLE users--,就完蛋。
- 使用场景:动态表名/列名无法参数化时,必须拼接部分 SQL,但过滤逻辑、WHERE 条件、ORDER BY 等应尽量交由参数控制
- 性能影响:首次执行会生成执行计划并缓存,后续相同模板+不同参数可复用,比
exec更高效 - 兼容性:SQL Server 2005+ 全支持,Azure SQL 也完全兼容
sp_executesql 的正确调用格式
必须同时提供三部分:SQL 字符串、参数定义字符串、参数值列表。漏掉任意一环,参数就失效,退化为字符串拼接。
正确示例:
DECLARE @sql NVARCHAR(MAX) = N'SELECT * FROM users WHERE status = @status AND created_date > @since'; DECLARE @params NVARCHAR(500) = N'@status TINYINT, @since DATETIME'; EXEC sp_executesql @sql, @params, @status = 1, @since = '2025-01-01';
-
@sql中的变量名(如@status)必须和@params中声明的类型一致,且和最后传入的参数名匹配 - 不要写成
EXEC sp_executesql @sql, @params, @status = @status—— 这种写法容易混淆,且在嵌套调用时易出错 - 字符串参数值要加引号,但传入
sp_executesql时不用手动加,引擎自动处理
哪些地方仍可能被绕过
即使用了 sp_executesql,如果把用户输入直接拼进 @sql 字符串(比如表名、排序字段),依然危险。这类动态部分无法参数化,必须白名单校验或严格过滤。
典型风险点:
- 拼接表名:
@sql = N'SELECT * FROM ' + @table_name + N' WHERE id = @id'——@table_name必须通过OBJECT_ID(@table_name, 'U')校验是否存在且为用户表 - 拼接 ORDER BY 字段:
ORDER BY ' + @sort_col—— 应限定为预设字段名列表,如CASE @sort_col WHEN 'name' THEN name WHEN 'created' THEN created END - 拼接 LIKE 模式:
WHERE name LIKE '%' + @keyword + '%'是安全的,但若写成'%' + @keyword + '%' + @wildcard,而@wildcard来自用户,则可能引入额外通配符或转义失控
最常被忽略的一点:参数类型声明必须精确。用 @id INT 却传入超长字符串,SQL Server 可能隐式截断或报错,但更糟的是,有人会故意传入畸形值试探边界——类型不匹配反而暴露逻辑漏洞。所以声明参数时,宁可窄不可宽,VARCHAR(50) 就别写成 NVARCHAR(MAX)。

















