领先技术: 子类化和重写 ASP.NET 页面 - 第 II 部分 -- MSDN Mag...

来源:百度文库 编辑:神马文学网 时间:2024/07/08 17:10:16
领先技术
子类化和重写 ASP.NET 页面 - 第 II 部分
Dino Esposito
more ...
打印
E-mail
添加到收藏夹
评价
RSS (领先技术)
Add RSS to Any
相关信息
Live Spaces
Digg This
BlogThis!
Slashdot
del.icio.us
Technorati
Explore the code.
Download the code.
Get the sample code for this article. NEW:Explore the sample code online!
- or -
代码下载位置:CuttingEdge2007_05.exe (163KB)
目录
背景知识
原始解决方案
模拟一次很简单的攻击
其中的原理是什么呢?
改进后的 TextBox 控件
替换 TextBox 控件
最终的解决方案
最近一个很偶然的机会,我发现了一个大型网站,上面全是一些极其简单的 Web 用户控件,确切地说是一些 ASCX 文件。开发人员在发现所使用的服务器控件会出现异常行为后,往往认为这种方法是很有必要的。
因此,开发人员将站点内的这类服务器控件全部更换为包含原始控件修改版本的用户控件(同时由于无法确定更换服务器控件会导致何种后果,因此开发人员还替换了其他大量控件。)开发人员认为,将这样一种额外的抽象层置于页面和控件之间会更可靠。另外一个好处就是可以在 ASP.NET 应用程序中轻松替换用户控件(如果需要的话),而无需修改二进制文件和重新启动应用程序。(这种情况并非始终都会发生,但有些部署方案会要求执行该操作。)
曾经有公司请我来审阅应用程序,他们问我的第一个问题就是:“是否有更好的方法可以在不大量返工每个页面的情况下替换整个站点的服务器控件?”
我在自己主持的2007 年 4 月专栏中,针对如何在不修改原始源代码的情况下对 ASP.NET 网站进行有限的(有时是临时的)修改给出了几种解决方案。本月我又发现几种技巧,无需修改源代码,通过声明的方式即可替换服务器控件和 URL。
当时我无法立即回答他们的问题,但却知道如何找到解决方法。我想如果是我开发了 ASP.NET 基础结构,我会在配置文件中放置某种设置,以便开发人员能够通过声明的方式将标记映射到控件。在 ASP.NET 中这并非是一个全新的理念。早在 ASP.NET 1.x 中,您就可以通过声明的方式更改一些与代码相关的内容,例如网页和用户控件的基类。(但是,这种方法只适用于未在 Page 指令中显式使用 Inherits 子句的页面和用户控件。)因此我产生了一个疑问,为什么服务器控件不可以采取这种方法呢。事实证明我当时的推断是正确的:ASP.NET 2.0 正是为此才提供了 节。
背景知识
我想还是先向大家介绍一些背景知识。此方案始于一次内部安全审查,当时客户发现应用程序内存在一个可能导致经典 SQL 注入式攻击的漏洞。公司对这一漏洞应用了快速修补程序,但却导致了另一个问题。
在客户的网站上,许多页面都允许查询字符串中包含固定的五字符代码。这种代码会随后用于构成 SQL 语句。该公司当时仍在运行类似以下内容的代码:
Dim code As String = Request.QueryString(“Code”).ToString();Dim command As String = _“SELECT * FROM customers WHERE id=’” & code & “’”说心里话,我真的希望您的网站已经不再运行类似代码!这种代码完全盲目地信任任何通过查询字符串传递的信息,并会将该信息附加到构成 SQL 命令的字符串。这样做会形成非常严重的安全隐患。手段高明的黑客能够轻而易举地发现那些看似正常、实则危险的文本,它们能够将原始的和为特定目的编写的 SQL 命令变成危险的攻击。如果您需要更多有关 SQL 注入的详细信息,建议您先阅读 Paul Litwin 撰写的文章“Stop SQL Injection Attacks Before They Stop You”。
 
