跳转到主内容
websoft网络软件专家 - 深耕网络技术,打造实用软件!

如何防止SQL注入利用存储过程_确保存储过程不拼字符串

必须用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 < 1 OR @user_id > 999999 RETURN
这类硬约束 对字符串类参数(如用户名),用
LEN(@name) > 0 AND LEN(@name) <= 50 AND @name NOT LIKE '%[^a-zA-Z0-9_]%'
控制内容范围 避免在存储过程里做
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 params)
方法,看起来方便,实则埋雷:它往往把所有参数都当字符串塞进
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 请求进来、穿过中间件、落到数据库的路径上,有没有哪个环节悄悄把参数当字符串连了又连。这种链路一旦拉长,光靠存储过程里的防御根本兜不住。

相关文章