位置:首页 > SQL > 如何防止动态 SQL 存储过程发生注入攻击?

如何防止动态 SQL 存储过程发生注入攻击?

时间:2026-08-24  |  作者:夜鞌不睡  |  阅读:0

目录

  1. sp_executesql 能防什么,不能防什么
  2. 对象名必须用 QUOTENAME() 包裹,并配合白名单
  3. 排序方向等非值片段,必须做严格枚举控制
  4. 同时收紧权限与执行计划,避免把风险放大
  5. 过滤单引号、分号、关键字,基本都靠不住

前言

在 SQL Server 里,动态 SQL 一旦把表名、列名、排序方向直接交给外部输入,`sp_executesql` 也帮不上忙,因为它只能保护值参数。本文把真正有效的防线拆成对象名包装、非值片段白名单和最小权限执行三部分,读完你可以快速判断一个存储过程到底是“可控动态”,还是仍然暴露在注入面前。

很多团队在把查询改成动态 SQL 后,会默认以为只要用了 sp_executesql 就算安全,实际问题恰恰出在这里:它只能参数化“值”,并不能替代表名、列名、排序方向这些 SQL 语法结构。要判断一个动态 SQL 存储过程是否真的可靠,关键要看三件事:对象名有没有安全包装,非值片段有没有白名单约束,执行上下文有没有被限制在最小权限范围内。

sp_executesql 能防什么,不能防什么

sp_executesql 的作用边界需要先讲清楚。它适合处理查询条件里的值参数,例如 WHERE Id = @IdWHERE Name = @Name,这样可以避免把用户输入直接拼进 SQL 文本。

但列名、表名、schema 名、排序字段、排序方向这些内容,属于 T-SQL 语法本身,不能通过参数占位符替代。如果把这类内容直接拼接进动态 SQL,注入风险仍然存在。因此,安全设计的核心不是“全部参数化”,而是把“语法结构”和“数据值”分开处理:值参数交给 sp_executesql,语法片段则必须用专门的约束手段。

对象名必须用 QUOTENAME() 包裹,并配合白名单

表名、列名、schema 名都属于标识符,最常见的安全处理方式是使用 QUOTENAME()。这是 SQL Server 认可的安全包装函数,会自动转义并加上双方括号。

展示对象名动态拼接时的安全处理步骤,包括输入约束、长度截断、QUOTENAME 包裹和字段白名单映射
动态对象名的安全处理链路对象名不能参数化时,安全性来自输入约束、长度控制和 QUOTENAME() 包裹。

例如,QUOTENAME('user; drop table x --') 会返回 [user; drop table x --]。这样一来,原本可能被解释成额外语句的内容,会被当成一个整体标识符处理,非法字符失去语法意义。

这里有一个常见误区:有人会用替换单引号、删除特殊字符之类的方式处理对象名。这种做法对标识符场景并不可靠,面对 unicode 同形字、嵌套注释等绕过方式时基本站不住脚。对象名的安全处理,应该遵循下面几条规则:

  • 只允许传入纯标识符字符串,不带空格、点号、括号等额外结构
  • 如果字段来自前端下拉框,不直接信任前端值,而是做白名单映射
  • schema 与表名必须分别处理,通常写成 QUOTENAME(@schemaname) + QUOTENAME(@tablename),缺一不可

字段排序就是典型例子。与其让前端直接传真实列名,不如只接收业务侧定义好的键,再在存储过程中映射成实际字段:

CASE @sortcol WHEN 'name' THEN 'FirstName' END

这样做的意义不只是“防止乱传值”,更重要的是把可用字段范围明确收死,避免用户借动态排序字段去探测数据库结构。

还有一个容易被忽略的细节:输入长度也要先控制。像 @tablename 这种外部输入,最好先截断,例如 SUBSTRING(@input, 1, 128),再交给 QUOTENAME() 处理。这样可以降低异常构造输入带来的风险,也更符合 SQL Server 标识符长度的实际边界。

排序方向等非值片段,必须做严格枚举控制

除了对象名,另一个高风险点是排序方向。很多动态查询会写成 ORDER BY ... ' + @direction,看起来只是拼个 ASCDESC,但这同样属于 SQL 语法片段,不能直接信任用户输入。

展示排序方向和其他非值动态片段的白名单控制方式,强调只能用严格枚举而非直接拼接
非值动态片段的枚举边界排序方向、JOIN 类型、聚合函数名这类非值片段无法参数化,必须先枚举再拼接。