从性能方面考虑,发送动态构建的命令是不明智的。这些命令不会从重用查询计划中获益,因为代码本身在每次提交时都可能发生变化。使用参数化查询或存储过程有两大优势:一是它能确保至少对发送的数据类型进行一次自动检查,二是它能够从 SQL Server™ 和其他数据库中的查询计划缓存中获得好处。
正如“领先技术”2007 年 3 月刊中所述,您应该像验证控制台实用程序的命令行那样静态地对查询字符串进行验证。这样可以避免数字、日期和布尔值的歧义,但如果是字符串,您就无可奈何了。在这里,问题的关键是字符串中到底包含哪些内容。您需要使用某种业务逻辑仔细地验证传递的字符串。但验证字符串长度并不难,不管是通过 HTTP 模块还是通过有限地修改代码,都是可以实现的。

原始解决方案
发现 SQL 注入漏洞后,客户认为将可接受参数的大小限制为五个字符(即代码的实际大小)就能快捷有效地解决问题。仅有五个可用的字符,黑客是奈何不了您的数据库– 至少希望是这样的。因此,客户安装了“领先技术”2007 年 3 月刊中演示的 HTTP 模块,并检查了受影响页面的查询字符串大小。结果发现实际发送到页面的字符未超过五个。
但是,该应用程序组合了新的 ASP.NET 页面和经过修改的经典 ASP 页面,其中某些页面能够允许用户在文本框内键入并提交相同的代码。而在服务器上,指定的代码会在回发过程中通过前述的相同方法附加到 SQL 语句中。因此,通过文本框提交的文本长度也需要进行同样的限制。开发人员原以为这是个简单的问题,因此将文本框的 MaxLength 属性设置为所需的值:

 
修复过的问题看上去万无一失。长度超过五个字符的代码无法进入站点的中间层。但这并不一定意味着站点处于可避免注入的安全状态,但这种做法确实限制了遭受攻击的可能性。或者说他们是这么认为的。

模拟一次很简单的攻击
假设有一个类似于图 1 所示的 ASP.NET 示例页面。页面的源代码如图 2 所示。该页面具有一个 MaxLength 属性为 5 的文本框和一个提交按钮。单击按钮后,会执行回发操作并对文本框的内容进行处理。正常情况下,在浏览器中显示页面的位置是无法键入五个以上的字符的。如果您尝试粘贴更长的文本字符串,字符串将被相应地截断为指定长度。

图 1 ASP.NET 示例页面 (单击该图像获得较小视图)

图 1 ASP.NET 示例页面 (单击该图像获得较大视图)
现在我们从攻击者的角度考虑这个问题。对网页进行攻击通常需要先创建格式为 Plain HTML 的页面副本,然后改变某些值并发布“破坏”版的页面。要获得页面的 HTML,恶意用户只需向普通用户那样显示页面:选择“查看源代码”,然后将内容保存为本地 HTML 文件。但这种方法只有在攻击者可以实际访问页面时才可能奏效。例如,如果页面受到保护,攻击者就必须出示有效凭据才能查看页面。但是,被盗用的身份验证 Cookie、网络钓鱼诈骗及其他社交诈骗术都可以使有用的信息流入不正当人的手中。
将 ASP.NET 页的标记保存到本地机器后,攻击者需要对其进行一些更改。首先,攻击者必须更改表单的 action 属性,使其指向同一 ASP.NET 页面的绝对 URL。以下是 ASP.NET 的 default.aspx 页面的服务器表单的典型标记:
攻击者会将其更改为以下内容:
 

 
ASP.NET HtmlForm 控件上没有 action 属性,但当您使用 Plain HTML 时,仍可以将表单内容发布到任何需要的 URL。第二项需要更改的是将要发布的“破坏”数据。我要为 ASP.NET TextBox 服务器控件发出的标记设置一个 value 属性,将其设为远大于规定五个字符的字符串:

 
当攻击者在自己的计算机上显示该 HTML 页面并单击按钮时,您认为会发生什么?结果如图 3 所示。左侧浏览器窗口的地址栏指明所显示的页面为本地 HTML 页。但在右侧的浏览器窗口中,您会发现经过修改的表单内容已发布到远程 ASP.NET 应用程序。攻击者避开了五个字符的限制。这表明恶意用户是有办法发送文本框上任意大小的页面文本的,无论 MaxLength 为何种设置。

