65.9K
CodeProject 正在变化。 阅读更多。
Home

保护您的数据:防止 SQL 注入

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.72/5 (20投票s)

2016 年 6 月 8 日

CPOL

9分钟阅读

viewsIcon

32846

保护您的数据:防止 SQL 注入

引言

多年来,自从我加入不同的技术社区论坛以来,我总是看到提问者/发帖者提供的代码容易受到 SQL 注入攻击。虽然有一些热心贡献者会继续指导初学者如何防范,但仍然有少数人会继续向提问者提供易受攻击的代码。这令人难过,因此我才 urge 写这篇文章。

我知道有无数的文章强调了防止 SQL 注入攻击的方法,但易受攻击的代码仍然随处可见,在各种论坛甚至文章和博客中。我认为这其中的一些主要原因是:

  1. 经验丰富的开发人员继续提供初学者会模仿的易受攻击的代码。
  2. 他们不理解他们正在编写的代码。
  3. 只要代码对他们有用,他们就不在乎代码本身。
  4. 他们害怕学习正确的方法,因为他们可能会破坏他们现有的“能运行”的代码。
  5. 他们只是懒惰。
  6. 是的,他们就是懒惰。

对于初学者来说,如果您被重定向到本文,那么您的代码一定出了问题。本文将介绍一些易受攻击的代码如何破坏您的数据以及如何防止它的例子。

什么是 SQL 注入?

摘自 文档:SQL 注入是一种代码注入技术,用于攻击数据驱动的应用程序,攻击者通过在输入字段中插入恶意的 SQL 语句来执行(例如,将数据库内容转储给攻击者)。这些攻击允许攻击者伪造身份、篡改现有数据、引起否认问题(如作废交易或更改余额)、允许完全披露系统上的所有数据、销毁数据或使其不可用,并成为数据库服务器的管理员。

嗯……什么?

图 1:那是我的狗,“Hugo” ;)

如果这对您来说没有意义,那么让我们在 ASP.NET 中看一个例子。假设我们有以下表数据

图 2:示例数据

示例 1

假设您想在名为“Field1”的列中搜索某些值,然后将结果显示在像 GridView 这样的数据控件中。在大多数情况下,您会看到下面的代码,它将从 SQL 数据库中搜索一些记录。

