动态SQL必须禁用EXEC,一律改用sp_executesql并严格参数化;用户输入绝不可拼入SQL字符串,仅元数据(如表名)可用QUOTENAME()或白名单校验;参数声明与传参须类型、数量、顺序完全匹配。

动态拼接 EXEC 或 sp_executesql 时,字符串直接拼接变量就是高危操作
只要把用户输入(比如 @table_name、@where_cond)不经验证就插进 SQL 字符串里,再用 EXEC(@sql) 执行,就等于把数据库的执行权交给了攻击者。常见现象是:传入 @name = 'admin''; DROP TABLE users; --',结果语句变成 SELECT * FROM users WHERE name = 'admin'; DROP TABLE users; --'。
根本原则是:**用户输入绝不能出现在最终拼出的 SQL 字符串中,必须通过参数化方式传入**。
-
EXEC(@sql)不支持参数化,一律禁用;必须改用sp_executesql - 所有运行时值(如查询条件、排序字段、分页偏移)都得作为
@param参数传入,不能拼进@sql字符串 - 只有极少数元数据(如表名、列名、排序方向)才可能需要拼接,但必须严格白名单校验或使用
QUOTENAME()
QUOTENAME() 是唯一安全处理对象名的方式
当确实需要动态指定表名、列名或架构名时(例如多租户按客户分表),不能靠简单替换或正则过滤,必须用 QUOTENAME()。它会自动加方括号并转义内部特殊字符,比如 QUOTENAME('user]; DROP TABLE x; --') 返回 [user]] DROP TABLE x; --],让后续解析失败而非执行。
注意:QUOTENAME() 只适用于标识符(identifier),不适用于值(value)。值必须走 sp_executesql 参数。
- 只对已知有限集合的对象名做白名单检查更稳妥,例如:
IF @table_name NOT IN ('orders', 'users', 'logs') THROW 50000, 'Invalid table name', 1; -
QUOTENAME(@schema)和QUOTENAME(@table)要分开调用,不要拼成QUOTENAME(@schema + '.' + @table)—— 这会绕过校验 - 排序字段(如
@order_by)若需动态,只能限定为几个合法列名,再映射到实际列,不能直接拼接
sp_executesql 的参数声明和传参必须严格匹配
很多人写了 sp_executesql @sql, N'@id INT', @id = @input_id,却在 @sql 里写成 'WHERE id = ' + CAST(@input_id AS VARCHAR) —— 这样参数形同虚设,还是拼接。
正确做法是:SQL 字符串里所有变量位置都用 @param 占位符,且声明的类型、数量、顺序必须与实际传入一致。
- 示例安全写法:
DECLARE @sql NVARCHAR(MAX) = N'SELECT * FROM users WHERE status = @status AND created_at > @since'; EXEC sp_executesql @sql, N'@status TINYINT, @since DATETIME2', @status = @input_status, @since = @input_since;
- 类型要精确:用
INT就别传VARCHAR,否则隐式转换可能绕过长度限制或触发截断漏洞 - 不要省略参数名映射(即
@status = @input_status),避免顺序错乱导致值错位
业务逻辑层能拦截的,就别推给存储过程
存储过程不是防火墙。很多注入风险其实源于上游没做基础校验:比如前端传来的 @action 值本该是 'insert' 或 'update',结果收到 'insert; EXEC xp_cmdshell ''dir'';' —— 这种非预期值,应在应用层或存储过程开头就用 IF NOT EXISTS (SELECT 1 FROM ...) 或枚举判断拦截。
- 对枚举类参数(如状态、类型、操作码),优先用
CHECK CONSTRAINT或IN列表硬校验 - 对长度敏感的输入(如用户名、邮箱),在存储过程中先用
LEN()+LIKE检查是否含分号、注释符、括号等危险模式(仅作兜底,不能替代参数化) - 日志里记录原始输入参数(脱敏后),方便事后追溯异常调用来源
真正难的不是写对那几行 sp_executesql,而是判断哪些地方“看起来必须拼接”,其实可以通过重构规避——比如把按客户分表改成统一表+tenant_id字段,就能消灭90%的 QUOTENAME() 使用场景。

















