使用 sp_executesql 实现参数化查询是防SQL注入的底线,仅对表名、列名等非参数部分拼接且须经白名单或系统视图校验,其他用户输入一律走参数占位符。

存储过程中用 sp_executesql 代替 EXEC 是底线
直接拼接字符串再 EXEC 执行,等于把参数当代码跑,哪怕加了 REPLACE(@input, '''', '''''') 也拦不住绕过。SQL Server 的 sp_executesql 支持真正的参数化,变量值不参与语法解析,这是防注入最有效的机制。
- 只在必须动态生成表名、列名、排序字段等**非参数部分**才拼接,且必须白名单校验(比如从
@sort_col IN ('created_at', 'status')中判断) -
@sql字符串里所有用户可控内容,只要不是白名单内的标识符,一律进参数占位符@p1、@p2 - 不要给
sp_executesql的参数列表传字符串拼接结果,例如N'@p1 nvarchar(50) = ''' + @user_input + ''' '—— 这又回到拼接老路
动态对象名(表/列)过滤只能靠白名单或系统视图校验
参数化查询对表名、列名、数据库名无效,因为它们属于 SQL 语法结构,不是数据值。硬编码白名单太死板,更稳妥的是查 sys.tables 或 sys.columns 确认存在且归属预期 schema。
- 校验表名:
IF NOT EXISTS (SELECT 1 FROM sys.tables t JOIN sys.schemas s ON t.schema_id = s.schema_id WHERE s.name = 'dbo' AND t.name = @table_name),再抛错 - 避免用
OBJECT_ID(@table_name)单独判断——它不校验 schema,'malicious_table; DROP TABLE users--'可能被截断后误判成功 - 别信
QUOTENAME()能防所有情况:它只转义标识符,对含括号或特殊 Unicode 的恶意输入可能失效,仍需前置校验
不要在存储过程里拼接 WHERE 条件字符串
常见错误是根据参数是否为空,动态拼 ' AND status = ''' + @status + ''''。这种写法一旦漏判空值或类型转换出错,就留出注入口,而且执行计划无法复用。
- 统一用静态 SQL +
IS NULL判断:例如WHERE (@status IS NULL OR status = @status),SQL Server 优化器能处理得比动态 SQL 更稳 - 如果真要跳过某些条件(比如全文索引场景),改用
OPTION (RECOMPILE)让每次编译适配实际参数,而不是拼字符串 - 注意
NULL和空字符串语义差异:前端传空字符串时,@status = ''不等于@status IS NULL,需按业务逻辑显式处理
警惕 CONTEXT_INFO、临时表、OPENROWSET 这些“隐性拼接点”
很多人只盯住 EXEC 和 sp_executesql,却忽略其他执行上下文。比如用 SET CONTEXT_INFO 存用户 ID 后在触发器里拼 SQL,或者用 OPENROWSET 构造远程查询字符串——这些地方照样会因未过滤而中招。
-
CONTEXT_INFO是二进制字段,取出来要CONVERT(nvarchar, CONTEXT_INFO()),但转换后若直接拼进动态 SQL,和普通变量没区别,该过滤还得过滤 - 临时表名如果由参数生成(如
#tmp_' + @user_id),必须走QUOTENAME(),且长度限制要检查,超长会被截断破坏转义 -
OPENROWSET的连接字符串和查询语句都是拼出来的,比普通动态 SQL 更危险,尽量避免;非要使用,所有变量必须经白名单或系统视图双重确认
真正难的不是写对那几行 sp_executesql,而是整条调用链上每个可能带入外部输入的地方,都得想清楚:它最终会不会变成 SQL 文本的一部分?只要答案是“会”,就必须走参数化或白名单,没有例外。










