如何在 SQL Server 中清理(防止 SQL 注入)动态 SQL?

Posted

技术标签:

【中文标题】如何在 SQL Server 中清理(防止 SQL 注入)动态 SQL?【英文标题】:How to cleanse (prevent SQL injection) dynamic SQL in SQL Server? 【发布时间】:2011-05-05 09:09:42 【问题描述】:

我们有大量依赖动态 SQL 的 SQL Server 存储过程。

存储过程的参数用于动态 SQL 语句。

我们需要在这些存储过程中使用标准验证函数来验证这些参数并防止 SQL 注入。

假设我们有这些约束:

    我们无法重写过程以不使用动态 SQL

    我们不能使用 sp_OACreate 等来使用正则表达式进行验证。

    我们不能修改调用存储过程的应用程序在参数传递给存储过程之前对其进行验证。

是否有一组字符我们可以过滤掉以确保我们不会受到 SQL 注入的影响?

【问题讨论】:

哎哟。通常是 3) 应该修改它以防止 SQL 注入。请记住,这是“SQL 注入”,而不是“SQL 拒绝”。一旦它到达数据库,它应该已经被清理了。但是如果你说你不能改变应用程序,那么我猜你不能。有兴趣看看答案。 【参考方案1】:

我相信您需要担心三种不同的情况:

字符串(任何需要引号的):'''' + replace(@string, '''', '''''') + '''' 名称(不允许使用引号的任何内容):quotename(@string) 不能引用的东西:这需要列入白名单

注意Everything 来自用户控制的字符串变量(charvarcharncharnvarchar 等)来源必须使用上述方法之一。这意味着,如果将它们存储在字符串变量中,即使您希望是数字的东西也会被引用。

更多详情,请参阅Microsoft Magazine (过时链接:2016-10-19)

这是一个使用所有三种方法的示例:

