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

了解 SQL 注入并创建防 SQL 注入的 ASP.NET 应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (38投票s)

2012 年 9 月 14 日

CPOL

7分钟阅读

viewsIcon

143631

downloadIcon

2196

本文将讨论什么是 SQL 注入,它如何影响我们网站的安全性,以及应该采取哪些步骤来创建一个防 SQL 注入的 ASP.NET 应用程序。

引言

本文将讨论什么是 SQL 注入,它如何影响我们网站的安全性,以及应该采取哪些步骤来创建一个防 SQL 注入的 ASP.NET 应用程序。

背景

作为 ASP.NET 开发者,我们经常编写动态 SQL 来执行一些数据库操作。这些动态 SQL 在某些情况下可能是通过将字符串与用户输入拼接而成的。如果我们不验证用户输入并照单全收,那么这种场景会带来非常严重的 SQL 注入问题。

SQL 注入是一种攻击,用户会在网站输入一些 SQL 代码作为输入,从而导致创建开发者不打算编写的 SQL 语句。这些 SQL 语句可能导致未经授权的访问、泄露秘密用户信息,有时甚至可能清除服务器上的所有数据。

使用代码

了解 SQL 注入

让我们通过查看那些会使应用程序容易受到 SQL 注入攻击的不良编码实践,进一步探讨这个问题。让我们创建一个简单的表,其中包含用户的用户名和密码以进行身份验证。

现在我将创建一个小页面,让用户输入他的 login 凭据并根据用户表进行验证。

注意:密码绝不应以明文形式存储。此表包含明文密码仅为了本文的简单性。

我将用于验证用户的实际代码包含通过字符串拼接创建的动态 SQL 。如果 userid password 在数据库中找到,此代码将返回 true,否则返回 false。  

public bool IsUserAuthenticated_Bad(string username, string password)
{
    DataTable result = null;
    try
    {
        using (SqlConnection con = new SqlConnection(ConfigurationManager.ConnectionStrings["SampleDbConnectionString1"].ConnectionString))
        {
            using (SqlCommand cmd = con.CreateCommand())
            {
                cmd.CommandType = CommandType.Text;
                cmd.CommandText = "select userID from Users where userID = '" + username + "' and password = '" + password + "'";
                
                using (SqlDataAdapter da = new SqlDataAdapter(cmd))
                {
                    result = new DataTable();
                    da.Fill(result);

                    //check if any match is found
                    if (result.Rows.Count == 1)
                    {
                        // return true to indicate that userID and password are matched.
                        return true;
                    }
                }
            }
        }
    }
    catch (Exception ex)
    {
        //Pokemon exception handling
    }

    //user id not found, lets treat him as a guest        
    return false;
}

对于所有普通用户,此代码将正常工作。我甚至可以使用 userid sampleuser password samplepwd 进行测试,并且它会正常工作。对于除此以外的任何其他数据,它都应该显示身份验证失败(因为这是表中唯一的记录)。用于测试此输入的查询将是

select userID from Users where userID = 'sampleuser' and password = 'samplepwd'

现在让我们尝试向此页面注入一些 SQL 。让我将 hacker' or 1=1-- 作为 username ,并在 password 中输入任何内容(甚至留空)。现在,由此产生的 SQL 将变为

select userID from Users where userID = 'hacker' or 1=1--' and password = ''

现在,当我们执行此查询时,1=1 子句将始终返回 true(并且密码检查被注释掉了)。现在,无论用户输入了什么数据,此 SQL 都将返回一行,使此函数返回 true,进而验证用户。因此,我现在所做的就是,即使我不知道有效的用户凭据,我也获得了对网站的访问权限。

我们将很快详细探讨如何解决这个问题。但在那之前,让我们再看一个 SQL 注入的例子,以便更深入地理解。

在此第二个示例中,我们将假设恶意用户以某种方式获得了数据库 schema,然后他正试图操纵应用程序以查找一些机密信息。假设我们有一个页面,旨在显示分配给组织中用户的所有产品。

让我们从查看 Product 表开始。

现在让我们看看检索此数据的代码

public DataTable GetProductsAssigner_Bad(string userID)
{
    DataTable result = null;
    try
    {
        using (SqlConnection con = new SqlConnection(ConfigurationManager.ConnectionStrings["SampleDbConnectionString1"].ConnectionString))
        {
            using (SqlCommand cmd = con.CreateCommand())
            {
                cmd.CommandType = CommandType.Text;
                cmd.CommandText = "select * from Products where AssignedTo = '" + userID + "'";

                using (SqlDataAdapter da = new SqlDataAdapter(cmd))
                {
                    result = new DataTable();
                    da.Fill(result);
                }
            }
        }
    }
    catch (Exception ex)
    {
        //Pokemon exception handling
    }

    //user id not found, lets treat him as a guest        
    return result;
}

现在,如果我用正确的数据调用此函数(就像普通用户会做的那样),它将显示结果。也就是说,如果我为 sampleuser 调用此页面,结果查询将是

select * from Products where AssignedTo = 'sampleuser'

现在,让我将此查询字符串与此页面一起使用:userID=' UNION SELECT 0 AS Expr1, password, userID FROM Users -- 。一旦此数据与当前代码一起使用,它将显示数据库中的所有用户名和密码。一旦我们查看此输入的最终查询,原因将非常清楚。

select * from Products where AssignedTo = '' UNION SELECT 0 AS Expr1, password, userID FROM Users --