图 3 可发布任意长度字符串的本地 HTML 页 (单击该图像获得较小视图)

图 3 可发布任意长度字符串的本地 HTML 页 (单击该图像获得较大视图)

其中的原理是什么呢?
您可能想知道问题出在何处。是在浏览器中?还是在 ASP.NET 运行库中?或者可能是 TextBox 控件中?没错,真正的问题就出现在 TextBox 控件中。
如果含有 TextBox 服务器控件的页面在回发后被重新创建在服务器上,则 TextBox 服务器控件将不会对 MaxLength 进行检查。很显然,为了安全起见,在指定 Text 属性之前,TextBox 应该检查 MaxLength 的值,并将其与已发布文本的长度进行比较。

改进后的 TextBox 控件
TextBox 位于服务器端,与 HTML 标记相对应,它可以接收用户键入到输入缓冲区内的文本。TextBox 需要对该文本进行处理,以激活 TextChanged 服务器事件,并使页面内的其他控件可以使用数据。处理已发布数据的 ASP.NET 控件可以实现 IPostBackDataHandler 接口,方法如下:
Public Interface IPostBackDataHandlerFunction LoadPostData(ByVal postDataKey As String, _ByVal postCollection As NameValueCollection) As BooleanSub RaisePostDataChangedEvent()End InterfaceLoadPostData 方法会检查 TextBox 控件的回发数据是否与其前一个值不同,如果是,则加载该内容并返回 true。否则即返回 false。
 
还原每个控件视图状态的内容后,会立即在页面的 Init 和 Load 事件之间调用 LoadPostData 方法。postDataKey 参数指示了已发布集合内的名称,该集合引用了要加载的内容。postCollection 参数引入了所有已发布值的集合 – 查询字符串或表单集合,具体取决于所选的 HTTP 谓词。
在生命周期中稍后会调用 RaisePostDataChangedEvent 方法,以触发一项与控件相关的可选事件,该事件能够指示控件的状态在回发后是否被更改。在实践中,只有在 LoadPostData 返回 true 时才会调用 RaisePostDataChangedEvent 方法。
图 4 显示的伪代码显示了为 System.Web.UI.WebControls.TextBox 控件实现 LoadPostData 方法。基本上,该方法可将读取自视图状态的 Text 属性的值与已发布值进行比较。如果两个值不同,则已发布值将替换当前值,并成为控件 Text 属性的新值。
如您所见,已发布的值被盲目地分配给 Text 属性,而并未充分考虑字符串的长度。通过 LoadPostData 方法,每个控件都可以更新所需数量的属性,并且可以交叉检查对测试有意义的所有属性。如图 4 所示,TextBox 实现 LoadPostData 方法,限制了验证,使其只能确保控件为非只读,进而对新旧文本进行比较。
图 5 所示为一个全新的 TextBox 控件,其 LoadPostData 方法的实现稍有不同。重写的方法只是先将已发布文本截断至允许的最大长度,然后再进行文本比较。如图 6 所示,任何超过最大长度的文本都会被自动截断,因此在回发过程中不会再用于生成更长的结果。无论客户端浏览器的功能如何,都会出现这种情况。

图 6 超过最大长度的文本会被“截断” (单击该图像获得较小视图)

