彻底消除动态sql拼接是防御sql注入的底线,因参数化仅保护值而非结构;表名列名等元数据须用quotename()或白名单校验,运行时值必须通过sp_executesql严格参数化,禁用exec(@sql)。

直接说结论:不能靠“拼接得更小心”来防注入,必须把结构和数据彻底分开——元数据走 QUOTENAME() 或白名单校验,运行时值一律走 sp_executesql 参数化,EXEC 一概禁用。
为什么 EXEC(@sql) + 字符串拼接等于开门揖盗
只要用户输入进了 @sql 字符串,哪怕只多一个单引号、一个分号或一个注释符,SQL Server 就会把它当代码执行。比如传入 @sort = 'name asc; DROP TABLE logs; --',拼成 'ORDER BY ' + @sort 后实际执行的就是完整恶意语句。
-
REPLACE(@input, '''', '''''')挡不住]; DROP TABLE x; --这类绕过 -
CAST/CONVERT转类型不解决根本问题,拼接发生在类型转换之前 - 前端做了 JS 校验?后端照样能绕过,数据库才是最后一道防线
表名、列名、排序字段这些“结构”怎么处理
它们不是值,不能参数化,但也不能裸拼。安全路径只有两条,且必须二选一:
- 用
QUOTENAME()包裹,且必须先拆再包:QUOTENAME(@schema) + '.' + QUOTENAME(@table),而不是QUOTENAME(@schema + '.' + @table) - 更推荐硬白名单校验:
IF @table NOT IN ('users', 'orders', 'products') THROW 50000, 'Invalid table name', 1; - 排序字段(
@order_by)必须映射到固定列:CASE @order_by WHEN 'name' THEN 'user_name' WHEN 'id' THEN 'user_id' END,再拼进 SQL - 排序方向(
ASC/DESC)同样不能直拼,需用CASE映射或白名单校验
sp_executesql 的参数化到底怎么写才有效
声明错、传参错、拼接错,三者任一出问题,参数化就形同虚设。关键细节全在参数声明和调用里:
-
@params字符串必须显式写全类型和长度:N'@status TINYINT, @since DATETIME2(3), @name NVARCHAR(50)',不能只写@status -
@sql字符串里只能出现参数占位符,绝不能出现CAST(@status AS VARCHAR)或字符串拼接 - 传参必须用
@param_name = @variable形式:@status = @input_status,不能只写@input_status - 所有存储过程输入参数本身也应强类型、窄范围,比如用
@id INT而非@id SQL_VARIANT
最容易被忽略的是:动态 SQL 的安全性不取决于你用了多少层转义或包裹,而取决于你是否从第一行就划清了“哪里是模板、哪里是数据”的边界。一旦开始拼接值,就已经输了。










