smhy8187

 

阻止 SQL Injection 攻击

http://msdn.microsoft.com/msdnmag/issues/04/09/SQLInjection/Default.aspx?loc=zh

数据安全性

抢先阻止 SQL Injection 攻击



本文讨论:
  • SQL Injection 攻击的工作原理
  • 测试漏洞
  • 验证用户输入
  • 使用 .NET 功能预防攻击
  • 处理异常的重要性
本文使用了以下技术:
ASP.NET、C#、SQL


Download ImageGet the sample code for this article.
目录
边栏


过装备高级服务器端技术(如 ASP.NET)和功能强大的数据库服务器(如 Microsoft® SQL Server),开发人员能够极其轻松地创建动态的数据驱动网站。但是,黑客通过极其常见的一类攻击(即 SQL Injection 攻击),可以轻松地利用 ASP.NET 和 SQL 的功能对您发起攻击。

 

SQL Injection 攻击的基本原理如下:您创建一个网页,允许用户向文本框中输入文本,用于对数据库执行查询。黑客向文本框中输入不正确的 SQL 语句以更改查询的性质,从而可以使用它入侵、更改或损坏后端数据库。这是如何实现的呢?让我举例说明一下。


正确的 SQL 变得不正确

许多 ASP.NET 应用程序使用如图 1 所示的窗体验证用户身份。某用户单击 BadLogin.aspx 的“登录”按钮时,cmdLogin_Click 方法会尝试验证该用户的身份,方法是运行一个查询,对 Users 表中用户名和密码与用户在窗体的文本框控件中输入的值匹配的记录数进行计数。

大多数情况下,窗体完全按预期工作。用户输入与 Users 表中记录匹配的用户名和密码。动态生成的 SQL 查询用于检索匹配的行数。然后,验证用户身份并将其重定向到请求页。输入无效用户名和/或密码的用户无法通过身份验证。但是,黑客也可能向“用户名”文本框中输入以下看上去无害的文本来进入系统,而不必知道有效的用户名和密码:

' Or 1=1 --
黑客通过向查询中注入不正确的 SQL 来侵入系统。此特定攻击会起作用,是因为所执行的查询由固定字符串和用户输入的值串联而成,如下所示:
string strQry = "SELECT Count(*) FROM Users WHERE UserName='" +
txtUser.Text + "' AND Password='" + txtPassword.Text + "'";
在用户输入有效的用户名“Paul”和密码“password”时,strQry 将变为:
SELECT Count(*) FROM Users WHERE UserName='Paul' AND Password='password'
但是当黑客输入以下内容时
' Or 1=1 --
查询现在变为:
SELECT Count(*) FROM Users WHERE UserName='' Or 1=1 --' AND Password=''

 

由于一对连字符标明 SQL 中注释的开始,因此查询仅变为:

SELECT Count(*) FROM Users WHERE UserName='' Or 1=1
对于表中的所有行,表达式 1=1 始终为真,而为真的表达式与另一个表达式执行“或”运算后将始终返回真。因此,假定 Users 表中至少存在一行,此 SQL 将始终返回非零的记录计数。

 

并非所有的 SQL Injection 攻击都涉及窗体身份验证。它所利用的只是具有一些动态构造的 SQL 和不可信用户输入的应用程序。如果条件合适,则这样的攻击所导致的损坏程度可能仅限于黑客的 SQL 语言知识程度和数据库配置。

现在,请考虑图 2 所示的代码(摘自 BadProductList.aspx)。此页显示罗斯文数据库中的产品,允许用户使用名为 txtFilter 的文本框筛选生成的产品清单。与上一示例一样,此页也会受到 SQL Injection 攻击,因为执行的 SQL 是通过用户输入的值动态构造的。此特定页是黑客的“乐土”,因为狡猾的黑客可以劫持它,以泄露机密信息、更改数据库中的数据、损坏数据库记录,甚至创建新的数据库用户帐户。

与 SQL 兼容的大多数数据库(包括 SQL Server)将元数据存储在名为 sysobjects、syscolumns、sysindexes 等的一系列系统表中。这意味着,黑客可能使用系统表探知数据库的架构信息以便其进一步损害数据库。例如,在 txtFilter 文本框中输入的以下文本可能会被用于披露数据库中用户表的名称:

' UNION SELECT id, name, '', 0 FROM sysobjects WHERE xtype ='U' --

 