现在我们看到,字符串拼接的动态 SQL 容易受到 SQL 注入。通过注入 SQL 可能会产生许多其他问题。想象一下,注入的 SQL 正在删除表或截断所有表。在这种情况下,问题将是灾难性的。

如何防止 SQL 注入

ASP.NET 为我们提供了防止 SQL 注入的优秀机制。为了防止我们网站上的注入攻击,应该遵循一些经验法则。

  1. 用户输入绝不应被信任。它应该始终被验证
  2. 动态 SQL 绝不应通过字符串拼接创建。
  3. 始终优先使用存储过程。 
  4. 如果需要动态 SQL ,应与参数化命令一起使用。
  5. 所有敏感和机密信息都应加密存储。
  6. 应用程序绝不应使用/以管理员权限访问数据库。 

用户输入绝不应被信任。它应该始终被验证

这里的基本经验法则是用户输入绝不应被信任。首先,我们应该对所有输入字段应用过滤器。如果任何字段应该接受数字,那么我们绝不应该接受其中的字母。其次,所有输入都应该根据正则表达式进行验证,以便没有 SQL 字符和 SQL 命令关键字传递给数据库。

此过滤和验证都应在客户端使用 JavaScript 完成。这对于普通用户来说就足够了。恶意用户仍然可以绕过客户端验证。因此,为了遏制这一点,所有验证也应该在服务器端完成。

动态 SQL 绝不应通过字符串拼接创建。

如果我们有通过字符串拼接创建的动态 SQL ,那么我们总是面临获得一些不应该与应用程序一起使用的 SQL 的风险。建议完全避免字符串拼接。

始终优先使用存储过程。

存储过程是执行数据库操作的最佳方式。如果我们使用存储过程,我们总是可以确定不会生成任何不良 SQL 。让我们为 login 页面所需的数据库访问创建一个存储过程,并看看使用存储过程执行数据库操作的正确方法是什么。

CREATE PROCEDURE dbo.CheckUser 	
	(
	@userID varchar(20),
	@password varchar(16)
	)
AS
	select userID from Users where userID = @userID and password = @password
	RETURN

现在,让我们在代码中使用这个存储过程,得到一个好的版本。

public bool IsUserAuthenticated_Good(string username, string password)
{
    DataTable result = null;
    try
    {
        using (SqlConnection con = new SqlConnection(ConfigurationManager.ConnectionStrings["SampleDbConnectionString1"].ConnectionString))
        {
            using (SqlCommand cmd = con.CreateCommand())
            {
                cmd.CommandType = CommandType.StoredProcedure;
                cmd.CommandText = "CheckUser";
                cmd.Parameters.Add(new SqlParameter("@userID", username));
                cmd.Parameters.Add(new SqlParameter("@password", password));

                using (SqlDataAdapter da = new SqlDataAdapter(cmd))
                {
                    result = new DataTable();
                    da.Fill(result);

                    //check if any match is found
                    if (result.Rows.Count == 1)
                    {
                        // return true to indicate that userID and password are matched.
                        return true;
                    }
                }
            }
        }
    }
    catch (Exception ex)
    {
        //Pokemon exception handling
    }

    //user id not found, lets treat him as a guest        
    return false;
}

如果需要动态 SQL,应与参数化命令一起使用。

如果我们仍然发现自己在代码中需要动态 SQL ,那么参数化命令是执行此类动态 SQL 业务的最佳方式。通过这种方式,我们总是可以确保不会生成任何不良 SQL 。让我们为产品页面所需的数据库访问创建一个参数化命令,并看看执行数据库操作的正确方法是什么。

public DataTable GetProductsAssigner_Good(string userID)
{
    DataTable result = null;
    try
    {
        using (SqlConnection con = new SqlConnection(ConfigurationManager.ConnectionStrings["SampleDbConnectionString1"].ConnectionString))
        {
            using (SqlCommand cmd = con.CreateCommand())
            {
                cmd.CommandType = CommandType.Text;
                cmd.CommandText = "select * from Products where AssignedTo = @userID";
                cmd.Parameters.Add(new SqlParameter("@userID", userID));

                using (SqlDataAdapter da = new SqlDataAdapter(cmd))
                {
                    result = new DataTable();
                    da.Fill(result);
                }
            }
        }
    }
    catch (Exception ex)
    {
        //Pokemon exception handling
    }

    //user id not found, lets treat him as a guest        
    return result;
}

所有敏感和机密信息都应加密存储。

所有敏感信息都应该加密存储在数据库中。这样做的好处是,即使用户以某种方式获得了数据,他也只能看到加密的值,这对于不了解应用程序使用的加密技术的人来说并不容易使用。

应用程序绝不应使用/以管理员权限访问数据库。

这将确保即使通过一些注入将不良 SQL 传递到数据库,数据库也不会允许任何灾难性操作,例如删除表。

注意:请参阅所附的示例应用程序,了解 SQL 注入的工作示例以及如何使用参数化命令和存储过程来遏制它们。

值得关注的点:

这是一篇关于 SQL 注入的非常基础的文章。我特别关注 ASP.NET 应用程序,但相同的概念也适用于任何 ADO.NET 应用程序。本文面向那些对 SQL 注入知之甚少或一无所知,并希望创建防 SQL 注入应用程序的初学者。我希望这能有所帮助。

历史

  • 2012 年 9 月 14 日:第一个版本
  • 2013 年 1 月 9 日:示例应用程序中的错误修复。
© . All rights reserved.