图 6 超过最大长度的文本会被“截断” (单击该图像获得较大视图)
仔细比较图 4 和图 5 中 LoadPostData 方法的源代码,您就会发现一个细微的差别。在图 4 中,方法在其基类(System.Web.UI.Control 类)上调用至 ValidateEvent。在图 5 中,同一代码是通过调用 ClientScriptManager 对象上的 ValidateEvent 而被替换的:
Page.ClientScript.ValidateEvent(Me.UniqueID, String.Empty)
 
由于 Control 基类上的 ValidateEvent 方法是声明为 Friend(在 C# 内部),因此从 System.Web.dll 程序集之外定义的任何类是无法调用它的。Control 基类上的 ValidateEvent 方法的调用堆栈最终会调用 ClientScriptManager 对象上的 ValidateEvent 方法;ClientScriptManager 对象的实例则通过 Page 类的 ClientScript 属性得以公开。
ValidateEvent 是 ASP.NET 2.0 中可用于实现事件验证的一个工具。事件验证是一项内置功能,旨在避免页面处理那些不是由页面和已注册控件专门生成的事件(和事件参数)。

替换 TextBox 控件
经过一些列操作,现在您获得了一个全新的 TextBox 控件。这个全新的控件可确保任何分配给 Text 属性的超过最大长度的文本都能被检测到并得以删除。您会在 ASP.NET 页中使用此控件吗?只需向每个页面注册该控件并替换出现的所有原始文本框即可。
在 ASP.NET 2.0 中,将以下配置脚本添加到 块的 节下的 web.config 文件中,这样您可以节省不少时间。
这段脚本保证 web.config 文件控制的所有页面均可自动注册指定的标记和控件。
 
但目前仍存在一个问题,而且是个很大的问题。那就是如何在新旧文本框之间切换?幸运的是,ASP.NET 2.0 中的配置文件内提供了一个 节:

 
节允许您在编译时将一种控件类型重新映射到另一种控件类型。通过这种重新映射,我们使用被映射的类型替代了受配置文件控制的全部页面和用户控件的原始类型根据前面给出的代码,任何引用了系统 TextBox 的地方均将使用 Dino.Samples.TextBox。您要做的只是编写新控件并编辑 web.config 文件。这种简单的做法是不是有些不可思议?但确实是非常有效的。
毫无疑问,重新映射的类型必须为继承自原始类型的类。还要指出的是,ASP.NET 团队在 ASP.NET AJAX Extensions 1.0 的 pre-RTM build 中使用了此功能,以便使用可与 UpdatePanel 控件很好兼容的新验证程序控件来替换原始控件。

最终的解决方案
客户最终正确地诊断出 ASP.NET TextBox 控件及其处理已发布数据的方式存在问题。他们通过创建新的 TextBox 控件令人满意地修复了问题。由于开发人员之前并不了解有更好的方法或通过声明的方式来替换整个站点的控件,因此他们手动替换了所有出现的控件,并将其打包放入一个用户控件中。这样做是为了尽量降低将来的更改可能造成的影响。
有了 tagMapping 功能,找到解决方案简直易如反掌。使用 tagMapping 这一技巧比较灵活,可以用来替换错误的控件或者为现有控件添加新功能。但是请注意,如果重新映射的控件具有了新的属性或方法,您需要修改源代码才能使用这些新属性和方法。
(提到以声明的方式进行映射,ASP.NET 2.0 还具有一个特性,即包含 节。它是 的直接子级。 节在 ASP.NET 2.0 中是声明性的,它对应的是 HttpContext 对象上的 RewritePath 方法。)
总之,您要注意,在设置了 MaxLength 之后,原始 ASP.NET TextBox 控件将无法对 Text 属性的任何发布值进行裁剪。但本专栏通过修改控件解决了这一限制,应该对您解决这一问题有所帮助。您可以在 web.config 文件中新加一行简单的代码,通过声明的方式将其插入应用程序。

将您向 Dino 提出的问题和意见发送至:cutting@microsoft.com cutting@microsoft.com.
NEW:Explore the sample code online! - or - 代码下载位置:CuttingEdge2007_05.exe (163KB)