精通 Oracle 的 .NET 应用程序开发


基于 Oracle 数据库开发安全的 .NET 应用程序
作者:John Paul Cook

了解如何在您的 .NET 应用程序中充分利用 Oracle 内建的安全特性。

本文相关下载:
 示例代码
 用于 Windows 的 Oracle 数据库 10 g
 Oracle Data Provider for .NET

 查看完整的“精通 Oracle 的 .NET 应用程序开发”索引

随着对安全性重视程度的不断提高,各公司对安全性的认识已经超越了仅限于拒绝非法访问的层面而变得越发成熟。国际、国内、州和省的法律法规对安全性的要求也越来越细化。仅保护数据库免受未授权的访问已不能满足要求。还要求保留审计线索,以显示哪些用户在数据库中执行了哪些操作。

幸运的是,选择 Oracle 作为其数据库平台的 .NET 开发人员可以通过 Oracle Data Provider for .NET (ODP.NET) 利用 Oracle 的安全特性构建全面安全策略的基础。

在“精通 Oracle 的 .NET 应用程序开发”的这一部分中,我将介绍如何在考虑安全性的前提下设计应用程序,这包括

  • 如何在保持连接池所有优点的同时使用代理认证将实际最终用户 ID 传递给 Oracle 服务器。在使用连接池时,代理认证还允许对数据库服务器按用户认证。
  • 如何使用客户端标识符向 Oracle 服务器传递自定义标识符字符串或实际最终用户 ID
  • 如何在不使用 Oracle 用户 ID 和口令的情况下,使用 Oracle 的 Windows 认证进行数据库服务器认证,即一次性登录
  • 如何通过参数化查询防止 SQL 注入,从而消除这个对数据库安全性最大的威胁之一

您将有机会应用您在五个实验中学到的内容。这个五个实验从比较简单到较复杂难度不等。

