必须用sp_executesql代替exec实现参数化查询,严格声明参数类型与长度,对表名列名等动态部分采用白名单校验,并对输入参数做强类型声明和范围检查。

存储过程中用 sp_executesql 代替 EXEC 才安全
直接拼接字符串再执行,哪怕在存储过程里,照样被注入。SQL Server 的 EXEC 会把整个字符串当命令解析,参数没隔离——@sql = 'SELECT * FROM users WHERE id = ' + @id 这种写法,传入 @id = '1; DROP TABLE users; --' 就完蛋。
必须改用 sp_executesql,它支持真正的参数化查询,SQL 引擎会在编译阶段就区分代码和数据:
DECLARE @sql NVARCHAR(MAX) = N'SELECT * FROM users WHERE status = @status AND created_after = @since'; EXEC sp_executesql @sql, N'@status TINYINT, @since DATETIME', @status = 1, @since = '2024-01-01';
-
sp_executesql的第二个参数是参数定义字符串,必须显式声明类型和长度(比如NVARCHAR(50),不能只写NVARCHAR) - 第三个及之后的参数才是实际值,顺序和定义严格对应
- 别图省事把变量名和参数名搞混:定义里写
@status,调用时也得传@status = ...,不是传变量值本身 - 如果动态部分涉及表名或列名(无法参数化),必须走白名单校验,不能靠 REPLACE 或正则过滤
所有用户输入进存储过程前,先做类型强转和范围检查
即使用了 sp_executesql,如果参数本身是弱类型或未校验,攻击者仍可能绕过。比如把 @id 设为 INT 类型,但调用时传入 '1 OR 1=1',SQL Server 会隐式转成 1——看似安全,实则掩盖了上游传参不规范的问题。
- 存储过程参数声明必须用具体、窄的类型:
@user_id INT,而不是@user_id SQL_VARIANT或宽泛的NVARCHAR(MAX) - 对数字类参数,加
IF @user_id 999999 RETURN这类硬约束 - 对字符串类参数(如用户名),用
LEN(@name) > 0 AND LEN(@name) 控制内容范围 - 避免在存储过程里做
CAST或CONVERT转换用户输入——转换失败会报错,但成功转换后可能已失真
禁止在存储过程中拼接对象名(表名、列名、排序字段)
表名、列名、ORDER BY 字段这些语法成分,SQL Server 不允许用参数占位,硬拼就是高危操作。见过太多人写 SET @sql = 'SELECT * FROM ' + @table_name,再加一层 QUOTENAME(@table_name) 就以为万事大吉——但 QUOTENAME 只防单引号,防不了 ]; DROP TABLE x; -- 这种结尾注入。
- 真正安全的做法:用白名单映射。例如定义
CASE @sort_col WHEN 'name' THEN 'user_name' WHEN 'email' THEN 'contact_email' ELSE 'id' END - 如果必须支持任意列名,把合法列名预先查出来存在临时表或表值函数里,用
EXISTS (SELECT 1 FROM @allowed_cols WHERE col_name = @input)校验 -
QUOTENAME只能用于你完全可控的内部标识符(比如日志表按月分表的后缀),不能用于任何可能来自前端或配置的输入 - 排序方向(
ASC/DESC)同样不能拼,应转为BIT参数,在 CASE 中分支控制
从应用层调用存储过程时,禁用“通用执行”封装
有些 ORM 或自研 DAO 层喜欢抽象出一个 ExecuteStoredProcedure(string procName, Dictionary<string object> params)</string> 方法,看起来方便,实则埋雷:它往往把所有参数都当字符串塞进 SqlParameter,忽略类型精度,甚至自动把 null 转成 DBNull.Value 而不校验业务逻辑是否允许空值。
- .NET 中调用时,必须显式指定
SqlDbType和Size(如new SqlParameter("@name", SqlDbType.NVarChar, 50) { Value = userName }) - 禁止把用户原始 HTTP 参数(如
Request.Query["id"])不加解析直接传给存储过程——先转int.TryParse,失败就拒掉 - Java 的 JDBC 同理:用
CallableStatement,setInt(1, id)比setObject(1, id)更可靠 - 如果存储过程返回结果集结构不稳定(比如根据参数动态 SELECT 不同列),客户端必须按列名取值,而非按索引——否则字段顺序一变就错位
最麻烦的从来不是写存储过程本身,而是确认每一条从 Web 请求进来、穿过中间件、落到数据库的路径上,有没有哪个环节悄悄把参数当字符串连了又连。这种链路一旦拉长,光靠存储过程里的防御根本兜不住。