protected void btnSearch_Click(object sender, EventArgs e) {  
            SqlConnection conn = new SqlConnection
            (@"Data Source=ServerName\SQLEXPRESS;Initial Catalog=DemoDB;
            Integrated Security=SSPI;");
            SqlCommand cmd = new SqlCommand("Select * from GridViewDynamicData 
            where Field1= '" + txtSearch.Text +"'", conn);
            conn.Open();
            SqlDataAdapter ad = new SqlDataAdapter(cmd);
            DataTable dt = new DataTable();
            ad.Fill(dt);
            if(dt.Rows.Count > 0)
            {
                GridView1.DataSource = dt;
                GridView1.DataBind();
            }
            conn.Close();
}

上面的代码通常用于根据 TextBox 的值搜索数据库记录。它使用 ADO.NET 连接到数据库并针对 SQL Server 数据库执行 SQL。然后将结果填充到 DataTable 中,然后将其绑定到您的 GridView。在运行时,用户输入的值与 SQL string 动态合并,以创建有效的 SQL 命令,如下图所示。

图 3:显示 SQL 命令文本

如您在上面的文本可视化工具中所见,用户提供的“Test 1”值已与核心 SQL 合并以完成命令。运行上面的代码将为您提供预期的结果,如下所示。

图 4:输出

太棒了!应用程序运行得很顺利,您得到了预期的结果。现在,当黑客输入恶意值时,请看下图。

图 5:显示 SQL 命令文本

从上图可以看出,我只是在 TextBox 中输入了 **';Drop Table Members--**,然后将这些值附加到了核心 SQL 中。结果绝对是一个有效的 SQL 命令,它将在 SQL 数据库上执行,这可能导致删除您的 Members 表。第一个字符值中的单引号在 T-SQL 中代表一个 string 分隔符。后面的双破折号/连字符(--)字符基本上用于注释 SQL 中的前导文本,如果您允许用户在不管理这些字符的情况下输入它们,那么您的数据将面临风险。现在您可能想问黑客如何知道您的数据库表名?好吧,他们很可能不知道,但您应该考虑您的数据库表命名方式。它们必然是具有 common sense 的名称,反映了它们的用途,而且很快就能猜到它们是什么,尤其如果您使用的是ASPNETDB.mdf数据库,它是公开可用的。将数据库表名重命名为晦涩(非常难猜的名字)的名称并不能解决问题,因为有人可以轻松地使用随机 string 生成器。

示例 2

另一个常见的例子是验证来自数据库的用户凭据,使用以下代码

protected void btnLogin_Click(object sender, EventArgs e) {  
            SqlConnection conn = new SqlConnection
            (@"Data Source=ServerName\SQLEXPRESS;Initial Catalog=DemoDB;
            Integrated Security=SSPI;");
            SqlCommand cmd = new SqlCommand
            ("Select * from SYSUser where LoginName= '" + txtUserName.Text + "' 
            and PasswordEncryptedText='" + txtPassword.Text + "'", conn);
            conn.Open();
            SqlDataAdapter ad = new SqlDataAdapter(cmd);
            DataTable dt = new DataTable();
            ad.Fill(dt);
            if (dt.Rows.Count > 0)
                Response.Write("OK");
            else
                Response.Write("Failed");

            conn.Close();
}

同样,如果您提供正确的有效凭据,上面的代码将正常工作。如果 LoginNamePassword 值与数据库中的某一行匹配,它将显示 OK,否则显示 Failed。现在,如果我在 LoginName TextBoxPassword TextBox 中都输入 **' or 'hacked' = 'hacked**,那么您的 SQL 命令查询将导致以下结果

图 6:显示 SQL 命令文本

附加这些恶意值将始终匹配至少一行,因此 dt.Rows.Count 将始终为 > 0,从而允许黑客进入您的安全站点。

另一种情况是,如果黑客知道您的 LoginName,例如您的 LoginName 是“Admin”,他们可以简单地附加值 **'--**,您的 SQL 查询现在将变成这样

图 7:显示 SQL 命令文本
Select * from SYSUser where LoginName= 'Admin'--' and PasswordEncryptedText=''  

如果您注意到,由于注入的 SQL 语法,您的 WHERE 子句中的剩余条件已被注释掉,从而忽略了剩余的条件。所以,如果您的数据库中确实存在 LoginNameAdmin”,那么您的 dt.Rows.Count 将为 > 0,从而授予黑客访问您网站的权限。

图 8:输出

上图中返回的结果是“OK”。这意味着黑客轻易绕过了您的身份验证,并能够访问您的安全页面。一旦他们进入您的安全站点,他们就有可能开始篡改您的站点,或者破坏您数据库中的某些数据,或者使某些数据消失。

上面演示的例子只是 SQL 注入攻击的一些典型例子。其他攻击途径可以是来自表单、Cookie 和查询字符串的值,其中可以自动向您的核心 SQL 命令注入额外的 SQL 命令来改变行为。

解决方案

仅供您参考,转义和替换 string 中的字符并不能完全防止 SQL 注入攻击。为了防止 SQL 注入攻击,请使用参数化查询。这是防止此类攻击的理想方法。

使用参数化查询

ADO.NET 参数化查询是一种使用占位符作为参数的查询,参数值在执行时提供。当参数化查询发送到 SQL Server 时,它们通过系统存储过程 sp_executesql 执行。

在示例 1 中,我们可以将代码重写为

protected void btnSearch_Click(object sender, EventArgs e) {  
            DataTable dt = new DataTable();
            using (SqlConnection sqlConn = new SqlConnection
            (ConfigurationManager.ConnectionStrings["DBConnection"].ConnectionString)){
                string sql = "SELECT * FROM GridViewDynamicData WHERE Field1 = @SearchText";
                using(SqlCommand sqlCmd = new SqlCommand(sql,sqlConn)){
                    sqlCmd.Parameters.AddWithValue("@SearchText", txtSearch.Text);
                    sqlConn.Open();
                    using(SqlDataAdapter sqlAdapter = new SqlDataAdapter(sqlCmd)){
                        sqlAdapter.Fill(dt);
                    }
                }
            }

            if(dt.Rows.Count > 0){
                GridView1.DataSource = dt;
                GridView1.DataBind();
            }
 }

如果您注意到,上面的代码有一些变化,使代码更干净、可维护和安全。首先是使用 using 语句包装 SqlConnectionSqlCommandSqlDataAdapter 对象。由于这些对象实现了 IDisposable,将它们放在 using 语句中将在对象使用后自动释放和关闭其连接。换句话说,如果我们把代码放在 using 语句中,我们就不需要显式地在代码中释放对象,因为 using 语句会处理它。另外,using 语句在后台使用 try 和 finally 块,它会在 finally 块中释放 IDisposable 对象。第二是将连接字符串移到 web.config 文件中,并使用 System.Configuration.ConfigurationManager 类进行引用。第三是将 SQL 查询移到一个名为“sql”的单独 string 变量中。在该查询中,您将看到参数:@SearchText,它取代了连接的 TextBox 值。所有 SQL 参数都应以 @ 符号作为前缀。您的 SQL 查询中声明的每个参数都应该有一个对应的值,因此在这种情况下,我们添加了 sqlCmd.Parameters.AddWithValue("@SearchText", txtSearch.Text) 这行。 SqlParameterCollection.AddWithValue 方法基本上是将一个值添加到 SqlParameterCollection 的末尾。

同样,SQL 参数查询将被发送到 SQL Server,然后由 sp_executesql 命令执行。根据我们的例子,查询将如下所示

exec sp_executesql N'SELECT * FROM GridViewDynamicData 
WHERE Field1 = @SearchText', N'@SearchText varchar(50)',@SearchText='Test 3'  

当命令执行时,参数和查询文本被分开处理。因此,string 值可能包含的所有 SQL 语法都将被视为字面 string 的一部分,而不是 SQL 语句的一部分。这实际上就是 SQL 注入被阻止的方式。

使用存储过程

如果您出于某些原因不想将 SQL 查询嵌入到 C# 代码中,您还可以使用带参数查询的存储过程。一个例子与我之前演示的例子差不多,除了您只需要将 SqlCommandCommandType 设置为 StoredProcedure,并将您的存储过程名称作为 CommandText 提供。

DataTable dt = new DataTable();  
using (SqlConnection sqlConn = new SqlConnection
(ConfigurationManager.ConnectionStrings["DBConnection"].ConnectionString)){  
    string sql = "YourStoredProcedureName";
    using (SqlCommand sqlCmd = new SqlCommand(sql, sqlConn)){
        sqlCmd.CommandType = CommandType.StoredProcedure;
        sqlCmd.Parameters.AddWithValue("@SearchText", txtSearch.Text);
        sqlConn.Open();
        using (SqlDataAdapter sqlAdapter = new SqlDataAdapter(sqlCmd)){
            sqlAdapter.Fill(dt);
        }
    }
}

使用对象/关系映射框架 (ORM)

ORM,如 Microsoft Entity Framework 和 NHibernate,在执行操作时会发出参数化 SQL 语句。因此,使用它们将为您提供针对 SQL 注入攻击的保护,而无需您额外付出努力。使用这些数据访问机制还可以为您节省大量麻烦,因为您可以直接针对概念应用程序模型进行编程,而不是直接针对数据库进行编程。因此,您不必处理那些拼写错误和 SQL 语法。以下是一个代码片段示例:

using (DemoDBEntities db = new DemoDBEntities()){  
                var result = db.GridViewDynamicData.Where(o => o.Field1.Equals(txtSearch.Text));
                if (result.Any())
                    return result.ToList();
}

其他技巧

  • 在将值传递给参数之前,请确保对所有输入类型进行验证。这是因为如果您的 SQL 参数类型期望一个数字值,而您传递了一个 string 类型,那么您的应用程序将抛出错误。
  • 请确保验证输入控件中要输入的范围、预期的值和字符长度。

结语

现在您已经了解了 SQL 注入攻击以及它如何可能危害您的网站和数据;我希望您开始使用参数化查询来保护您的站点免受此类攻击。所以别再懒惰了,因为您真的没有借口。

对于论坛贡献者,尤其是经验丰富的贡献者,请养成习惯,当您看到容易受到 SQL 注入攻击的代码时,向初学者提供参数化查询代码。我们是一个社区,所以让我们帮助大家走上正确的道路。

再次强调,养成始终使用参数化查询的习惯。希望有人觉得这篇帖子有用。

© . All rights reserved.