UNION 语句对黑客尤其有用,因为黑客可以通过它将一个查询的结果接合到另一查询结果上。在这种情况下,黑客已将数据库中用户表的名称接合到 Products 表的原始查询。唯一的技巧是将列的数目和数据类型与原始查询匹配。前一查询可能会披露数据库中存在一个名为 Users 的表。第二个查询可能会披露 Users 表中的列。使用此信息,黑客可能将以下内容输入到 txtFilter 文本框中:

' UNION SELECT 0, UserName, Password, 0 FROM Users --
输入此查询可披露在 Users 表中找到的用户名和密码,如图 3 所示。

 

图 3 查询 Users 表
图 3 查询 Users 表

SQL Injection 攻击还可用于更改数据或损坏数据库。SQL Injection 黑客可能向 txtFilter 文本框中输入以下内容,将第一种产品的价格从 18 美元更改为 0.01 美元,然后在有人注意到所发生情况之前快速采购几箱该产品:

'; UPDATE Products SET UnitPrice = 0.01 WHERE ProductId = 1--

 

此攻击会起作用,是因为 SQL Server 允许您将多条 SQL 语句(用分号或空格分隔)串联在一起。在此示例中,DataGrid 未显示任何内容,但是更新查询成功运行。这一技术可能会被用来执行 DROP TABLE 语句,或执行创建新用户帐户并将该用户添加到 sysadmin 角色的系统存储过程。使用图 2 所示的 BadProductList.aspx 页,这些攻击都是可能的。

Back to top

均等机会攻击

SQL Injection 攻击不仅限于 SQL Server,认识到这一点很重要。其他数据库(包括 Oracle、MySQL、DB2、Sybase 等)也容易遭受此类型的攻击。之所以会发生 SQL Injection 攻击,是因为 SQL 语言包含许多使其非常强大和灵活的功能,即:

  • 使用一对连字符在 SQL 语句中嵌入注释的功能
  • 将多个 SQL 语句串在一起并在一个批处理中执行它们的功能
  • 使用 SQL 从一组标准系统表查询元数据的功能

 

通常,数据库支持的 SQL 语句的功能越强大,数据库受攻击的可能性就越大。因此,SQL Server 是 Injection 攻击的主要目标也就不足为奇了。

SQL Injection 攻击并不仅限于 ASP.NET 应用程序。传统的 ASP、Java、JSP 和 PHP 应用程序面临同样的风险。实际上,也可能对桌面应用程序展开 SQL Injection 攻击。例如,在本文的下载文件(可从本文顶部的链接获得)中包括一个名为 SQLInjectWinForm 的 Windows® 窗体应用程序示例,该应用程序也容易受到 SQL Injection 攻击。

尽管采取一两项关键措施预防 SQL Injection 攻击是很容易的,但最好是对问题采用分层方法。这样,如果其中一项措施因某个漏洞而失去作用,则您仍可以受到保护。在图 4 中概述了建议的层。

Back to top

所有输入都是不可信的

图 4 中列出的第一个原则是极其重要的:假定所有用户输入都是不可信的!决不可以在数据库查询中使用未经验证的用户输入。ASP.NET 验证控件(尤其是 RegularExpressionValidator 控件)是一种验证用户输入的有效工具。

有以下两种基本的验证方法:禁止棘手字符或者仅允许少量的必需字符。尽管可以轻松地禁止一些棘手字符,如连字符和单引号,但是此方法不是太理想,有以下两个原因:首先,可能漏过对黑客很有用的字符;其次,通常有多种方式可以表示错误字符。例如,黑客也许能够将单引号转义,以便您的验证代码漏过它并将转义后的引号传递到数据库,数据库会将它视为与普通的单引号字符相同。更好的方法是识别可允许的字符并仅允许那些字符。此方法需要做更多的工作,但是可确保更加紧密地控制输入,因此是更安全的方法。不管采用哪种方法,都同样希望限制输入的长度,因为某些攻击需要大量的字符。

GoodLogin.aspx(在代码下载中也可以找到它)包含两个正则表达式验证程序控件,其中一个用于用户名,另一个用于密码,使用以下 ValidationExpression 值,可将输入限制为 4 到 12 个字符(数字、字母字符和下划线):

[\d_a-zA-Z]{4,12}

 

您可能需要允许用户将可能有害的字符输入到文本框中。例如,用户可能需要将单引号(或省略号)作为人名的一部分输入。在这种情况下,可以通过使用正则表达式或者 String.Replace 方法将每个单引号实例替换为两个单引号,使单引号无害。例如:

string strSanitizedInput = strInput.Replace("'", "''");

 

Back to top

避免动态 SQL

我在本文中说明的 SQL Injection 攻击都依赖于动态 SQL(即,通过将 SQL 与用户输入的值串联在一起而构造的 SQL 语句)的执行。但是,使用参数化 SQL 会大大降低黑客将 SQL 注入您的代码的能力。

图 5 中的代码使用参数化 SQL 阻止 Injection 攻击。如果必须无条件使用即席 SQL,则参数化 SQL 是很重要的。如果您的 IT 部门不信任存储过程或者使用诸如 MySQL(5.0 版之前不支持它们)的产品,则这可能是必需的。但是,只要有可能,就应该使用存储过程的附加功能,删除对数据库中基表的所有权限,这样就取消了创建如图 3 所示的查询的功能。图 6 所示的 BetterLogin.aspx 使用存储过程 procVerifyUser 来验证用户。

Back to top

使用最低权限执行

BadLogin.aspx 和 BadProductList.aspx 中演示的错误做法之一是,使用了利用 sa 帐户的连接字符串。下面是连接字符串(可以在 Web.config 中找到):

<add key="cnxNWindBad"
value="server=localhost;uid=sa;pwd=;database=northwind;" />

 

此帐户使用系统管理员角色运行,这意味着允许它执行几乎所有操作:创建登录名,删除数据库等等。可以确切地讲,将 sa(或任何高权限帐户)用于应用程序数据库访问是一个很糟糕的主意。更好的主意是创建有限访问帐户并改用该帐户。GoodLogin.aspx 中所用的帐户使用以下连接字符串:

<add key="cnxNWindGood"
value="server=localhost;uid=NWindReader;pwd=utbbeesozg4d;
database=northwind;" />

 

NWindReader 帐户使用 db_datareader 角色(将其访问限制为读取数据库中的表)运行。BetterLogin.aspx 通过使用存储过程和登录名 WebLimitedUser(它仅具有执行该存储过程的权限,而无权执行基础表)改进了该情况。

Back to top

安全地存储机密信息

图 3 所示的 SQL Injection 攻击导致显示 Users 表中的用户名和密码。使用窗体身份验证时通常使用此类型的表,而且在许多应用程序中,密码以明文形式存储。更好的替代方法是在数据库中存储加密密码或哈希密码。哈希密码比加密密码更安全,因为无法对其进行解密。通过向哈希中添加 salt(密码安全随机值),可以进一步强化哈希密码。BestLogin.aspx 包含用于将用户输入的密码与 SecureUsers 表中所存储密码的 salt 强化哈希版本比较的代码(参见图 7)。哈希难题的另一部分是 AddSecureUser.aspx。此页可以用于生成 salt 强化哈希密码并将其存储在 SecureUsers 表中。

BestLogin.aspx 和 AddSecureUser.aspx 都使用 SaltedHash 类库中的代码,如图 8 所示。此代码(由 Jeff Prosise 创建)使用 System.Web.Security 命名空间中的 FormsAuthentication.HashPasswordForStoringInConfigFile 方法创建密码哈希,并使用 System.Security.Cryptography 命名空间中的 RNGCryptoServiceProvider.GetNonZeroBytes 方法创建随机 16 字节 salt 值(使用 Convert.ToBase64String 将其转换为字符串时,它将变为 24 个字符)。

虽然不与 SQL Injection 攻击直接相关,但是 BestLogin.aspx 演示了另一种安全最佳做法:对连接字符串进行加密。如果连接字符串包含嵌入式数据库帐户密码,则确保连接字符串的安全尤其重要,这与 BestLogin.aspx 的情况一样。由于您需要连接字符串的解密版本才能连接到数据库,因此无法对连接字符串进行哈希处理。您需要改为将它加密。下面是在 Web.config 中存储且由 BestLogin.aspx 使用的加密连接字符串的外观:

<add key="cnxNWindBest"
value="AQAAANCMnd8BFdERjHoAwE/
Cl+sBAAAAcWMZ8XhPz0O8jHcS1539LAQAAAACAAAAAAADZgAAqAAAABAAAABdodw0YhWfcC6+
UjUUOiMwAAAAAASAAACgAAAAEAAAALPzjTRnAPt7/W8v38ikHL5IAAAAzctRyEcHxWkzxeqbq/
V9ogaSqS4UxvKC9zmrXUoJ9mwrNZ/
XZ9LgbfcDXIIAXm2DLRCGRHMtrZrp9yledz0n9kgP3b3s+
X8wFAAAANmLu0UfOJdTc4WjlQQgmZElY7Z8"
/>

 

