如何通过参数化过程在存储过程中避免EXEC命令的SQL注入风险?
- 内容介绍
- 文章标签
- 相关推荐
本文共计868个文字,预计阅读时间需要4分钟。
由于EXEC(或sp_executesql)执行的SQL是动态字符串,如果用户输入直接拼接的字符串,攻击者可能通过注入恶意代码。例如,输入@@table_name='users; DROP TABLE orders; --',拼接成EXEC('SELECT * FROM ' + @@table_name),就会实际执行删除表的操作。
根本问题不在 EXEC 本身,而在于字符串拼接环节丢失了参数边界。SQL Server 不会对拼接后的字符串做参数校验,所有内容都被当作文本原样执行。
必须用 sp_executesql + 参数占位符
sp_executesql 是唯一支持真正参数化的动态执行方式,它把 SQL 模板和参数值分开传递,SQL Server 内部会严格区分代码和数据。
- 模板中用
@param_name占位,不拼字符串 - 参数定义列表(第二个参数)必须显式声明每个参数的类型和长度,比如
N'@id INT, @name NVARCHAR(50)' - 参数值列表(第三个及后续参数)只传变量名,不加引号、不拼接
示例:
DECLARE @sql NVARCHAR(MAX) = N'SELECT * FROM users WHERE status = @status AND age > @min_age'; DECLARE @params NVARCHAR(MAX) = N'@status TINYINT, @min_age INT'; EXEC sp_executesql @sql, @params, @status = 1, @min_age = 18;
对象名(表名/列名)不能参数化,必须白名单校验
SQL Server 不允许把表名、列名、排序字段等作为 sp_executesql 的参数,否则报错 Must declare the scalar variable "@table"。这类标识符只能拼接,但必须严格过滤。
- 优先用白名单:比如只允许
@sort_col IN ('created_at', 'name', 'score'),硬编码比正则更可靠 - 必须用
QUOTENAME()包裹,它会自动加方括号并转义特殊字符,如QUOTENAME(@col) → [user.name],但注意它不防逻辑注入(比如合法列名后跟恶意条件) - 绝对不要用
REPLACE(@input, '''', '''''')手动转义,这无法覆盖所有绕过方式
安全拼接示例:
DECLARE @table SYSNAME = 'orders'; DECLARE @col SYSNAME = 'total'; DECLARE @sql NVARCHAR(MAX) = N'SELECT ' + QUOTENAME(@col) + N' FROM ' + QUOTENAME(@table); EXEC sp_executesql @sql;
EXEC(@string) 和 EXEC sp_executesql 的关键区别
两者语法相似,但底层机制完全不同:EXEC(@string) 是纯文本解释执行,没有任何参数上下文;sp_executesql 则构建了独立的执行环境,支持参数缓存和类型绑定。
- 性能:用
sp_executesql的相同模板 SQL 可复用执行计划,EXEC(@string)每次都编译新计划 - 调试:参数值在 Profiler 或 Extended Events 中可见,而拼接字符串里的值不可见
- 兼容性:SQL Server 2005+ 都支持,无版本顾虑
- 错误定位:参数类型不匹配时,
sp_executesql报错明确指向参数定义,拼接出错则报“语法错误”,难以溯源
一句话:只要涉及变量代入,就不用 EXEC(@string),没有例外。
最易被忽略的是对象名校验——很多人以为用了 sp_executesql 就万事大吉,结果在表名拼接处栽跟头。QUOTENAME() 是底线,白名单才是保险栓。
本文共计868个文字,预计阅读时间需要4分钟。
由于EXEC(或sp_executesql)执行的SQL是动态字符串,如果用户输入直接拼接的字符串,攻击者可能通过注入恶意代码。例如,输入@@table_name='users; DROP TABLE orders; --',拼接成EXEC('SELECT * FROM ' + @@table_name),就会实际执行删除表的操作。
根本问题不在 EXEC 本身,而在于字符串拼接环节丢失了参数边界。SQL Server 不会对拼接后的字符串做参数校验,所有内容都被当作文本原样执行。
必须用 sp_executesql + 参数占位符
sp_executesql 是唯一支持真正参数化的动态执行方式,它把 SQL 模板和参数值分开传递,SQL Server 内部会严格区分代码和数据。
- 模板中用
@param_name占位,不拼字符串 - 参数定义列表(第二个参数)必须显式声明每个参数的类型和长度,比如
N'@id INT, @name NVARCHAR(50)' - 参数值列表(第三个及后续参数)只传变量名,不加引号、不拼接
示例:
DECLARE @sql NVARCHAR(MAX) = N'SELECT * FROM users WHERE status = @status AND age > @min_age'; DECLARE @params NVARCHAR(MAX) = N'@status TINYINT, @min_age INT'; EXEC sp_executesql @sql, @params, @status = 1, @min_age = 18;
对象名(表名/列名)不能参数化,必须白名单校验
SQL Server 不允许把表名、列名、排序字段等作为 sp_executesql 的参数,否则报错 Must declare the scalar variable "@table"。这类标识符只能拼接,但必须严格过滤。
- 优先用白名单:比如只允许
@sort_col IN ('created_at', 'name', 'score'),硬编码比正则更可靠 - 必须用
QUOTENAME()包裹,它会自动加方括号并转义特殊字符,如QUOTENAME(@col) → [user.name],但注意它不防逻辑注入(比如合法列名后跟恶意条件) - 绝对不要用
REPLACE(@input, '''', '''''')手动转义,这无法覆盖所有绕过方式
安全拼接示例:
DECLARE @table SYSNAME = 'orders'; DECLARE @col SYSNAME = 'total'; DECLARE @sql NVARCHAR(MAX) = N'SELECT ' + QUOTENAME(@col) + N' FROM ' + QUOTENAME(@table); EXEC sp_executesql @sql;
EXEC(@string) 和 EXEC sp_executesql 的关键区别
两者语法相似,但底层机制完全不同:EXEC(@string) 是纯文本解释执行,没有任何参数上下文;sp_executesql 则构建了独立的执行环境,支持参数缓存和类型绑定。
- 性能:用
sp_executesql的相同模板 SQL 可复用执行计划,EXEC(@string)每次都编译新计划 - 调试:参数值在 Profiler 或 Extended Events 中可见,而拼接字符串里的值不可见
- 兼容性:SQL Server 2005+ 都支持,无版本顾虑
- 错误定位:参数类型不匹配时,
sp_executesql报错明确指向参数定义,拼接出错则报“语法错误”,难以溯源
一句话:只要涉及变量代入,就不用 EXEC(@string),没有例外。
最易被忽略的是对象名校验——很多人以为用了 sp_executesql 就万事大吉,结果在表名拼接处栽跟头。QUOTENAME() 是底线,白名单才是保险栓。