EXEC 'SELECT * FROM Employee WHERE Salary > ''' +
     REPLACE(@salary, '''', '''''') +   -- replacing quotes even for numeric data
     ''' ORDER BY ' + QUOTENAME(@sort_col) + ' ' +  -- quoting a name
     CASE @sort_dir WHEN 'DESC' THEN 'DESC' END     -- whitelisting

还请注意,通过在EXEC 语句中内联执行所有字符串操作,无需担心截断问题。如果您将中间结果分配给变量,您必须确保变量足够大以容纳结果。如果您使用SET @result = QUOTENAME(@name),您应该定义@result 以容纳至少258 (2 * 128 + 2) 个字符。如果您使用SET @result = REPLACE(@str, '''', ''''''),您应该将@result 定义为@str 大小的两倍(假设@str 中的每个字符都可能是引号)。当然,保存最终 SQL 语句的字符串变量必须足够大,以容纳所有静态 SQL 以及所有结果变量。

【讨论】:

我同意这里,这完全取决于正在构建的 SQL 【参考方案2】:

琐碎的情况可以通过QUOTENAME 和REPLACE 来修复:

set @sql = N'SELECT ' + QUOTENAME(@column) + 
   N' FROM Table WHERE Name = ' + REPLACE(@name, '''', '''''');

虽然 QUOTENAME 也可以用于文字来添加单引号并用双单引号替换单引号,因为它会将输入截断为 128 个字符,因此不建议这样做。

但这只是冰山一角。您需要妥善处理多部分名称 (dbo.table):引用多部分名称将导致无效标识符 [dbo.table],必须对其进行解析和拆分(使用 PARSENAME),然后正确引用到 @987654329 @。

另一个问题是截断攻击,即使您对文字执行微不足道的 REPLACE,也可能发生这种情况,请参阅 New SQL Truncation Attacks And How To Avoid Them。

SQL 注入问题永远无法通过在每个过程中放置​​一个魔术函数来解决。这就像在问“我想要一个能让我的代码运行得更快的函数”。防止注入攻击是端到端游戏,需要始终编码纪律,不能简单地添加作为事后的想法。您最好的机会是检查每一个过程并逐行分析 T-SQL 代码,同时注意漏洞,然后在发现问题时解决问题。

【讨论】:

我建议 not 使用 PARSENAME,因为它旨在用于已引用的名称。如果您的用户告诉您他想从secret..table 获取数据,您想查询[secret..table] 并得到一个错误。你不希望他能够查询[secret]..[table] 在我看来,使用除 sp_executesql 以外的任何东西并将所有值作为参数传递的任何东西运行任何动态 SQL 都是纯粹的弊端。【参考方案3】:

有了这些限制,你就完蛋了。

这里有两个选项可能会给你一些方向:

    使用白名单验证器/解析器,它只接受格式和关键字和预期表格的查询。这可能只适用于真正理解语法的非常好的 SQL 解析器。

    在受限环境中执行查询。例如,使用权限非常有限的用户帐户。例如,只允许(读取)访问某些永远不会返回敏感数据的视图,而不允许访问所有其他视图、所有存储过程、函数和表。更安全的是在另一个数据库服务器上执行这些查询。另外不要忘记禁用OPENROWSET 命令。

请注意以下几点:

    当你接受所有查询,除了那些有无效关键字的查询,你肯定会失败,因为黑名单总是失败。尤其是像 SQL 这样复杂的语言。

    不要忘记允许来自您不信任的来源的动态 SQL 在最纯粹的意义上是邪恶的,即使您使用这些技巧也是如此,因为偶尔会发现 bugs 可以通过发送来滥用特制的 SQL 到服务器。因此,即使您应用了这些技巧,风险仍然存在。

    当您决定采用允许动态 SQL 的解决方案时。请不要认为您可以自己想出一个安全的解决方案,尤其是在您试图保护敏感的业务数据时。聘请数据库服务器安全专家来帮助您。

【讨论】:

【参考方案4】:

这是一个非常讨厌的问题,它不是你想要解决的问题,但是这里有一个可行的简单案例,(如果我错过了一个案例,请告诉我,这个附带NO保证)

create proc Bad 
  @param nvarchar(500) 
as 

exec (N'select ''' + @param + N'''') 

go

-- oops injected
exec Bad 'help'' select ''0wned!'' select ''' 

go 

create proc NotAsBad
   @param nvarchar(500) 
as 

declare @safish nvarchar(1000), @sql nvarchar(2000) 
set @safish = replace(@param, '''', '''''')

set @sql = N'select ''' + @safish  + N''''

exec (@sql) 

go 

-- this kind of works, but I have not tested everything
exec NotAsBad 'help'' select ''0wned!'' select ''' 

【讨论】:

+1,我从未见过任何暗示这不起作用。 在我看来,使用除 sp_executesql 以外的任何东西并将所有值作为参数传递的任何东西运行任何动态 SQL 都是纯粹的弊端。 仍然容易受到攻击。假设 NotAsBad 的主体包含以下内容: set @sql = N'select * from ' +@safish ....如果用户可以猜到表的名称,他们可以提交 @param = 'tablename;删除数据库 xyz; --' @frankadelic 这适用于微不足道的情况,当然,根据您的上下文,您需要以不同的方式转义 sql,因此警告不要这样做,我同意@KM,一般来说这是这样的事情是个坏主意,而不是你想解决的问题【参考方案5】:

是否有一组字符我们可以过滤掉以确保我们不会受到 SQL 注入的影响?

SQL 注入不称为“特定字符集注入”,这是有原因的。过滤掉某些字符可能会使特定的利用复杂化,但并不能阻止 SQL 注入本身。要利用 SQL 注入,必须编写 SQL。而且 SQL 不限于少数特殊字符。

【讨论】:

【参考方案6】:

OWASP 有一些关于这个策略的信息。它应该始终是最后的选择(如我链接到的文章中所述),但如果它是您唯一的选择......

http://www.owasp.org/index.php/SQL_Injection_Prevention_Cheat_Sheet

文章中关于它是最后的选择的引述

但是,这种方法很脆弱 与使用参数化相比 查询。这种技术应该只 谨慎地用于改造遗留物 以具有成本效益的方式编码。 从头开始构建的应用程序,或 需要低风险的应用 应建立公差或 使用参数化重写 查询。

本质上,反对这种方法的论点是,即使您确实逃避了所有已知的错误输入,也不能保证将来不会有人想出规避它的方法。

但是,要具体回答您的问题...

要转义的字符列表在我上面链接的文章中。

编辑 如前所述,该文章没有提供很好的链接。但是,对于 SQL Server,这个可以:http://msdn.microsoft.com/en-us/library/ms161953.aspx

请注意,您需要转义的字符列表会因 DB 平台而异,但看起来您使用的是 SQL Server,所以这应该是相关的..

引用以下文章:

过滤输入也可能有助于通过删除转义字符来防止 SQL 注入。但是,由于可能造成问题的字符数量众多,因此这不是可靠的防御方法。以下示例搜索字符串分隔符。

private string SafeSqlLiteral(string inputSQL)

  return inputSQL.Replace("'", "''");

LIKE 子句

请注意,如果您使用 LIKE 子句,通配符仍然必须转义:

s = s.Replace("[", "[[]");
s = s.Replace("%", "[%]");
s = s.Replace("_", "[_]");

【讨论】:

-1:文章没有说明要为 MS SQL Server 转义哪些字符。它只是链接到另一篇文章,并没有明确说明要转义哪些字符。【参考方案7】:

您能否获得 SQL CLR 非常有用——您至少可以使用它来编写比使用 T-SQL 更有效的清理程序。在一个完美的世界中,您可以用参数化语句和其他更强大的结构完全替换存储的过程。

【讨论】:

很遗憾,由于 DBA 的限制,我无法使用 CLR【参考方案8】:

还有另一种可能可能起作用的方法,尽管它取决于存储过程的参数中允许使用哪些字符。与其转义可用于 SQL 注入的麻烦字符,不如删除这些字符。例如,如果你有这个 SP:

create procedure dbo.MYSP(@p1 varchar(100))
as begin
  set @p1 = Replace(@p1, '''',' '); -- Convert single quotes to spaces
  set @p1 = Replace(@p1, ';', ' ');
  set @p1 = Replace(@p1, '--', ' ');      
  set @p1 = Replace(@p1, '/*', ' ');      
  set @p1 = Replace(@p1, '*/', ' ');      
  set @p1 = Replace(@p1, 'xp_', ' ');      
  ...
end;

您可以用空格或空字符串替换任何单引号。这种方法也可用于替换注释字符,例如 /* */ -- 通过使用更多替换命令(如我刚刚在上面显示的那样)。但请注意,这种方法只有在您从不期望在正常输入中出现这些字符时才有效,这取决于您的应用程序。

注意替换字符集基于https://msdn.microsoft.com/en-us/library/ms161953(SQL.105).aspx

【讨论】:

SQL 注入不称为“单引号注入”。是有原因的。 我不熟悉“单引号注入”,我刚才描述的技术是一种防范 SQL 注入的方法,它基于我上面引用的 Microsoft 文章。我不清楚你为什么不赞成这个答案。 我一直热衷于了解有关安全性的更多信息,欢迎您解释为什么微软在 msdn.microsoft.com/en-us/library/ms161953(SQL.105).aspx 中的建议是“故意存在缺陷的”。 因为例如,如果在本网站上使用此建议,您将完全无法发布您的答案 我再次试图理解这里 - 你认为微软的建议是可悲的吗?对我来说,考虑到他们在问题中列出的所有限制条件,这似乎是一种可能对原始问题有所帮助的方法。

以上是关于如何在 SQL Server 中清理(防止 SQL 注入)动态 SQL?的主要内容,如果未能解决你的问题,请参考以下文章

如何清理SQL Server ErrorLog错误日志

SQL Server 快速清除日志文件的方法

SQL Server如何防止动态sql中的sql注入

清理请求以防止 SQL 注入攻击

如何防止 SQL Server 表中的重复

如何清理SQL Server中的事务日志