|
脚本利用
从浏览器的角度来看,Web 页只是一个长字符串。浏览器会顺序处理这个字符串,在此过程中,会显示某些字符,同时按特殊规则解释其他字符(如 <b> 和 <script>)。如果恶意用户可以将某些特殊字符插入到页中,则浏览器将不知道这些字符不应该处于该位置,将作为页的一部分处理它们。
一个简单化的脚本利用的工作方式如下所示。假定一个应用程序允许用户发送有关最新电影的评论以供其他用户阅读。利用步骤可能为:
应用程序显示一个用户可以输入评论的窗体。恶意用户编写了一个其中包含 <script> 块的评论。 发送窗体,恶意用户的评论将存储在数据库中。 另一用户访问该站点。在构造页时,应用程序会从数据库中读取评论并将它们放在页中。恶意用户的 <script> 块将写入页中,就好像它是文本评论一样。 当第二个用户的浏览器显示此页时,它将遇到 script 块并执行它。 恶意用户还可以使用其他方法来利用脚本。常见的情况是:大多数脚本利用都会要求应用程序接受恶意输入,并将其插入到页中(回显它),浏览器将在该页中执行它。这种利用带来的潜在损害取决于所执行的脚本。它可以是无足轻重的,如在浏览器中弹出的烦人的消息。但是,它也可以产生严重的损害,方法是偷窃 Cookie、偷窃用户输入(如密码),甚至在用户的计算机上运行本机代码(如果对 Internet 安全性的要求不严格)。
防止脚本利用
防止脚本利用的主要方法就是决不信任来自用户的信息。假定从浏览器发送到您的应用程序的任何数据都包含恶意脚本。
同样,每次将字符串写入页时,您都应该假定字符串可能包含恶意脚本。(除非您自己以编程方式创建了该字符串。)例如,在从数据库中读取字符串时,您应该假定它们可能包含恶意脚本。安全意识很强的开发人员甚至不信任他们自己的数据库,理由是他们认为恶意用户可能有办法篡改数据库。
ASP.NET 为您提供了几种防止脚本利用的方法:
ASP.NET 通过请求验证来自动防止脚本利用。默认情况下,如果 Request 对象包含 HTML 编码的元素或某些 HTML 字符(如表示长破折号的 —),则 ASP.NET 页框架将引发一个错误。建议您为此错误在应用程序中创建一个处理程序。 如果您要在应用程序中显示字符串,但不信任它们,则向它们应用 HTML 编码。例如,进行编码后,标记 <b> 将变成 <b>。如果您正在显示的字符串来自您尚未确定信任其内容的数据库时,您可能会这样做。 如果您希望应用程序接受某些 HTML(例如,来自用户的某些格式设置指令),请关闭自动检查,并创建筛选器来精确定义应用程序将接受哪些 HTML。 安全说明 决不要禁用自动请求验证而不添加您自己的检查或筛选器。 注意 不要创建试图只筛选出不可接受元素的筛选器,因为预料每个可能的错误输入十分困难。相反,如果您确实要创建筛选器,请创建具有可接受输入的已定义列表的筛选器。
在 Web 应用程序中防止脚本利用
大多数脚本利用发生在用户可以将可执行代码(脚本)插入您的应用程序时。默认情况下,ASP.NET 提供请求验证。不管窗体发送包含什么样的 HTML,该验证都会引发错误。
您可以使用下列方法防止脚本利用:
在接受或显示字符串之前,将 HTML 编码应用于它们,以便字符串不包括任何可执行元素。
如果您的应用程序需要接受某些 HTML,则禁用请求验证并创建您自己的 HTML 筛选器。
本主题中的过程说明如何执行这些任务。
应用 HTML 编码
HTML 编码使用 HTML 保留字符转换 HTML 元素,以便显示它们而不是执行它们。
应用 HTML 编码
在显示字符串之前,调用 Server 对象的 HtmlEncode 方法。HTML 元素会转换为浏览器将显示(而不解释为 HTML)的字符串表示形式。
以下示例说明 HTML 编码。在一个实例中,在显示用户输入之前对其进行编码。在第二个实例中,在显示数据库中的数据之前对其进行编码。
注意 只有通过向 @ Page 指令中添加 alidateRequest="false" 来在页中禁用请求验证时,此示例才将起作用。决不禁用请求验证而不添加您自己的检查或筛选器。
' Visual Basic
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e _
As System.EventArgs) Handles Button1.Click
Label1.Text = Server.HtmlEncode(TextBox1.Text)
Label2.Text = _
Server.HtmlEncode(dsCustomers.Customers(0).CompanyName)
End Sub
// C#
private void Button1_Click(object sender, System.EventArgs e)
{
Label1.Text = Server.HtmlEncode(TextBox1.Text);
Label2.Text =
Server.HtmlEncode(dsCustomers1.Customers[0].CompanyName);
}
筛选 HTML 元素
默认情况下,Web 窗体页检测发送到服务器的信息中的任何 HTML 元素和保留字符。这样,将防止用户试图将脚本嵌入您的应用程序。当页检测到 HTML 时,它会引发一个错误。您可以使用 Page_Error 或 Application_Error 处理程序捕捉此错误。有关详细信息,请参见显示安全的错误信息。
但是,如果您的应用程序需要接受某些 HTML 元素,可关闭请求验证,并创建一个只允许使用要接受的 HTML 元素的筛选器。
注意 不要创建试图只筛选出不可接受元素的筛选器,因为预料每个可能的错误输入十分困难。相反,如果您创建筛选器,则创建一个定义可接受输入的筛选器。
筛选 HTML 元素
通过将属性 ValidateRequest="false" 添加到 @ Page 指令中禁用请求验证。
安全说明 决不要禁用自动请求验证而不添加您自己的检查或筛选器。
使用 HtmlEncode 方法对字符串进行编码。
调用 String.Replace 方法,将要接受的已编码 HTML 标记转换回它们的 HTML 形式。
提示 如果您熟悉正则表达式,则可以使用一个正则表达式来高效地执行筛选。有关详细信息,请参见 .NET Framework 正则表达式。
以下示例说明一个简单的筛选器,它接受加粗元素和带下划线的元素(<b>、</b>、<u>、</u>)。在显示所有其他用户输入之前都对其进行编码。
安全说明 许多 HTML 标记都允许在其属性中使用脚本。例如,标记 <img src="javascript:alert('hi')"> 是合法的。如果您希望接受比简单的格式设置标记更复杂的 HTML 标记,则必须确保恶意用户无法假借允许的 HTML 标记将脚本传递到您的应用程序。
' Visual Basic
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim userinput As String = TextBox1.Text
userinput = Server.HtmlEncode(userinput)
' Accepts <b>, </b>, <u>, </u>, case-insensitive
userinput = userinput.Replace("<b>", "<b>")
userinput = userinput.Replace("</b>", "</b>")
userinput = userinput.Replace("<B>", "<B>")
userinput = userinput.Replace("</B>", "</B>")
userinput = userinput.Replace("<u>", "<u>")
userinput = userinput.Replace("</u>", "</u>")
userinput = userinput.Replace("<U>", "<U>")
userinput = userinput.Replace("</U>", "</U>")
Label1.Text = userinput
End Sub
// C#
private void Button1_Click(object sender, System.EventArgs e)
{
String userinput = TextBox1.Text;
userinput = Server.HtmlEncode(userinput);
// Accepts <b>, </b>, <u>, </u>, case-insensitive
userinput = userinput.Replace("<b>", "<b>");
userinput = userinput.Replace("</b>", "</b>");
userinput = userinput.Replace("<B>", "<B>");
userinput = userinput.Replace("</B>", "</B>");
userinput = userinput.Replace("<u>", "<u>");
userinput = userinput.Replace("</u>", "</u>");
userinput = userinput.Replace("<U>", "<U>");
userinput = userinput.Replace("</U>", "</U>");
Label1.Text = userinput;
}
SQL 语句利用
还有一种与脚本利用类似的利用,它导致恶意 SQL 语句的执行。如果应用程序提示用户输入信息并将用户的输入串联为表示 SQL 语句的字符串,则会出现这种情况。例如,应用程序可能提示输入客户姓名,目的是为了执行类似如下的语句:
"Select * From Customers where CustomerName = " & txtCustomerName.Value 但是,对数据库有所了解的恶意用户可能使用文本框输入包含客户姓名的嵌入式 SQL 语句,产生类似如下的语句:
Select * From Customers Where CustomerName = 'a' Delete From Customers Where CustomerName > '' 执行该查询时,就会危害数据库。
为了防止 SQL 语句利用,决不使用字符串串联创建 SQL 查询。相反,使用参数化查询并将用户输入分配给参数对象。
数据适配器命令中的参数
数据适配器的命令(在 SelectCommand、InsertCommand、UpdateCommand 和 DeleteCommand 对象的 CommandText 属性中定义)经常涉及参数。在运行时,参数用于向命令代表的 SQL 语句或存储过程传递值。
参数在两种上下文中使用:
选择参数 - 在产品应用程序中,经常只获取数据库中数据的一个子集。其做法是,使用包含 WHERE 子句的 SQL 语句或存储过程,该子句具有用于获得选择判据(在运行时获取)的参数。此外,当更新或删除记录时,将使用 WHERE 子句指出要更改的一条或多条记录。WHERE 子句中使用的值通常在运行时导出。
更新参数 - 当更新现有记录或插入新记录时,已更改记录或新记录中列的值将在运行时建立。此外,开放式并发检查中使用的值也使用参数来建立。
注意 对于 Oracle,在 SQL 语句或存储过程中使用命名参数时,必须在参数名称前加冒号 (:)。但是,当在代码中的其他地方引用命名参数时(例如,当调用 Add 时),不要在命名参数前加冒号 (:)。数据提供程序自动提供冒号。有关更多信息,请参见 OracleParameter 类。
选择参数
当选择记录来填充数据集时,经常在 WHERE 子句中包括一个或多个参数,以便能够在运行时指定要获取哪些记录。例如,用户可能会搜索书籍数据库以查找其键入 Web 页的特定书名关键字。为了允许该操作,可以指定如下 SQL 语句作为 SelectCommand 的 CommandText 属性。参数用占位符(问号)或命名参数变量指示。涉及 OleDbCommand 和 OdbcCommand 对象的查询的参数使用问号;使用 SqlCommand 对象的查询使用以 @ 符号开头的命名参数,而使用 OracleCommand 对象的查询使用以冒号 (:) 开头的命名参数。
使用占位符的查询可能如下所示:
SELECT BookId, Title, Author, Price from BOOKS
WHERE (Title LIKE ?)
使用 SqlCommand 命名参数的查询可能如下所示:
SELECT BookId, Title, Author, Price from BOOKS
WHERE (Title LIKE @title)
使用 OracleCommand 命名参数的查询可能如下所示:
SELECT BookId, Title, Author, Price from BOOKS
WHERE (Title LIKE :title)
在应用程序中,您提示用户输入书名关键字。然后设置参数值并运行命令。
注意 有时,您可能需要获取数据库表的全部内容(例如在建立查找表时),但通常只需要获取所需数据以使应用程序效率较高。
在 Visual Studio 中,可以使用“查询生成器”生成带参数的 SQL 语句。如果从“服务器资源管理器”拖动元素,Visual Studio 可在某些情况下(但非所有情况下)配置参数,并且您需要手动完成配置。
更新参数
无论适配器的 SelectCommand 对象是否包含参数化命令,UpdateCommand、InsertCommand 和 DeleteCommand 属性的命令始终包含参数。
UpdateCommand 和 InsertCommand 属性的命令对于数据库中要更新的每一列都需要参数。此外,UpdateCommand 和 DeleteCommand 语句需要参数化的 WHERE 子句,它标识要更新的记录,其方式与经常配置 SelectCommand 对象的方式类似。
设想用户可以用其购买书籍的应用程序。用户在购物时将维护一个购物车(以数据表的形式实现)。在 ShoppingCart 表中,用户为要购买的每本书维持一条记录,以书籍 ID 和客户 ID 一起作为购物车记录的键。
当用户向购物车添加书籍时,应用程序可能调用 SQL INSERT 语句。在适配器中,该语句的语法可能如下所示:
INSERT INTO ShoppingCart
(BookId, CustId, Quantity)
Values (?, ?, ?)
三个问号代表参数占位符,它们将在运行时以客户 ID、书籍 ID 和数量的值填写。如果打算使用命名参数,同样的查询可能如下所示:
INSERT INTO ShoppingCart
(BookId, CustId, Quantity)
Values (@bookid, @custid, @quantity)
如果用户决定更改购物车中的某一项(例如更改其数量),则应用程序可能调用 SQL UPDATE 语句。该语句的语法可能如下所示:
UPDATE ShoppingCart
SET (BookId = ?, CustId = ?, Quantity = ?)
WHERE (BookId = ? AND CustId = ?)
或者如果打算使用命名参数,则可能如下所示:
UPDATE ShoppingCart
SET (BookId = @bookid, CustId = @custid, Quantity = @quantity)
WHERE (BookId = @bookid AND CustId = @custid)
在该语句中,SET 子句中的参数以已更改记录的更新值填写。WHERE 子句中的参数标识要更新哪条记录,并且以来自该记录的原始值填写。
用户还可以从购物车移除项。在此情况下,如果打算使用参数占位符,则应用程序可能调用具有如下语法的 SQL DELETE 语句:
DELETE FROM ShoppingCart
WHERE (BookId = ? AND CustId = ?)
或者如果打算使用命名参数,则可能如下所示:
DELETE FROM ShoppingCart
WHERE (BookId = @bookid AND CustId = @custid)
参数集合和参数对象
为允许您在运行时传递参数值,数据适配器的四个命令对象均支持 Parameters 属性。该属性包含单个参数对象的集合,这些对象与语句中的占位符一一对应。
下表显示了与每个数据适配器相对应的参数集合:
数据适配器 参数集合
SqlDataAdapter SqlParameterCollection
OleDbDataAdapter OleDbParameterCollection
OdbcDataAdapter OdbcParameterCollection
OracleDataAdapter OracleParameterCollection
注意 对于 Oracle,在 SQL 语句或存储过程中使用命名参数时,必须在参数名称前加冒号 (:)。但是,当在代码中的其他地方引用命名参数时(例如,当调用 Add 时),不要在命名参数前加冒号 (:)。Oracle .NET Framework 数据提供程序会自动提供冒号。
使用参数集合,您可省去必须手动将 SQL 命令构造为具有运行时值的字符串的麻烦。此外,还获得在参数中进行类型检查的好处。
如果使用“数据适配器配置向导”配置适配器,则为所有四个适配器命令自动建立和配置参数集合。如果从“服务器资源管理器”将元素拖动到窗体或组件上,则 Visual Studio 可以执行下列配置:
如果将表或某些列拖动到设计器上,则 Visual Studio 将生成一个不带参数的 SelectCommand 对象(明确地说是一条 SQL SELECT 语句),以及参数化的 UpdateCommand、InsertCommand 和 DeleteCommand 对象。如果希望 SelectCommand 对象语句有参数,则必须手动进行配置。
如果将存储过程拖动到设计器上,则 Visual Studio 将生成一个 SelectCommand 对象,带有存储过程所需的参数。但是,如果需要,您必须自己配置 UpdateCommand、InsertCommand 和 DeleteCommand 对象及其参数。
一般而言,如果想为适配器创建参数化查询,则应使用“数据适配器配置向导”。但是,如果需要,可以使用“属性”窗口手动配置参数。
参数集合的结构
命令的参数集合中的项与相应的命令对象所需的参数一一对应。如果命令对象是一条 SQL 语句,则集合中的项对应于该语句中的占位符(问号)。以下 UPDATE 语句需要有五个参数项的集合:
UPDATE ShoppingCart
SET (BookId = ?, CustId = ?, Quantity = ?)
WHERE (BookId = ? AND CustId = ?)
以下是带有命名参数的相同语句:
UPDATE ShoppingCart
SET (BookId = @bookid, CustId = @custid, Quantity = @quantity)
WHERE (BookId = @bookid AND CustId = @custid)
如果命令对象引用的是存储过程,则集合中的参数项数由该过程本身决定。参数与 SQL 语句中的占位符可能不完全对应。
在存储过程中,还可以对参数进行命名。在此情况下,参数在集合中的位置并不重要。相反,集合中的每个参数项都有一个 ParameterName 属性,用于使自身与存储过程中的相应参数匹配。
当手动配置参数集合时,必须确切理解哪些参数是存储过程需要的。许多存储过程返回值;如果是这样,该值在参数集合中传递回应用程序,因此您必须允许返回值。此外,某些存储过程包含多条 SQL 语句,您必须确保参数集合反映传递给过程中的全部语句的所有值。
如果参数不是命名参数(如在存储过程中那样),则集合中的项按位置映射到命令所需的参数。如果命令是一个存储过程并且返回值,则为该返回值保留集合中的第一项(零项)。
因此,可以根据集合中的索引位置引用单个参数对象。但是,参数对象还支持 ParameterName 属性,它提供一种独立于参数顺序来引用参数的方法。例如,下面两条语句可能等效(假定集合中的第二个参数被命名为 Title_Keyword):
' Visual Basic
' Encloses the keyword in SQL wildcard characters.
titleKeyword = "%" & txtTitleKeyword.Text & "%"
OleDbDataAdapter1.SelectCommand.Parameters(1).Value = titleKeyword
OleDbDataAdapter1.SelectCommand.Parameters("Title_Keyword").Value = titleKeyword
// C#
// Encloses the keyword in SQL wildcard characters.
string titleKeyword = "%" + txtTitleKeyword.Text + "%";
this.OleDbDataAdapter1.SelectCommand.Parameters[1].Value = titleKeyword;
this.OleDbDataAdapter1.SelectCommand.Parameters["Title_Keyword"].Value = titleKeyword;
编程时使用参数名通常比通过索引值引用参数好得多,因为它减少了在参数个数变化时的维护需要,并使您不必记忆存储过程是否返回值。通过名称而不是索引值来引用参数存在少许附加系统开销,但这可通过编程方便和应用程序的可维护性抵消。
建立参数值
建立参数值的方式有两种:
通过显式设置参数的 Value 属性。
通过将参数映射到数据集表中的列,以便可以在需要时从数据行提取值。
在填充数据集或调用命令时显式设置参数值(即对于选择参数)。例如,在上面搜索书籍的示例中,应用程序可能有一个文本框,用户在该框中输入书名关键字。然后,您在调用适配器的 Fill 方法之前,将参数的值显式设置为文本框的文本。完成该操作的代码可能如下所示,它在填充数据集之前将文本框的内容建立为参数。
' Visual Basic
' Encloses the keyword in SQL wildcard characters.
titleKeyword = "%" & txtTitleKeyword.Text & "%"
OleDbDataAdapter1.SelectCommand.Parameters("Title_Keyword").Value = titleKeyword
OleDbDataAdapter1.Fill(dsAuthors1)
// C#
// Encloses the keyword in SQL wildcard characters.
titleKeyword = "%" + txtTitleKeyword.Text + "%";
this.OleDbDataAdapter1.SelectCommand.Parameters["Title_Keyword"].Value = titleKeyword;
this.OleDbDataAdapter1.Fill(dsAuthors1);
映射的参数值在更新中使用。当调用适配器的 Update 方法时,该方法依次处理数据集表中的记录,分别为每条记录进行适当更新(更新、插入或删除)。在此情况下,参数值已可以作为数据集记录中的列使用。例如,当更新进程到达数据集表中的一个新记录(必须为其在数据库中调用 INSERT 语句)时,INSERT 语句的 VALUE 子句的值可以从该记录直接读出。
这些是典型方案,但不是唯一的几个方案。存储过程有时使用 out 参数或通过过程的返回值返回数据。如果是这样,返回值应映射到数据集表中的列。
也有可能显式设置更新参数。适配器支持 RowUpdating 事件,每次更新行时都将调用该事件。可以为该事件创建一个处理程序,并在其中设置参数值。这使您可以对参数值进行十分精确的控制,并可以执行诸如下面的处理:在参数值写入数据库记录之前动态创建参数值。
<< 上一页 [11] [12] [13] [14] [15] [16] [17] [18] [19] [20] 下一页 |