sp_executesql本身不防sql注入,安全性完全取决于sql字符串是否拼接用户输入;参数化仅对显式声明并正确绑定的变量生效,表名、列名等动态对象必须白名单校验。

sp_executesql 本身不防注入,拼接字符串就等于开门放人
很多人以为只要写了 sp_executesql,SQL Server 就会自动把所有变量当参数处理——这是最危险的误解。实际上,sp_executesql 只是一个执行接口,它是否安全,完全取决于你传给它的第一个参数(SQL 字符串)是怎么构造的。如果这个字符串里混进了用户输入拼接的内容,那和直接用 EXEC 没区别。
- 危险写法:
DECLARE @sql NVARCHAR(MAX) = N'SELECT * FROM users WHERE name = ''' + @input + ''''; EXEC sp_executesql @sql;—— 单引号被绕过、注释被插入、语句被截断,全都能生效 - 关键错觉:看到
sp_executesql就放松警惕,没意识到真正起作用的是“不拼”,而不是“用了哪个函数” - 类型声明(如
N'@p NVARCHAR(50)')只约束参数格式,不拦截拼接逻辑;一旦字符串已成型,引擎照常解析执行
参数定义字符串写错,等于白搭
sp_executesql 的第二个参数是参数定义字符串,必须显式声明类型、长度和名称,且和后续传入的实际值严格对应。漏掉、写错、类型宽松,都会让参数化形同虚设。
- 漏定义:
EXEC sp_executesql N'SELECT * FROM t WHERE id = @id', @id = 123—— 缺少定义字符串,SQL Server 报错或隐式转为批处理,@id被当作变量而非参数 - 类型太宽:
N'@name SQL_VARIANT'或NVARCHAR(MAX)容易绕过长度校验,给 Unicode 同形字、嵌套注释留出空间 - 名称不一致:定义写
@user,调用却传@username = @input,参数绑定失败,@input可能退化为拼接源
表名、列名、排序方向这些根本不能参数化
SQL Server 不允许在 FROM、ORDER BY、GROUP BY 等位置使用参数占位符。试图用 @table_name 直接拼进 SQL 字符串,哪怕套了 QUOTENAME(),也挡不住 ]; DROP TABLE x; -- 这类结尾注入。
- 错误依赖:
SET @sql = 'SELECT * FROM ' + QUOTENAME(@table_name)——QUOTENAME只转义括号和中括号,对分号、破折号、Unicode 控制字符无感 - 正确路径:必须走白名单校验,比如
IF @sort_col NOT IN ('name', 'email', 'created_at') THROW 50000, 'Invalid sort column', 1; - 更稳妥做法:查系统视图确认对象存在且归属预期 schema,例如
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
输出参数和上下文变量也可能带毒
容易被忽略的是,存储过程的“输出通道”不是单向安全的。如果输出参数或返回结果集里包含未经清洗的原始用户输入(比如从日志表读出的 user_agent 字段),而上层应用又把它拼进另一条动态 SQL,就会形成二次注入链。
- 典型场景:触发器里读取
CONTEXT_INFO构造审计日志 SQL;或临时表字段含用户可控内容,再被OPENROWSET引用 - 盲点风险:这类拼接不在主逻辑里,静态扫描难发现,但攻击面一样真实
- 防御要点:所有跨上下文传递的数据,只要可能参与 SQL 构造,就必须重新校验、重绑定,不能默认“已经进过存储过程就可信”
真正卡住注入的,从来不是函数名或语法糖,而是你有没有在每一处字符串拼接前,问一句:“这个值,能不能放进 @p 占位符?如果不能,它是不是白名单里的合法标识符?”——其余都是障眼法。