SQL Server 不支持把排序方向本身做参数化,所以正确思路不是“继续拼接”,而是先把输入约束为严格的枚举值,再把校验后的结果用于 SQL 拼接。例如:

DECLARE @sortdir NVARCHAR(4) = UPPER(@sortdir_input);
IF @sortdir NOT IN ('ASC', 'DESC') SET @sortdir = 'ASC';
-- 然后在动态 SQL 中:
... ORDER BY ' + @sortcol + ' ' + @sortdir

这里要注意两点。第一,@sortdir 不能放进 sp_executesql 的参数列表里替代 SQL 关键字,它必须在拼接前就完成校验。第二,校验不能只看字面值,还要防止空格、制表符、unicode 全角字符等混入,否则表面上看是 ASC,实际字符串可能已经被污染。

同样的原则也适用于其他非值类动态片段,例如 JOIN 类型、聚合函数名、分组字段等。凡是不能参数化、又会改变 SQL 结构的部分,都应该通过白名单枚举或固定映射来控制,而不是让外部输入直接参与拼接。

同时收紧权限与执行计划,避免把风险放大

动态 SQL 的问题不只是“有没有注入”,还包括一旦发生注入,攻击面会不会被权限模型放大。很多存储过程本身只是做查询,但如果运行上下文设置得过高,例如使用 EXECUTE AS OWNER,或者过程实际以 db_owner 身份执行,那么即使调用方原本只有 SELECT 权限,注入进去的恶意语句也可能借过程权限执行。

展示动态 SQL 的权限与运维风险,包括最小权限执行、禁用高危权限、避免元数据拼接和减少执行计划碎片
权限上下文与执行计划的双重收口输入校验之外,还要限制过程执行身份和 SQL 模板数量,避免注入被高权限放大。

这意味着,动态 SQL 存储过程的安全边界不能只靠输入校验,还必须收紧执行上下文。比较稳妥的做法包括:

  • 让存储过程运行在最小权限身份下,例如 EXECUTE AS 'app_reader'
  • 该身份只保留目标表所需的 SELECT 等最低权限
  • 明确禁用 CREATE TABLEDROPEXEC 等 DDL/DCL 能力,哪怕看起来只是辅助用途

另一个常被忽视的问题是执行计划污染。每次拼接出不同的 SQL 字符串,SQL Server 都可能生成新的执行计划。这不仅会额外消耗 CPU,还会让日志里充满大量“唯一语句”,导致排查异常时很难看出攻击模式。原本应该是少数稳定模板的查询,最后变成了不可归类的碎片化语句,这会直接削弱监控和审计能力。

因此,能参数化的值一定要参数化,不能参数化的结构片段则尽量限制在少量可预测的白名单组合内。这样既是为了防注入,也是为了控制执行计划数量和提高日志可读性。

另外,最好不要通过 sys.tablessys.columns 等系统视图动态查询元数据,再反过来拼接 SQL。表面上这是“更灵活”,实际上等于把数据库结构暴露成攻击面的组成部分。一旦输入链路被利用,攻击者可以更容易借助元数据组织更精准的注入语句。

过滤单引号、分号、关键字,基本都靠不住

在实际项目里,最容易出现的错误不是完全不防,而是用了看似防护、实际上无效的土办法。比如把 ' 替换成 ''、删除 ;、屏蔽 UNION 关键字,或者加一层简单正则。这些方法在真实攻击环境下都很脆弱。

原因很直接:攻击者并不需要老老实实用最显眼的 payload。没有分号,仍然可以尝试时间盲注,例如 WAITFOR DELAY '0:0:5';关键字被过滤,也可以通过注释拆分、变形拼接等方式绕过,例如 /*/**/UNION SELECT。如果防线建立在“猜攻击者会输入什么字符”上,基本迟早会失守。

真正能落地、而且可长期维护的做法其实只有两条:

  • 所有对象名统一使用 QUOTENAME() 包裹,并且先做输入约束或白名单映射
  • 所有非值类动态片段都限制为可枚举的固定集合,例如排序方向、JOIN 类型、聚合函数名

如果这两条没有做到,其他“过滤器”再多,也只是增加心理安慰。反过来说,只要把对象名、枚举值、执行权限三层边界收紧,大多数动态 SQL 存储过程的注入面就会明显缩小。

免责声明:文中图文均来自网络,如有侵权请联系删除,心愿游戏发布此文仅为传递信息,不代表心愿游戏认同其观点或证实其描述。

相关文章

更多

精选合集

更多

大家都在玩

热门话题

大家都在看

更多