在SQL存储过程中如何安全地传递参数防止注入攻击?

作者:袖梨 2026-07-09
存储过程参数必须直接使用,严禁拼接进动态SQL;安全做法是参数直引用或通过USING绑定值,结构部分(如表名)须经元数据白名单校验,且执行账号权限需严格限制。

存储过程参数必须直接用,不能拼进动态SQL

MySQL 存储过程中,只要参数不参与字符串拼接,就天然免疫注入。比如 SELECT * FROM users WHERE id = p_user_id 这种写法,p_user_id 是输入参数,MySQL 会按类型安全绑定,无需额外处理。

常见错误是把参数拿去拼 CONCAT()SET @sql = CONCAT('SELECT * FROM users WHERE id = ', p_user_id)——哪怕 p_user_id 是整数,也打开了注入口。一旦传入 1 OR SLEEP(5),查询就变成全表扫描加延迟。

  • 所有 WHERE、JOIN、HAVING 条件中,直接引用参数名,别转成字符串再拼
  • 避免在存储过程里用 PREPARE + EXECUTE 包裹含参数的字符串,除非真需要动态结构
  • 如果参数是字符串类型,注意 MySQL 默认不会自动加引号,但类型校验(如 INT 参数传入字母会报错)本身已是第一道过滤

必须用动态SQL时,只拼结构,值全走 USING

当确实要构建可变表名或字段列表(比如分表查询),PREPARE + EXECUTE 不可避免,但必须严格分离:结构部分(表名、列名)靠白名单或系统视图校验;数据部分(WHERE 值、LIMIT 数)全部通过 USING 绑定。

错误示例:SET @sql = CONCAT('SELECT * FROM ', p_table_name, ' WHERE status = ''', p_status, '''') —— 这里 p_table_namep_status 都没防住。

  • 正确做法:SET @sql = CONCAT('SELECT * FROM ', (SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = 'mydb' AND TABLE_NAME = p_table_name LIMIT 1));
  • 然后 PREPARE stmt FROM @sql; EXECUTE stmt USING p_status;
  • USING 只支持值绑定,不支持标识符,所以 p_status 安全,而表名校验必须前置

SQL Server 存储过程必须用 sp_executesql,禁用 EXEC(@sql)

SQL Server 中 EXEC(@sql) 等价于把整个字符串当命令执行,用户输入一旦混入,立刻执行任意语句。而 sp_executesql 支持参数化,值不参与语法解析,是唯一合规路径。

典型翻车点:有人以为给 @sql 加了 QUOTENAME() 就安全,但若参数本身是拼出来的:N'@p nvarchar(50) = ''' + @user_input + '''',还是回到了字符串拼接的老路。

  • 必须写成:EXEC sp_executesql @sql, N'@name nvarchar(50)', @name = @user_input
  • @sql 字符串里只能出现占位符 @name,不能出现任何用户输入的字面量
  • 类型声明(N'@name nvarchar(50)')必须精确匹配,否则可能触发隐式转换导致截断或失败

动态对象名(表/列)必须白名单校验,escapeId() 或 QUOTENAME() 是补救不是替代

connection.escapeId()(Node.js mysql2)或 QUOTENAME()(SQL Server)能转义反引号或方括号,但它们不验证对象是否存在、是否合法。攻击者传入 user`; DROP TABLE logs; --escapeId() 会输出 `user`; DROP TABLE logs; --`,依然执行两句话。

真正可靠的方式是查元数据表确认存在且归属预期范围:

  • MySQL:SELECT 1 FROM information_schema.TABLES WHERE TABLE_SCHEMA = 'app_db' AND TABLE_NAME = p_table_name
  • SQL Server: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', 1;
  • 别依赖 OBJECT_ID(@name) 单独判断——它对非法名称返回 NULL,但无法区分不存在和恶意截断

最易被忽略的是权限上下文:即使表名校验通过,执行账号也必须无权访问其他敏感表。否则,校验形同虚设。

相关文章

精彩推荐