了解 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 日:示例应用程序中的错误修复。