ADO.NET 基础(防SQL注入)
与数据库交互的 Web 应用程序中最严重的风险之一:SQL 注入攻击。
SQL 注入是应用程序开发人员未预期的把 SQL 代码传入到应用程序的过程,它由于应用程序的糟糕设计而使攻击成为可能,并且只有那些直接使用用户提供的值构建 SQL 语句的应用程序才会受影响。
问题在于命令时如何被执行的。SQL 语句通过字符串的构造技术动态创建,文本框的值被直接复制到字符串中,他可能是这样的:
string sql = "SELECT * FROM Orders where CustomerID = '" + txtID.Text + "'";
在这个示例中,用户可以篡改 SQL 语句,通常,攻击的第一个目标是得到错误信息。如果错误没有被恰当处理,底层的信息就会暴露给攻击者。现在若是在文本框中输入:ALFKI' OR '1'='1,再看看这条 SQL 语句,它现在是这样的:
string sql = "SELECT * FROM Orders where CustomerID = 'ALFKI' OR '1'='1'";
这样产生的后果是没有显示当前用户的特定信息,却向攻击者提供了全部资料,如果屏幕上显示的是敏感信息,如社会保险号,生日或者信用卡资料等,就会带来严重的问题!
还可以进行更复杂的攻击!例如攻击者可以利用两个连接号(--)注释掉 SQL 语句剩余部分,虽然这样的攻击只限于SQL Server,不过对于其他类型的数据库也有等效的办法。另外,攻击者还可以执行含有任意 SQL 语句的批处理命令,对于 SQL Server,攻击者只需加上分号(;),攻击者用这样的方式还可以删除其他表的内容。甚至调用 SQL Server 的系统存储过程 xp_cmdshell 在命令行执行任意的程序。
下面是攻击者在文本框中输入的,他的目的是删除 Customers 表的全部行:ALFKI' ; DELETE * FROM Customers,得到的 SQL 语句是这样:
"SELECT * FROM Orders where CustomerID = 'ALFKI';DELETE * FROM Customers"
如何防止 SQL 注入攻击?
预防手段:
- 使用 TextBox.MaxLength 属性防止用户输入过长的字符,这样减少了贴入大量的脚本的可能性
- 使用 ASP.NET 验证控件锁定错误的数据
- 限制错误信息给出的提示,捕获到异常时只显示一些通用的信息,而不是显示 Exception.Message 属性中的信息,它会暴露出系统的攻击点
- 更为重要的是,一定要小心去掉特殊字符,比如将单引号替换为两个单引号
- 最好的解决方法是使用参数化的命令或者使用存储过程执行转义以防止 SQL 注入攻击
使用参数化命令:
参数化命令是在 SQL 文本中使用占位符的命令,占位符表示需要动态替换的值,它们通过 Command 对象的 Parameters 集合来传送。
例如下面这条SQL语句:
SELECT * FROM Customers where CustomerID = 'ALFKI'
可以写成这样:
SELECT * FROM Customers where CustomerID = @CustID
占位符随后单独提供并被自动编码
为每个参数创建一个 Parameter 对象,这些对象被加入到 Command.Parameters 集合中。下面的示例重写前面的代码防止可能的 SQL 注入攻击:
string connStr = WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
SqlConnection conn = new SqlConnection(connStr);
// 这里使用参数化的 SQL 语句
string sql = "SELECT * FROM Customers where CustomerID = @CustID";
SqlCommand cmd = new SqlCommand(sql, conn);
// 这里配置 Command.Parameters 集合
cmd.Parameters.AddWithValue("@CustID", txtID.Text);
conn.Open();
SqlDataReader reader = cmd.ExecuteReader();
若在修改后的页面上再次尝试 SQL 注入攻击,将得不到任何记录。因为没有客户 ID 值与 文本框输入的 ALFKI ' OR '1' = '1' 相等的订单项,这正是我们期待的结果。
调用存储过程
参数化命令时调用完整功能存储过程的诸多命令中的一小部分。
存储过程当然是保存在数据库上的批次执行的一条或多条SQL语句。它们是良好的逻辑封装体,可以接收(输入参数)和返回(输出参数)数据。
存储过程有很多优点:
- 更易于维护:例如,你可以优化存储过程中的命令而不必重新编译使用它的程序。
- 可以更安全地使用数据库:例如,可以让执行 ASP.NET 程序的 Windows 账号可以执行数据库存储过程,但不能访问基表。
- 可以提升性能:因为存储过程是多条语句的集合体,访问一次数据库可以做很多事情,如果数据库在其他计算机(不是web服务器)上,可以极大的减少执行复杂任务的总时间。
我们通过一个较为完整的示例来学习这一过程,向 Northwind 数据库添加一个存储过程:
create procedure InsertEmployee
@TitleOfCourtesy varchar(25),
@LastName varchar(20),
@FirstName varchar(10),
@EmployeeID int output
as
insert into Employees(TitleOfCourtesy,LastName,FirstName,HireDate)
values(@TitleOfCourtesy,@LastName,@FirstName,GetDate());
Set @EmployeeID = @@identity
这个存储过程有3个输入参数,1个输出参数。顺便一提,如果不使用存储过程的输出参数功能,要从刚刚插入的记录中获得自动生成的标识会是一件很麻烦的事。接着,我们创建一个程序来调用存储过程:
protected void Page_Load(object sender, EventArgs e)
{
string connStr = WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
SqlConnection conn = new SqlConnection(connStr);
// 调用存储过程,必须指定 Command.CommandType
SqlCommand cmd = new SqlCommand("InsertEmployee", conn);
cmd.CommandType = System.Data.CommandType.StoredProcedure;
// 向存储过程传递参数
// 需要精确指定【数据类型】和【参数的大小】以便和数据库中的细节相匹配
// 使用参数的 Value 属性进行赋值
cmd.Parameters.Add(new SqlParameter("@TitleOfCourtesy",SqlDbType.NVarChar,25));
cmd.Parameters["@TitleOfCourtesy"].Value = Title;
cmd.Parameters.Add(new SqlParameter("@LastName", SqlDbType.NVarChar, 20));
cmd.Parameters["@LastName"].Value = LastName;
cmd.Parameters.Add(new SqlParameter("@FirstName", SqlDbType.NVarChar, 10));
cmd.Parameters["@FirstName"].Value = FirstName;
// 【输出参数】也使用相同的方式添加
// 但是必须指定它的 Direction 属性为 OutPut
cmd.Parameters.Add(new SqlParameter("@EmployeeID", SqlDbType.Int, 4));
cmd.Parameters["@EmployeeID"].Direction = ParameterDirection.Output;
// 执行数据库命令
using (conn)
{
conn.Open();
int rtv = cmd.ExecuteNonQuery();
Label1.Text = string.Format("Inserted <b>{0}</b> record(s)<br />", rtv);
// 获取存储过程的输出参数
int empID = (int)cmd.Parameters["@EmployeeID"].Value;
Label1.Text += "New id: " + empID.ToString();
}
}
Parameters 集合的一个方便的方法时 AddWithValue()。该方法接收参数名及其值但不包括数据类型的信息,而是根据提供的数据猜测数据类型。显然,这对输出参数无效,因为你压根不会为输出参数提供值。