存储过程本身不防SQL注入,关键在写法:必须用sp_executesql代替EXEC实现参数化,严格声明参数类型与长度,动态对象名须白名单校验,所有用户输入需强类型声明和范围检查。

存储过程本身不防注入,关键在怎么写——只要拼字符串,哪怕在数据库里执行,照样被注入。
必须用 sp_executesql 代替 EXEC
SQL Server 的 EXEC 把整个字符串当命令执行,参数没隔离;sp_executesql 才支持真正的参数化,数据和代码在编译阶段就分开。
-
EXEC('SELECT * FROM users WHERE id = ' + @id)是高危写法,传入@id = '1; DROP TABLE users; --'就会执行删表 - 正确写法:先定义 SQL 字符串不含值,再用参数列表传参
DECLARE @sql NVARCHAR(MAX) = N'SELECT * FROM users WHERE status = @status';EXEC sp_executesql @sql, N'@status TINYINT', @status = 1; - 第二个参数(类型声明)不能省:
N'@status TINYINT'要写全,不能只写N'@status'或漏掉长度 - 参数名必须一致:声明是
@status,调用就得写@status = 1,不是直接传1
动态对象名(表名、列名、排序字段)不能参数化,必须白名单校验
SQL Server 不允许把表名、列名当参数传,硬拼就是风险点。QUOTENAME() 只防单引号,挡不住 ]; DROP TABLE x; -- 这类结尾注入。
- 错误做法:
SET @sql = 'SELECT * FROM ' + QUOTENAME(@table_name);—— 看似安全,实则校验缺失 - 正确做法:查系统视图确认存在且合法
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) THROW 50000, 'Invalid table name', 1; - 排序字段等也得走白名单:
IF @sort_col NOT IN ('created_at', 'status', 'name') THROW 50000, 'Invalid sort column', 1; - 别用
OBJECT_ID(@table_name)单独判断——它不校验 schema,恶意输入可能被截断后误判
存储过程参数声明必须窄、强、带校验
参数类型太宽或没约束,等于给绕过留门。比如声明 @user_id SQL_VARIANT 或 @name NVARCHAR(MAX),后面再怎么参数化都补不回漏洞。
- 数字类参数用具体类型:
@user_id INT,别用SQL_VARIANT或NVARCHAR - 加硬约束:
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、临时表、OPENROWSET
很多人只盯着 EXEC 和 sp_executesql,却忽略其他执行上下文。这些地方一样会因未过滤用户输入而中招。
-
SET CONTEXT_INFO存用户 ID 后,在触发器里拼 SQL?同样要参数化或白名单 - 用
INSERT INTO #temp EXEC('...')动态查数据?里面的字符串仍是拼接风险点 -
OPENROWSET构造远程查询时传入用户控制的服务器名或库名?必须提前校验是否在白名单内 - 所有用户可控内容进动态执行上下文前,都要走一遍类型强转 + 长度检查 + 白名单或系统视图确认
最常被忽略的一点:哪怕用了 sp_executesql,如果上游传参不规范(比如应用层把字符串当数字传),SQL Server 隐式转换后仍可能让攻击生效。防御必须从前端输入、应用层绑定、到存储过程声明层层收紧,不能只靠某一层“看起来安全”。

