本文假设您熟悉 Oracle Data Provider for .NET,并熟悉在 Visual Studio.NET 中创建工程(请参阅我先前的一篇文章 在 Oracle 数据库上构建 .NET 应用程序

代理认证

以下是摘自前一篇文章的代码,用于定义一个简单的 Oracle 连接字符串:

Dim oradb As String = 
  "Data Source=OraDb;User Id=scott;Password=tiger;" ' VB.NET 

string oradb = 
  "Data Source=OraDb;User Id=scott;Password=tiger;"; // C#
                            

应用程序经常使用类似这样的连接字符串提示用户输入用户 ID 和口令。Web 应用程序通常会这样做。它允许通过连接池提高性能,这是因为所有应用程序用户使用相同的 Oracle 证书进行连接,这也是对连接池的要求。从审计的角度来看,通过通用用户 ID 进行连接不是最理想的方式,这是因为所有的数据库操作都被记录成是由连接字符串中定义的用户而非实际用户所做出的。享受连接池的性能优势意味着您不得不忍受匿名现象。在数据库中,您将无法识别实际用户的身份。

代理认证将一个 Proxy User Id Proxy Password 添加到连接字符串中。换句话说,它传递了两个用户 ID,一个用于实际用户,一个用于聚集的用户(代理用户)。此特性使您能够维护一个中间层连接池,同时保持通过用户的 User Id 审计其操作的能力。留意 User Id Password 使用方式的细微差别,这取决于是否使用代理认证。当使用代理认证时,聚集的用户证书是通过 Proxy User Id Proxy Password 而不是 User Id Password 传递的。例如:

                               
Dim oradb As String = "Data Source=OraDb;User Id=ActualUser;Password=secret; 
    Proxy User Id=scott;Proxy Password=tiger;" ' VB.NET
string oradb = "Data Source=OraDb;User Id=ActualUser;Password=secret;
    Proxy User Id=scott;Proxy Password=tiger; "; // C#
                            

在实际的应用程序中,您决不会把实际的用户 ID 和口令硬编码。相反,您会提供文本框,用户可在其中输入用户 ID 和口令,并将其传递给连接字符串。当在 Windows 窗体或 Web 页面(在 Visual Studio 中称为 Web 表单)上为用户提供一个输入用户 ID 名和口令的对话框时,Microsoft 称之为表单认证。

在数据库中,代理认证创建第二个会话,称为轻型会话,它用于使数据库获知实际的用户。为此,数据库管理员必须为代理用户进行显式授权才能创建轻型连接。下面您将使用 SCOTT 作为我们的代理(聚集的)用户。

                               
alter user ActualUser grant connect through scott;

                            

您可以选择使连接字符串包含或排除实际用户的口令。如果在不提供口令的情况下使用实际用户,则连接将会成功。对于要认证的实际用户,您必须使用 Password 连接字符串属性。如果为用户提供无效的口令,则认证将会失败。

代理连接用于那些代表用户调用的数据库操作。当把代理连接返回连接池时,将终止轻型会话。

使用客户端标识符与在没有 Password 连接字符串属性的情况下使用代理认证相似。一个关键的区别是使用客户端标识符不会在数据库中创建第二个会话。另一个区别是任何字符串都可以用作客户端标识符。它不必对应于数据库用户的名称。要设置客户端标识符,可以在打开连接后将一个字符串值指定给连接对象的 ClientId 属性:

conn.ClientId = "SomeUser"  ' VB.NET
conn.ClientId = "SomeUser"; // C#
                            

该指定操作设置了用户会话的 CLIENT_IDENTIFIER 值。如果应用程序使用表单认证,则用户所输入的用户 ID 可用于设置 ClientId 的值。由于 ClientId USERENV 的一部分,因此可以将其与 Oracle 虚拟专用数据库结合使用来限制访问,即使该用户不是在 ALL_USERS 中定义的实际用户亦可如此。ClientId 还可用于提高可伸缩性。例如,当一个用户工作完成并将 ClientId 重置为下一个用户时,Web 应用程序可以使数据库连接保持打开的状态。

当您登录到 Windows 时,操作系统会记住您的 Windows 用户,并可以使用 Windows.Security.Principal 类在 .NET 应用程序中获取该用户。完成此项工作并为 Windows 用户设置 ClientId 的代码如下:

                               
Dim user As New WindowsPrincipal(WindowsIdentity.GetCurrent()) 
conn.ClientId = user.Identity.Name ' VB.NET
WindowsPrincipal user = new WindowsPrincipal(WindowsIdentity.GetCurrent());
conn.ClientId = user.Identity.Name; // C#
                            

通过这项技术,Oracle 数据库可以获知 Windows 用户。在没有 Password 连接字符串属性的情况下使用代理认证时,还可以将 Windows 用户身份通过 User Id 连接字符串属性传递给数据库。在这种情况下,如前文所述, User Id 连接字符串属性仅用于识别而非认证实际用户。

Windows 认证

本文讨论了使用 Oracle 用户 ID 将用户认证到数据库的过程。也可以使用 Windows 操作系统将用户认证到 Oracle,这就是所谓的一次性登录。我在 Windows IT Pro 杂志上的 在 Oracle 上执行 Windows 认证 一文中详细介绍了这部分内容。使用 Windows 认证需要修改连接字符串:

"Data Source=ORCL10g;User Id=/;"

斜线 (/) 向 Oracle 表明将使用 Windows 认证。因为只在建立 Oracle 数据库连接时才使用 Password 连接字符串属性,所以在这里把它删除了。如果在使用 Windows 认证时连接字符串中有 Password ,则将其忽略。

使用 Windows 认证时,Windows 用户必须属于一个在 Oracle 服务器上有权限的 Windows 组(如 ORA_DBA ),或者必须启用了外部认证。由于外部认证不如通过组成员访问安全,因此建议不要使用外部认证。

了解 SQL 注入攻击

到目前为止,本文已经介绍了如何跟踪谁在访问数据库。虽然这很有用,但却不如防止人们损害数据库的内容重要。对数据库应用程序的最大威胁之一是 SQL 注入。SQL 注入是指恶意用户用不安全的代码将 SQL 命令注入应用程序。SQL 注入漏洞是通过用户输入构建 SQL 语句的结果,并不是由于使用任何特定厂商的产品造成的。如果没有遵循编程安全措施,则所有厂商的所有 SQL 数据库都会有漏洞。

来看一个计算从事某种工作的员工数量的简单应用程序:

图 1

仔细查看用于构建数据库命令字符串的代码:

cmd.CommandText = "select count(ename) from emp where " _
+ "job = '" + TextBox1.Text + "'" ' VB.NET
cmd.CommandText = "select count(ename) from emp where " 
+ "job = '" + TextBox1.Text + "'"; // C#
                            

如果用户按照上面所示输入 CLERK,则数据库接收的命令文本如下:

                               
select count(ename) from emp where job = 'CLERK'
                            

只要用户不更改该逻辑,则一切运行正常。而允许在运行时基于用户输入构建 SQL 字符串则使用户能够更改 SQL 逻辑。假设用户决定不输入 CLERK 而是输入以下内容:

' or 1=1 --

这样就从根本上将查询的逻辑更改为:

                               
select count(ename) from emp where job = '' or 1=1 --'
                            

应用程序代码总是附加一个尾随的单引号。行内注释使 SQL 分析器忽略尾随的单引号。如果没有这种使用行内注释的技巧,则修改后的 SQL 会具有以下的无效语法:

                               
select count(ename) from emp where job = '' or 1=1 '
                            

用户修改后的 SQL 语法导致将表中的每一行进行计数。然而 where job = '' 本身会导致计数为零,而 or 1=1 导致对每行进行计数。

在前面的示例中,SQL 注入只是导致用户获得了与应用程序设计者初衷不符的结果。现在看一下用于认证用户的应用程序窗口:

图 2

下面是用于处理登录信息的代码:

cmd.CommandText = "select user_role from app_login where " _
+ "user_id = '" + TextBox1.Text + "' and " _
+ "password = '" + TextBox2.Text + "'" ' VB.NET
cmd.CommandText = "select user_role from app_login where " 
+ "user_id = '" + TextBox1.Text + "' and " 
+ "password = '" + TextBox2.Text + "'"; // C#
                            

假设有个恶意用户输入了以下内容:

admin' or 1=1 --

则得到的 SQL 为:

                               
select user_role from app_login
where user_id = 'admin' or 1=1 --' and password=''
                            

行内注释再次在破坏预期查询逻辑方面发挥了重要作用。-- 行内注释导致将查询字符串的其余部分当作注释处理,这样就使口令变得可有可无了。得到的查询字符串具有一个始终为真的 where 子句,这就使用户即使不提供口令也能以管理员身份登录。甚至有可能不用提供用户 ID 或口令就可以登录某些应用程序,这取决于基本查询字符串的结构。

防止 SQL 注入攻击

无论您怎么绞尽脑汁地验证用户输入,恶意用户还是有可能攻击成功,毕竟他们相当聪明。您也无法通过输入验证发现所有 SQL 注入企图。问题的根源并不是用户没有按您的预期输入 SQL 语法,而是把用户的输入当作 SQL 语法而不仅仅是字符串来处理了。

将输入视为字符串参数意味着不再将用户的输入作为 SQL 语句的一部分来处理,而只是将其视为一个字符串值传递给 SQL 查询。使用 OracleParameter 对象使恶意输入变得无害,因为将它解释为如下的形式:

                               
select user_role from app_login
where user_id = 'admin' or 1=1 --' and password = ''
                            

没有将双短划线视为行内注释,而只是将其视为文本字符串。用户输入没有成为所执行 SQL 查询语法的一部分。

要创建参数化的查询,如下修改查询字符串:

cmd.CommandText = "select user_role from app_login where " _
+ "user_id = :user_id and password = :password" ' VB.NET
cmd.CommandText = "select user_role from app_login where " 
+ "user_id = :user_id and password = :password"; // C#
                            

然后将 OracleParameter 对象实例化,并将其添加到 OracleParameters 集合。

                               
Dim p1 As New OracleParameter("dname", OracleDbType.Varchar2) ' VB.NET
p1.Value = TextBox1.Text
cmd.Parameters.Add(p1)
Dim p2 As New OracleParameter("loc", OracleDbType.Varchar2)
p2.Direction = ParameterDirection.Input  ' optional property
p2.Size = 13  ' optional property
p2.Value = TextBox2.Text
cmd.Parameters.Add(p2)
OracleParameter p1 = new OracleParameter("dname", OracleDbType.Varchar2);
p1.Value = textBox1.Text; // C#
cmd.Parameters.Add(p1);
OracleParameter p2 = new OracleParameter("loc", OracleDbType.Varchar2);
p2.Direction = ParameterDirection.Input; ' optional
p2.Size = 13; ' optional
p2.Value = textBox2.Text;
cmd.Parameters.Add(p2);
                            

其中有几个用于 OracleParameter 的构造符和多个 Parameters.Add 上的重载。此外,还有各种可以或必须设置的多个属性,这完全取决于您的查询。将参数传递给存储过程同样遵循以上介绍的编码样式。

实验 1:显示数据库用户

1. 首先向 Oracle.DataAccess 添加引用。如果需要详细的说明,请参见 在 Oracle 数据库上构建 .NET 应用程序

2. 向 Windows 窗体添加一个按钮控件和一个标签控件。

3. 添加代码,从 Oracle 数据库中检索数据并在窗体上显示结果。将代码放在按钮的一个单击事件处理程序中。这么做的最简单方式就是双击该按钮,它将为事件处理程序创建一个 stub。

4. 在 Public Class 声明之前添加 VB.NET Imports 语句,或在命名空间声明之前添加 C# using 语句。(注意:通过使用 可下载的代码示例 ,您可以不用实际键入任何代码就可以完成所有这些练习。)

Imports System.Data              ' VB.NET
Imports Oracle.DataAccess.Client ' ODP.NET Oracle managed provider

using System.Data;              // C#
using Oracle.DataAccess.Client; // ODP.NET Oracle managed provider

5. 此示例假定您拥有一个别名为 ORCL10g 的 tnsnames.ora。如果您不使用 tnsnames.ora,则必须修改连接字符串。我的 前一篇文章 提供了一个不依赖于 tnsnames.ora 的连接字符串。

Private Sub End Sub 语句之间添加单击事件处理程序代码的 VB.NET 版本。

Dim oradb As String = "Data Source=ORCL10g;User Id=scott;Password=tiger;"

Dim conn As New OracleConnection(oradb) ' VB.NET
conn.Open()

Dim cmd As New OracleCommand
cmd.Connection = conn
cmd.CommandText = "select user from user_users"
cmd.CommandType = CommandType.Text

Dim dr As OracleDataReader = cmd.ExecuteReader()
dr.Read()
label1.Text = dr.Item("user") ' or dr.Item(0)

conn.Dispose()

将以下的 C# 代码添加到按钮单击事件处理程序的 { 与 } 花括号之间。

string oradb = "Data Source=ORCL10g;User Id=scott;Password=tiger;";

OracleConnection conn = new OracleConnection(oradb); // C#
conn.Open();

OracleCommand cmd = new OracleCommand();
cmd.Connection = conn;
cmd.CommandText = "select user from user_users";
cmd.CommandType = CommandType.Text;

OracleDataReader dr = cmd.ExecuteReader();
dr.Read();
label1.Text = dr.GetString(0);

conn.Dispose();

6. 运行应用程序,并单击按钮。您将看到以下内容:

图 3
实验 2:添加代理认证

现在您已能够确保 Oracle 数据库服务器将您识别成哪个用户,下一步就是将代理认证添加到您的连接字符串中。

1. 创建一个新用户,作为在数据库服务器中以 SCOTT 身份登录时的实际用户。

create user ActualUser identified by secret;
grant connect to ActualUser;
alter user ActualUser grant connect through scott;

2. 修改连接字符串,使其包含 ActualUser 并使用代理认证:

Dim oradb As String = "Data Source=ORCL10g;" _
+ "User Id=ActualUser;Password=secret;" _
+ "Proxy User Id=scott;Proxy Password=tiger;" ' VB

string oradb = "Data Source=ORCL10g;"
                 + "User Id=ActualUser;Password=secret;"
                 + "Proxy User Id=scott;Proxy Password=tiger;"; // C#

3. 运行应用程序,并单击按钮。您将看到以下内容:

图 4
4. 从连接字符串中删除 Password=secret ,并再次运行应用程序。单击按钮。即使您没有提供口令,也将看到与前一步相同的结果。这是因为您没有认证实际的用户 — 您只是将实际用户的名称传递给了数据库。

5. 将 Password=wrongsecret 添加到连接字符串中,并再次运行应用程序。应用程序将不能运行并显示一个运行时错误,这是因为对实际用户的认证失败了。

实验 3:使用客户端标识符

1. 将实验 2 中的连接字符串按如下修改:

Dim oradb As String = "Data Source=ORCL10g;" _
+ "User Id=scott;Password=tiger;"  ' VB

string oradb = "Data Source=ORCL10g;"
                 + "User Id=scott;Password=tiger;";  // C#

2. 找到连接的 Open 方法,并在其下面一行中设置 ClientId 属性:

conn.Open()  ' VB
conn.ClientId = "SomeUser"  ' add this
conn.Open();  // C#
conn.ClientId = "SomeUser";  ' add this
                                  

3. 如下修改 CommandText 属性:

cmd.CommandText = 
"SELECT SYS_CONTEXT('USERENV','CLIENT_IDENTIFIER') FROM DUAL"  ' VB

cmd.CommandText = 
"SELECT SYS_CONTEXT('USERENV','CLIENT_IDENTIFIER') FROM DUAL"; // C#

4. 如下修改 DataReader 的 Item 属性:

Label1.Text = dr.Item(0)  ' VB

label1.Text = dr.GetString(0);  // C#

5. 运行应用程序,并单击按钮。您将看到以下内容:

图 5

实验 4:使用 Windows.Identity.Principal

1. 修改实验 3 中的代码,添加 Imports using 语句:

                                     
Imports System.Security.Principal  ' VB

using System.Security.Principal;  // C#

                                  

2. 创建 WindowsPrincipal 对象的一个实例,并使用它设置 ClientId 属性:

                                     
Dim user As New WindowsPrincipal(WindowsIdentity.GetCurrent())
conn.ClientId = user.Identity.Name  ' VB

WindowsPrincipal user = new WindowsPrincipal(WindowsIdentity.GetCurrent());
conn.ClientId = user.Identity.Name;  // C#

                                  

3. 运行应用程序,并单击按钮。您将看到类似下面的内容:

图 6

您要知道,我是在测试机器上以 Windows 用户 Winuser 登录到 ORAWIN 域的 。您将看到您的域名或机器名以及您的 Windows 用户名。

实验 5:参数化查询

1. 修改上一个实验中的窗体,添加两个标签和两个文本框。结果将如下所示:

图 7
2. 修改创建用于接收用户输入的查询字符串的代码:

cmd.CommandText = "select count(deptno) from dept where " _
+ "dname = '" + TextBox1.Text + "' and " _
+ "loc = '" + TextBox2.Text + "'"  ' VB

cmd.CommandText = "select count(deptno) from dept where " _
+ "dname = '" + textBox1.Text + "' and " _
+ "loc = '" + textBox2.Text + "'";  // C#

我们不创建和填充一个新表,而是使用 DEPT 表,并假设 DNAME 列包含用户 ID 且 LOC 列包含口令。

3. 运行应用程序,并输入以下有效输入:

输入 SALES 作为用户 Id。
输入 CHICAGO 作为口令。

您将看到窗体上显示计数为 1。这类似于一次有效登录。

3. 运行应用程序,并输入以下无效输入:

SALES 作为用户 Id。
INVALID 作为口令。

您将看到窗体上显示计数为 0。这类似于一次失败的登录。

4. 运行应用程序。在第一个文本框中输入以下无效输入:

SALES' and 1=1 --

此输入相当于输入一个有效用户 ID 却没有口令。因为输入是无效的,所以应该显示计数为 0,但实际情况不是这样。相反,显示计数为 1,这表明用户可以在不提供口令的情况下认证到应用程序。

5. 将查询字符串改为使用参数:

cmd.CommandText = "select count(deptno) from dept where " _
+ "dname = :dname and loc = :loc"  ' VB

cmd.CommandText = "select count(deptno) from dept where " _
+ "dname = :dname and loc = :loc";  // C#

6. 添加代码,将输入值与查询中的参数绑定:

                                     
Dim p1 As New OracleParameter("dname", OracleDbType.Varchar2)
p1.Value = TextBox1.Text
cmd.Parameters.Add(p1)

Dim p2 As New OracleParameter("loc", OracleDbType.Varchar2)
p2.Direction = ParameterDirection.Input  ' optional
p2.Size = 13  ' optional
p2.Value = TextBox2.Text
cmd.Parameters.Add(p2) ' VB.NET
OracleParameter p1 = new OracleParameter("dname", OracleDbType.Varchar2);
p1.Value = textBox1.Text;
cmd.Parameters.Add(p1);

OracleParameter p2 = new OracleParameter("loc", OracleDbType.Varchar2);
p2.Direction = ParameterDirection.Input; ' optional
p2.Size = 13; ' optional
p2.Value = textBox2.Text;
cmd.Parameters.Add(p2);  // C#
                                  

7. 如果您使用 C#,则进行以下更改:

label1.Text = dr.GetOracleDecimal(0).ToString(); // C#

8. 运行应用程序,并在第一个文本框中输入以下无效输入:

SALES' and 1=1 --

这次显示计数为 0,这表明只是将输入文本当作 varchar2 变量中的一列字母而不是 SQL 查询字符串的一部分来处理了。


John Paul Cook ( johnpaulcook@email.com ) 是居住在休斯顿的一位数据库和 .NET 顾问。他撰写了许多关于 .NET、Oracle 和其他主题的文章,并从 1986 年以来一直开发关系数据库应用程序。他目前的兴趣包括安全性和 IT 管理、Visual Studio 2005 和 Oracle 10 g 。他是 Oracle 认证 DBA 和 Microsoft MCSD for .NET。

将您的意见发送给我们

false ,,,,,,,,,,,,,,,,