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






4.87/5 (38投票s)
本文将讨论什么是 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 注入的优秀机制。为了防止我们网站上的注入攻击,应该遵循一些经验法则。
- 用户输入绝不应被信任。它应该始终被验证
- 动态 SQL绝不应通过字符串拼接创建。
- 始终优先使用存储过程。
- 如果需要动态 SQL,应与参数化命令一起使用。
- 所有敏感和机密信息都应加密存储。
- 应用程序绝不应使用/以管理员权限访问数据库。
用户输入绝不应被信任。它应该始终被验证
这里的基本经验法则是用户输入绝不应被信任。首先,我们应该对所有输入字段应用过滤器。如果任何字段应该接受数字,那么我们绝不应该接受其中的字母。其次,所有输入都应该根据正则表达式进行验证,以便没有 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 日:示例应用程序中的错误修复。


