构建安全的数据访问-异常管理(八)
异常条件可能会由配置错误、代码中的错误或恶意输入引起。如果没有正确的异常管理,这些条件可能会透露有关数据源位置和特性的敏感信息,以及有价值的连接详细信息。下面的建议适用于数据访问代码:
•捕获和记录 ADO.NET 异常。
•确保数据库连接总是处于断开状态。
•在 ASP.NET 应用程序中使用一般错误页面。
捕获和记录 ADO.NET 异常
将数据访问代码放在 try/catch 块中并处理异常。在编写 ADO.NET 数据访问代码时,由 ADO.NET 生成的异常类型取决于数据提供程序。例如:
•SQL Server .NET Framework 数据提供程序生成 SqlException,
•OLE DB .NET Framework 数据提供程序生成 OleDbException,
•ODBC .NET Framework 数据提供程序生成 OdbcException。
捕获异常
下面的代码使用 SQL Server .NET Framework 数据提供程序,并显示应该如何捕获类型为 SqlException 的异常。
try { // 数据访问代码 } catch (SqlException sqlex) // 比较具体 { } catch (Exception ex) // 比较一般 { }
记录异常
还应该记录来自 SqlException 类的详细信息。此类公开那些包含异常条件详细信息的属性。这些属性包括 Message 属性(用来描述错误)、Number 属性(用来唯一标识错误类型)以及 State 属性(其中包含其他信息)。State 属性通常用来指示特定错误条件出现的具体位置。例如,如果某个存储过程从多个行中生成同一错误,则 State 属性可以指出错误出现的具体位置。最后,Errors 集合中包含 SqlError 对象,这些对象提供详细的 SQL 服务器错误信息。
下面的代码片断显示了如何通过使用 SQL Server .NET Framework 数据提供程序来处理 SQL Server 错误条件:
using System.Data; using System.Data.SqlClient; using System.Diagnostics; // 由数据访问层 (DAL) 组件公开的方法 public string GetProductName( int ProductID ) { SqlConnection conn = new SqlConnection( "server=(local);Integrated Security=SSPI;database=products"); // 将所有的数据访问代码包含在 try 块中 try { conn.Open(); SqlCommand cmd = new SqlCommand("LookupProductName", conn ); cmd.CommandType = CommandType.StoredProcedure; cmd.Parameters.Add("@ProductID", ProductID ); SqlParameter paramPN = cmd.Parameters.Add("@ProductName", SqlDbType.VarChar, 40 ); paramPN.Direction = ParameterDirection.Output; cmd.ExecuteNonQuery(); // 在该方法返回之前先执行 finally 代码 return paramPN.Value.ToString(); } catch (SqlException sqlex) { // 处理数据访问异常条件 // 记录具体的异常详细信息 LogException(sqlex); // 将当前异常包装在一个相关性更强的 // 外部异常中,并重新引发新异常 throw new Exception( "Failed to retrieve product details for product ID: " + ProductID.ToString(), sqlex ); } finally { conn.Close(); // 确保连接处于断开状态 } } // Helper 例程,该例程将 SqlException 详细信息记录到 // 应用程序事件日志中 private void LogException( SqlException sqlex ) { EventLog el = new EventLog(); el.Source = "CustomAppLog"; string strMessage; strMessage = "Exception Number :" + sqlex.Number + "(" + sqlex.Message + ") has occurred"; el.WriteEntry( strMessage ); foreach (SqlError sqle in sqlex.Errors) { strMessage = "Message:" + sqle.Message + " Number:" + sqle.Number + " Procedure:" + sqle.Procedure + " Server:" + sqle.Server + " Source:" + sqle.Source + " State:" + sqle.State + " Severity:" + sqle.Class + " LineNumber:" + sqle.LineNumber; el.WriteEntry( strMessage ); } }
确保数据库连接总是处于断开状态
如果发生异常,一定要断开数据库连接,并释放其他所有受限制的资源。使用 finally 块或 C# using 语句,可以确保无论是否发生了异常条件,都会断开数据库连接。上面的代码阐释了 finally 块的用法。还可以按如下方式使用 C# using 语句:
using ((SqlConnection conn = new SqlConnection(connString))) { conn.Open(); // 在以下情况下将断开连接:生成异常或者控制流 // 通常会离开 using 语句的使用范围 }
在 ASP.NET 应用程序中使用一般错误页面
如果您的数据访问代码由 ASP.NET Web 应用程序或 Web 服务调用,则应该对 <customErrors> 元素进行配置,以防异常详细信息传播回到最终用户。还可以通过使用该元素来指定一般错误页面,如下所示。
<customErrors mode="On" defaultRedirect="YourErrorPage.htm" />
对于生产服务器设置 mode="On"。只有在发布之前开发和测试软件时才使用 mode="Off"。如果不这样做,将导致向最终用户返回大量错误信息(如图 14.4 中显示的信息)。这些信息可能包含数据库服务器的名称、数据库名称和连接凭据。
图 14.4
详细的异常信息会透露敏感数据
图 14.4 还显示了数据访问代码中接近导致异常的行的大量漏洞。特别是:
•连接字符串是硬编码的。
•特权极高的 sa 帐户用于连接到数据库。
•sa 帐户有一个弱密码。
•SQL 命令的构造容易受到 SQL 注入攻击;输入内容未进行验证,代码不使用参数化存储过程。