sp_executesql 是唯一支持参数绑定的防注入机制,必须替代 exec;表名列名等语法结构需白名单校验+quotename(),不可参数化;动态where应使用@p is null or col=@p模式;所有字符串拼接点须统一校验。

sp_executesql 必须代替 EXEC,否则等于裸奔
SQL Server 中 EXEC(@sql) 是注入高危路径——它把整个字符串当命令执行,参数值一旦混入,立刻被解析为语句。而 sp_executesql 才是唯一支持真正参数绑定的机制,值不参与语法解析,天然隔离数据与代码。
- 错误写法:
SET @sql = 'SELECT * FROM users WHERE id = ' + @id; EXEC(@sql);—— 传入'1; DROP TABLE logs; --'就直接删表 - 正确写法分三段:
@sql字符串里只出现占位符(如N'SELECT * FROM users WHERE status = @status'),第二参数声明类型(如N'@status TINYINT'),第三参数绑定值(如@status = 1) - 漏掉任何一段都失效:省略类型声明、不带
@param = value格式、或在@sql里拼接变量,等于没防
表名/列名不能参数化,必须白名单 + QUOTENAME()
sp_executesql 只能绑定值,无法绑定表名、列名、排序方向这些语法结构。所以它们必须提前验证合法性,不能靠 QUOTENAME() 单独兜底。
- 错误做法:
SET @sql = 'SELECT * FROM ' + QUOTENAME(@table_name)——QUOTENAME()对'users; DROP TABLE logs--'会生成[users; DROP TABLE logs--],仍是合法标识符 - 优先查
sys.tables或sys.columns:IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = @table_name AND schema_id = SCHEMA_ID('dbo')) RAISERROR(...) - 避免只用
OBJECT_ID(@table_name)判断——它不校验 schema,'malicious;--'可能被截断后误判存在 - 排序字段等有限枚举场景,直接白名单:
CASE WHEN @sort_col IN ('created_at', 'status') THEN @sort_col ELSE 'id' END
WHERE 条件别拼字符串,用 IS NULL 逻辑替代
动态拼 AND status = ''' + @status + '''' 是典型翻车点:空值判断漏掉、类型转换异常、执行计划无法复用,三者叠加等于敞开注入口。
- 安全写法:
WHERE (@status IS NULL OR status = @status)—— SQL Server 优化器能识别这种模式并生成合理计划 - 字符串类参数务必加长度限制和内容过滤:
IF LEN(@name) > 50 OR @name LIKE '%[^a-zA-Z0-9_]%' THROW 50000, 'Invalid name format', 1 - 数字类参数加硬范围检查:
IF @user_id 999999 RETURN - 禁止在过程里做
CAST(@input AS NVARCHAR)或CONVERT—— 转换失败报错,成功转换后可能已失真
最容易被忽略的隐性拼接点
这些地方往往藏得深、审查看不出,但一出问题就是高危漏洞。
- 用
CONTEXT_INFO存用户 ID 后在触发器里拼 SQL - 用
OPENROWSET构造远程查询字符串 - 日志记录逻辑里把参数拼进 INSERT 语句(比如
'User ' + @username + ' accessed ...') - 临时表建表语句中拼列定义:
CREATE TABLE #tmp (col_' + @suffix + ' INT)—— 这里@suffix必须同样走白名单校验
所有拼接点都得按同一套规则过一遍:白名单 or 系统视图校验 → QUOTENAME() 或 quote_ident() 封装 → 不进 @sql 字符串体,只进占位符位置。










