必须用 sp_executesql 防注入:参数化查询分离代码与数据;动态对象名须白名单校验+quotename();参数需严格类型声明与输入校验;禁用exec拼接、高权限执行及隐式拼接点。

必须用 sp_executesql,不能用 EXEC
直接拼接字符串再调用 EXEC 就等于把用户输入当代码执行。哪怕加了 REPLACE(@input, '''', ''''''),攻击者也能用 1; DROP TABLE users; -- 绕过。SQL Server 的 sp_executesql 在编译阶段就分离代码与数据,参数值不参与语法解析——这是防注入不可妥协的底线。
常见错误写法:SET @sql = 'SELECT * FROM orders WHERE status = ''' + @status + ''''; EXEC(@sql);正确写法必须拆成三段:SQL 字符串、参数定义字符串、参数值列表。
-
@sql里只放静态结构,所有变量位置用@param_name占位 - 第二个参数(如
N'@status TINYINT, @since DATETIME')必须显式声明类型和长度,不能只写NVARCHAR - 第三个及之后的参数要按定义顺序传值,且必须带
@param_name =前缀,不能只传变量名或裸值
动态对象名(表名/列名)怎么处理
表名、列名、ORDER BY 字段这些语法成分无法参数化,硬拼就是高危操作。不能靠 REPLACE 或正则“过滤”来“消毒”,更不能自己加单引号包裹。
唯一安全做法是白名单校验或系统视图确认:
- 硬编码白名单:如
IF @sort_col NOT IN ('created_at', 'status', 'amount') RETURN - 查系统视图:用
EXISTS (SELECT 1 FROM sys.columns WHERE name = @col AND object_id = OBJECT_ID(@table))确认存在且归属预期 schema - 拼接前必须套
QUOTENAME(@name),仅用于对象名,永远不用于数据值
参数声明和输入校验不能省
用了 sp_executesql 不代表万事大吉。如果参数类型太宽(比如 @id SQL_VARIANT 或 @name NVARCHAR(MAX)),或没做长度/范围检查,攻击面依然存在。
例如传入 @id = '1 OR 1=1',SQL Server 可能隐式转成 1 掩盖问题;但若上游应用本应传整数却传了字符串,说明约束失效。
- 数字类参数声明为具体类型(
INT,TINYINT),并在开头加硬约束:IF @user_id 999999 RETURN - 字符串类参数立即检查长度:
IF LEN(@name) = 0 OR LEN(@name) > 50 RETURN - 避免在存储过程里对输入做
CAST/CONVERT—— 转换失败报错,转换成功可能已失真
别漏掉隐性拼接点
容易被忽略的地方往往最危险:比如用 CONTEXT_INFO 存用户 ID 后在触发器里拼 SQL,或者用 OPENROWSET 构造远程查询字符串,甚至临时表名动态生成时没走 QUOTENAME()。
这些地方和主逻辑一样,只要涉及字符串拼接+用户可控输入,就构成注入路径。
- 检查所有
SET @sql = ...出现的位置,包括子过程、触发器、作业脚本 - 禁用
EXECUTE AS OWNER或高权限上下文执行存储过程,账号应仅具备最小必要权限(如只读 + 特定表 INSERT) - 即使存储过程本身写得再严,执行账号有
db_owner权限,一次成功的注入仍可能导致跨库破坏










