必须用sp_executesql防注入,因其支持参数化隔离数据与SQL结构;动态对象名需白名单校验;参数类型须窄且强校验;隐性拼接点同样危险。

必须用 sp_executesql,不能用 EXEC
存储过程本身不防注入,关键在动态SQL怎么执行。EXEC 把整个字符串当命令运行,用户输入一旦混进去,就直接变成可执行代码;sp_executesql 才真正支持参数化,数据和SQL结构在编译阶段就隔离。
常见错误:EXEC('SELECT * FROM users WHERE id = ' + @id) —— 传入 @id = '1; DROP TABLE users; --' 就会删表。
- 正确写法分三段:SQL模板(不含值)、参数类型声明、参数值绑定
-
@sql里只写N'SELECT * FROM users WHERE status = @status',所有变量用@param占位 - 第二个参数必须显式写全类型,比如
N'@status TINYINT',不能省略长度或只写N'@status' - 第三个参数必须带前缀,写成
@status = 1,不是直接传1或变量名
动态对象名(表名、列名、排序字段)必须白名单校验
SQL Server 不允许把表名、列名当参数传,硬拼就是高危点。QUOTENAME() 只防单引号,挡不住 ]; DROP TABLE x; -- 这类结尾注入。
错误做法:SET @sql = 'SELECT * FROM ' + QUOTENAME(@table_name) —— 没校验合法性,等于开门揖盗。
- 查系统视图确认存在且归属预期 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) - 排序字段等走硬编码白名单:
IF @sort_col NOT IN ('created_at', 'status', 'name') THROW 50000, 'Invalid sort column', 1 - 别用
OBJECT_ID(@table_name)单独判断——它不校验 schema,恶意输入可能被截断后误判
参数声明要窄、强、带校验
用了 sp_executesql 不代表安全了。参数类型太宽或没约束,等于给绕过留门。
例如声明 @user_id SQL_VARIANT 或 @name NVARCHAR(MAX),后面再怎么参数化都补不回漏洞。
- 数字类用具体类型:
@user_id INT,开头加硬范围检查:IF @user_id 999999 RETURN - 字符串立即检查长度和内容:
IF LEN(@name) = 0 OR LEN(@name) > 50 OR @name NOT LIKE '[a-zA-Z0-9_]%' RETURN - 禁止在过程里做
CAST(@input AS NVARCHAR)或CONVERT—— 转换失败报错,成功转换后可能已失真
别漏掉隐性拼接点
最容易被忽略的地方往往最危险:比如用 CONTEXT_INFO 存用户 ID 后在触发器里拼 SQL,或者用 OPENROWSET 构造远程查询字符串,甚至在日志记录逻辑里把参数转成字符串再拼进 INSERT 语句。
这些地方不会出现在主 SQL 模板里,但只要最终生成的字符串含用户输入且未经参数化,就是注入入口。
检查点要覆盖所有 SQL 字符串构造位置,不只是 sp_executesql 前那几行。