BestLogin 从 SecureConnection 类(如图 9 所示)调用 GetCnxString 方法,检索 cnxNWindBest AppSetting 值并使用以下代码对它进行解密:

 string strCnx = SecureConnection.GetCnxString("cnxNWindBest");

 

SecureConnection 类又调用 DataProtect 类库(此处未显示但包括在本文的下载中),这将包装对 Win32® 数据保护 API (DPAPI) 的调用。DPAPI 的出色功能之一是为您管理加密密钥。有关 DataProtect 类库的详细信息(包括使用它时要考虑的其他选项),请参阅 Microsoft 模式和实践指南“构建安全的 ASP.NET 应用程序:身份验证、授权和安全通信”。

图 10 EncryptCnxString.aspx
图 10 EncryptCnxString.aspx

可以使用 EncryptCnxString.aspx 页创建要粘贴到配置文件中的计算机特定的加密连接字符串。此页如图 10 所示。当然,除可能要加密或哈希的密码和连接字符串外,还存在其他机密信息,其中包括信用卡号码以及披露给黑客后可能造成损害的任何其他内容。ASP.NET 2.0 包括应该简化密码哈希和连接字符串加密的许多功能。

Back to top

正常失败

对运行时异常的处理不足是黑客将尝试利用的另一漏洞。因此,在所有的运行代码中包括异常处理程序是很重要的。此外,已处理的异常和未处理的异常应该始终将提供的可能有助于黑客攻击的信息减到最少。对于已处理的异常,需要在错误消息中权衡:对无经验的用户有帮助,但不要向无道德的黑客透露过多信息。

对于未处理的异常,应该通过将编译元素(在 Web.config 文件中)的调试属性设置为 false,并将 customErrors 元素的模式属性设置为 On 或 RemoteOnly,确保将为黑客提供的帮助减到最少。例如,看一看以下内容:

<compilation defaultLanguage="c#"
debug="false"
/>
<customErrors mode="RemoteOnly"
/>

 

RemoteOnly 设置将确保从本机访问站点的用户将收到信息丰富的错误消息,而从远程位置访问站点的用户将收到未披露有关异常的有用信息的一般错误消息。使用 On 设置可使所有用户(包括本地用户)看到一般错误消息。决不可在运行环境中使用 Off 设置。

Back to top

总结

SQL Injection 攻击与应用程序开发人员紧密相关,因为它们可用于侵入据推测是安全的系统并窃取、更改或销毁数据。不管使用的是哪个版本的 ASP.NET,您遭受这些攻击的损害都太容易了。实际上,您甚至无需使用 ASP.NET 就有可能受到 SQL Injection 攻击。使用用户输入的数据查询数据库的任何应用程序(包括 Windows 窗体应用程序)都是 Injection 攻击的潜在目标。

使您免受 SQL Injection 攻击并非很难。不受 SQL Injection 攻击的应用程序验证所有的用户输入并对其进行无害处理,从不使用动态 SQL,使用几乎无权限的帐户执行,哈希或加密其机密信息,并显示几乎不会对黑客披露任何有用信息的错误消息。通过按照多层预防方法,您可以确信在一道防御被规避时,您仍是受保护的。[有关测试应用程序是否存在 Injection 漏洞的信息,请参阅边栏“Injection 测试”。]

Back to top

Download Image NEW: Explore the sample code online! - or - 代码下载位置: SQLInjection.exe (153KB)
Figure
 
4
阻止 SQL Injection 攻击

原则 实现
决不信任用户输入 使用验证控件、正则表达式、代码等验证所有文本框输入
决不使用动态 SQL 使用参数化 SQL 或存储过程
决不使用管理员级帐户连接到数据库 使用有限访问帐户连接到数据库
不要以纯文本存储机密信息 加密或哈希密码和其他敏感数据;还应该加密连接字符串
异常应该泄露最少信息 不要在错误消息中披露太多信息;使用 customErrors 在未处理错误时显示最少的信息;将调试设置为 false

posted on 2007-07-21 08:19  new2008  阅读(927)  评论(0编辑  收藏  举报

导航