最近一個(gè)很偶然的機(jī)會(huì),我發(fā)現(xiàn)了一個(gè)大型網(wǎng)站,上面全是一些極其簡(jiǎn)單的 Web 用戶(hù)控件,確切地說(shuō)是一些 ASCX 文件。開(kāi)發(fā)人員在發(fā)現(xiàn)所使用的服務(wù)器控件會(huì)出現(xiàn)異常行為后,往往認(rèn)為這種方法是很有必要的。
因此,開(kāi)發(fā)人員將站點(diǎn)內(nèi)的這類(lèi)服務(wù)器控件全部更換為包含原始控件修改版本的用戶(hù)控件(同時(shí)由于無(wú)法確定更換服務(wù)器控件會(huì)導(dǎo)致何種后果,因此開(kāi)發(fā)人員還替換了其他大量控件。)開(kāi)發(fā)人員認(rèn)為,將這樣一種額外的抽象層置于頁(yè)面和控件之間會(huì)更可靠。另外一個(gè)好處就是可以在 ASP.NET 應(yīng)用程序中輕松替換用戶(hù)控件(如果需要的話(huà)),而無(wú)需修改二進(jìn)制文件和重新啟動(dòng)應(yīng)用程序。(這種情況并非始終都會(huì)發(fā)生,但有些部署方案會(huì)要求執(zhí)行該操作。)
曾經(jīng)有公司請(qǐng)我來(lái)審閱應(yīng)用程序,他們問(wèn)我的第一個(gè)問(wèn)題就是:“是否有更好的方法可以在不大量返工每個(gè)頁(yè)面的情況下替換整個(gè)站點(diǎn)的服務(wù)器控件?”
我在自己主持的
2007 年 4 月專(zhuān)欄中,針對(duì)如何在不修改原始源代碼的情況下對(duì) ASP.NET 網(wǎng)站進(jìn)行有限的(有時(shí)是臨時(shí)的)修改給出了幾種解決方案。本月我又發(fā)現(xiàn)幾種技巧,無(wú)需修改源代碼,通過(guò)聲明的方式即可替換服務(wù)器控件和 URL。
當(dāng)時(shí)我無(wú)法立即回答他們的問(wèn)題,但卻知道如何找到解決方法。我想如果是我開(kāi)發(fā)了 ASP.NET 基礎(chǔ)結(jié)構(gòu),我會(huì)在配置文件中放置某種設(shè)置,以便開(kāi)發(fā)人員能夠通過(guò)聲明的方式將標(biāo)記映射到控件。在 ASP.NET 中這并非是一個(gè)全新的理念。早在 ASP.NET 1.x 中,您就可以通過(guò)聲明的方式更改一些與代碼相關(guān)的內(nèi)容,例如網(wǎng)頁(yè)和用戶(hù)控件的基類(lèi)。(但是,這種方法只適用于未在 Page 指令中顯式使用 Inherits 子句的頁(yè)面和用戶(hù)控件。)因此我產(chǎn)生了一個(gè)疑問(wèn),為什么服務(wù)器控件不可以采取這種方法呢。事實(shí)證明我當(dāng)時(shí)的推斷是正確的:ASP.NET 2.0 正是為此才提供了 <tagMapping> 節(jié)。
背景知識(shí)
我想還是先向大家介紹一些背景知識(shí)。此方案始于一次內(nèi)部安全審查,當(dāng)時(shí)客戶(hù)發(fā)現(xiàn)應(yīng)用程序內(nèi)存在一個(gè)可能導(dǎo)致經(jīng)典 SQL 注入式攻擊的漏洞。公司對(duì)這一漏洞應(yīng)用了快速修補(bǔ)程序,但卻導(dǎo)致了另一個(gè)問(wèn)題。
在客戶(hù)的網(wǎng)站上,許多頁(yè)面都允許查詢(xún)字符串中包含固定的五字符代碼。這種代碼會(huì)隨后用于構(gòu)成 SQL 語(yǔ)句。該公司當(dāng)時(shí)仍在運(yùn)行類(lèi)似以下內(nèi)容的代碼:
Dim code As String = Request.QueryString(“Code”).ToString();Dim command As String = _“SELECT * FROM customers WHERE id=’” & code & “’”說(shuō)心里話(huà),我真的希望您的網(wǎng)站已經(jīng)不再運(yùn)行類(lèi)似代碼!這種代碼完全盲目地信任任何通過(guò)查詢(xún)字符串傳遞的信息,并會(huì)將該信息附加到構(gòu)成 SQL 命令的字符串。這樣做會(huì)形成非常嚴(yán)重的安全隱患。手段高明的黑客能夠輕而易舉地發(fā)現(xiàn)那些看似正常、實(shí)則危險(xiǎn)的文本,它們能夠?qū)⒃嫉暮蜑樘囟康木帉?xiě)的 SQL 命令變成危險(xiǎn)的攻擊。如果您需要更多有關(guān) SQL 注入的詳細(xì)信息,建議您先閱讀 Paul Litwin 撰寫(xiě)的文章“
Stop SQL Injection Attacks Before They Stop You”。
從性能方面考慮,發(fā)送動(dòng)態(tài)構(gòu)建的命令是不明智的。這些命令不會(huì)從重用查詢(xún)計(jì)劃中獲益,因?yàn)榇a本身在每次提交時(shí)都可能發(fā)生變化。使用參數(shù)化查詢(xún)或存儲(chǔ)過(guò)程有兩大優(yōu)勢(shì):一是它能確保至少對(duì)發(fā)送的數(shù)據(jù)類(lèi)型進(jìn)行一次自動(dòng)檢查,二是它能夠從 SQL Server? 和其他數(shù)據(jù)庫(kù)中的查詢(xún)計(jì)劃緩存中獲得好處。
正如
“領(lǐng)先技術(shù)”2007 年 3 月刊中所述,您應(yīng)該像驗(yàn)證控制臺(tái)實(shí)用程序的命令行那樣靜態(tài)地對(duì)查詢(xún)字符串進(jìn)行驗(yàn)證。這樣可以避免數(shù)字、日期和布爾值的歧義,但如果是字符串,您就無(wú)可奈何了。在這里,問(wèn)題的關(guān)鍵是字符串中到底包含哪些內(nèi)容。您需要使用某種業(yè)務(wù)邏輯仔細(xì)地驗(yàn)證傳遞的字符串。但驗(yàn)證字符串長(zhǎng)度并不難,不管是通過(guò) HTTP 模塊還是通過(guò)有限地修改代碼,都是可以實(shí)現(xiàn)的。
原始解決方案
發(fā)現(xiàn) SQL 注入漏洞后,客戶(hù)認(rèn)為將可接受參數(shù)的大小限制為五個(gè)字符(即代碼的實(shí)際大小)就能快捷有效地解決問(wèn)題。僅有五個(gè)可用的字符,黑客是奈何不了您的數(shù)據(jù)庫(kù)– 至少希望是這樣的。因此,客戶(hù)安裝了“領(lǐng)先技術(shù)”2007 年 3 月刊中演示的 HTTP 模塊,并檢查了受影響頁(yè)面的查詢(xún)字符串大小。結(jié)果發(fā)現(xiàn)實(shí)際發(fā)送到頁(yè)面的字符未超過(guò)五個(gè)。
但是,該應(yīng)用程序組合了新的 ASP.NET 頁(yè)面和經(jīng)過(guò)修改的經(jīng)典 ASP 頁(yè)面,其中某些頁(yè)面能夠允許用戶(hù)在文本框內(nèi)鍵入并提交相同的代碼。而在服務(wù)器上,指定的代碼會(huì)在回發(fā)過(guò)程中通過(guò)前述的相同方法附加到 SQL 語(yǔ)句中。因此,通過(guò)文本框提交的文本長(zhǎng)度也需要進(jìn)行同樣的限制。開(kāi)發(fā)人員原以為這是個(gè)簡(jiǎn)單的問(wèn)題,因此將文本框的 MaxLength 屬性設(shè)置為所需的值:
<asp:textbox runat=”server” id=”TextBox1” MaxLength=”5” />
修復(fù)過(guò)的問(wèn)題看上去萬(wàn)無(wú)一失。長(zhǎng)度超過(guò)五個(gè)字符的代碼無(wú)法進(jìn)入站點(diǎn)的中間層。但這并不一定意味著站點(diǎn)處于可避免注入的安全狀態(tài),但這種做法確實(shí)限制了遭受攻擊的可能性?;蛘哒f(shuō)他們是這么認(rèn)為的。
模擬一次很簡(jiǎn)單的攻擊
假設(shè)有一個(gè)類(lèi)似于圖 1 所示的 ASP.NET 示例頁(yè)面。頁(yè)面的源代碼如
圖 2 所示。該頁(yè)面具有一個(gè) MaxLength 屬性為 5 的文本框和一個(gè)提交按鈕。單擊按鈕后,會(huì)執(zhí)行回發(fā)操作并對(duì)文本框的內(nèi)容進(jìn)行處理。正常情況下,在瀏覽器中顯示頁(yè)面的位置是無(wú)法鍵入五個(gè)以上的字符的。如果您嘗試粘貼更長(zhǎng)的文本字符串,字符串將被相應(yīng)地截?cái)酁橹付ㄩL(zhǎng)度。
圖 1 ASP.NET 示例頁(yè)面 (單擊該圖像獲得較大視圖)
現(xiàn)在我們從攻擊者的角度考慮這個(gè)問(wèn)題。對(duì)網(wǎng)頁(yè)進(jìn)行攻擊通常需要先創(chuàng)建格式為 Plain HTML 的頁(yè)面副本,然后改變某些值并發(fā)布“破壞”版的頁(yè)面。要獲得頁(yè)面的 HTML,惡意用戶(hù)只需向普通用戶(hù)那樣顯示頁(yè)面:選擇“查看源代碼”,然后將內(nèi)容保存為本地 HTML 文件。但這種方法只有在攻擊者可以實(shí)際訪(fǎng)問(wèn)頁(yè)面時(shí)才可能奏效。例如,如果頁(yè)面受到保護(hù),攻擊者就必須出示有效憑據(jù)才能查看頁(yè)面。但是,被盜用的身份驗(yàn)證 Cookie、網(wǎng)絡(luò)釣魚(yú)詐騙及其他社交詐騙術(shù)都可以使有用的信息流入不正當(dāng)人的手中。
將 ASP.NET 頁(yè)的標(biāo)記保存到本地機(jī)器后,攻擊者需要對(duì)其進(jìn)行一些更改。首先,攻擊者必須更改表單的 action 屬性,使其指向同一 ASP.NET 頁(yè)面的絕對(duì) URL。以下是 ASP.NET 的 default.aspx 頁(yè)面的服務(wù)器表單的典型標(biāo)記:
<form name=”form1” method=”post” action=”Default.aspx” id=”form1”>攻擊者會(huì)將其更改為以下內(nèi)容:
<form name=”form1” method=”post”action=”http://targetserver/Source/Default.aspx” id=”form1”>
ASP.NET HtmlForm 控件上沒(méi)有 action 屬性,但當(dāng)您使用 Plain HTML 時(shí),仍可以將表單內(nèi)容發(fā)布到任何需要的 URL。第二項(xiàng)需要更改的是將要發(fā)布的“破壞”數(shù)據(jù)。我要為 ASP.NET TextBox 服務(wù)器控件發(fā)出的標(biāo)記設(shè)置一個(gè) value 屬性,將其設(shè)為遠(yuǎn)大于規(guī)定五個(gè)字符的字符串:
<input name=”TextBox1” type=”text”maxlength=”5” id=”TextBox1”value=”This is a far looooonger text” />
當(dāng)攻擊者在自己的計(jì)算機(jī)上顯示該 HTML 頁(yè)面并單擊按鈕時(shí),您認(rèn)為會(huì)發(fā)生什么?結(jié)果如圖 3 所示。左側(cè)瀏覽器窗口的地址欄指明所顯示的頁(yè)面為本地 HTML 頁(yè)。但在右側(cè)的瀏覽器窗口中,您會(huì)發(fā)現(xiàn)經(jīng)過(guò)修改的表單內(nèi)容已發(fā)布到遠(yuǎn)程 ASP.NET 應(yīng)用程序。攻擊者避開(kāi)了五個(gè)字符的限制。這表明惡意用戶(hù)是有辦法發(fā)送文本框上任意大小的頁(yè)面文本的,無(wú)論 MaxLength 為何種設(shè)置。
圖 3 可發(fā)布任意長(zhǎng)度字符串的本地 HTML 頁(yè) (單擊該圖像獲得較大視圖)
其中的原理是什么呢?
您可能想知道問(wèn)題出在何處。是在瀏覽器中?還是在 ASP.NET 運(yùn)行庫(kù)中?或者可能是 TextBox 控件中?沒(méi)錯(cuò),真正的問(wèn)題就出現(xiàn)在 TextBox 控件中。
如果含有 TextBox 服務(wù)器控件的頁(yè)面在回發(fā)后被重新創(chuàng)建在服務(wù)器上,則 TextBox 服務(wù)器控件將不會(huì)對(duì) MaxLength 進(jìn)行檢查。很顯然,為了安全起見(jiàn),在指定 Text 屬性之前,TextBox 應(yīng)該檢查 MaxLength 的值,并將其與已發(fā)布文本的長(zhǎng)度進(jìn)行比較。
改進(jìn)后的 TextBox 控件
TextBox 位于服務(wù)器端,與 <input type=text> HTML 標(biāo)記相對(duì)應(yīng),它可以接收用戶(hù)鍵入到輸入緩沖區(qū)內(nèi)的文本。TextBox 需要對(duì)該文本進(jìn)行處理,以激活 TextChanged 服務(wù)器事件,并使頁(yè)面內(nèi)的其他控件可以使用數(shù)據(jù)。處理已發(fā)布數(shù)據(jù)的 ASP.NET 控件可以實(shí)現(xiàn) IPostBackDataHandler 接口,方法如下:
Public Interface IPostBackDataHandlerFunction LoadPostData(ByVal postDataKey As String, _ByVal postCollection As NameValueCollection) As BooleanSub RaisePostDataChangedEvent()End InterfaceLoadPostData 方法會(huì)檢查 TextBox 控件的回發(fā)數(shù)據(jù)是否與其前一個(gè)值不同,如果是,則加載該內(nèi)容并返回 true。否則即返回 false。
還原每個(gè)控件視圖狀態(tài)的內(nèi)容后,會(huì)立即在頁(yè)面的 Init 和 Load 事件之間調(diào)用 LoadPostData 方法。postDataKey 參數(shù)指示了已發(fā)布集合內(nèi)的名稱(chēng),該集合引用了要加載的內(nèi)容。postCollection 參數(shù)引入了所有已發(fā)布值的集合 – 查詢(xún)字符串或表單集合,具體取決于所選的 HTTP 謂詞。
在生命周期中稍后會(huì)調(diào)用 RaisePostDataChangedEvent 方法,以觸發(fā)一項(xiàng)與控件相關(guān)的可選事件,該事件能夠指示控件的狀態(tài)在回發(fā)后是否被更改。在實(shí)踐中,只有在 LoadPostData 返回 true 時(shí)才會(huì)調(diào)用 RaisePostDataChangedEvent 方法。
圖 4 顯示的偽代碼顯示了為 System.Web.UI.WebControls.TextBox 控件實(shí)現(xiàn) LoadPostData 方法?;旧?,該方法可將讀取自視圖狀態(tài)的 Text 屬性的值與已發(fā)布值進(jìn)行比較。如果兩個(gè)值不同,則已發(fā)布值將替換當(dāng)前值,并成為控件 Text 屬性的新值。
如您所見(jiàn),已發(fā)布的值被盲目地分配給 Text 屬性,而并未充分考慮字符串的長(zhǎng)度。通過(guò) LoadPostData 方法,每個(gè)控件都可以更新所需數(shù)量的屬性,并且可以交叉檢查對(duì)測(cè)試有意義的所有屬性。如
圖 4 所示,TextBox 實(shí)現(xiàn) LoadPostData 方法,限制了驗(yàn)證,使其只能確保控件為非只讀,進(jìn)而對(duì)新舊文本進(jìn)行比較。
圖 5 所示為一個(gè)全新的 TextBox 控件,其 LoadPostData 方法的實(shí)現(xiàn)稍有不同。重寫(xiě)的方法只是先將已發(fā)布文本截?cái)嘀猎试S的最大長(zhǎng)度,然后再進(jìn)行文本比較。如圖 6 所示,任何超過(guò)最大長(zhǎng)度的文本都會(huì)被自動(dòng)截?cái)?,因此在回發(fā)過(guò)程中不會(huì)再用于生成更長(zhǎng)的結(jié)果。無(wú)論客戶(hù)端瀏覽器的功能如何,都會(huì)出現(xiàn)這種情況。
圖 6 超過(guò)最大長(zhǎng)度的文本會(huì)被“截?cái)唷?(單擊該圖像獲得較大視圖)
仔細(xì)比較
圖 4 和
圖 5 中 LoadPostData 方法的源代碼,您就會(huì)發(fā)現(xiàn)一個(gè)細(xì)微的差別。在
圖 4 中,方法在其基類(lèi)(System.Web.UI.Control 類(lèi))上調(diào)用至 ValidateEvent。在
圖 5 中,同一代碼是通過(guò)調(diào)用 ClientScriptManager 對(duì)象上的 ValidateEvent 而被替換的:
Page.ClientScript.ValidateEvent(Me.UniqueID, String.Empty)
由于 Control 基類(lèi)上的 ValidateEvent 方法是聲明為 Friend(在 C# 內(nèi)部),因此從 System.Web.dll 程序集之外定義的任何類(lèi)是無(wú)法調(diào)用它的。Control 基類(lèi)上的 ValidateEvent 方法的調(diào)用堆棧最終會(huì)調(diào)用 ClientScriptManager 對(duì)象上的 ValidateEvent 方法;ClientScriptManager 對(duì)象的實(shí)例則通過(guò) Page 類(lèi)的 ClientScript 屬性得以公開(kāi)。
ValidateEvent 是 ASP.NET 2.0 中可用于實(shí)現(xiàn)事件驗(yàn)證的一個(gè)工具。事件驗(yàn)證是一項(xiàng)內(nèi)置功能,旨在避免頁(yè)面處理那些不是由頁(yè)面和已注冊(cè)控件專(zhuān)門(mén)生成的事件(和事件參數(shù))。
替換 TextBox 控件
經(jīng)過(guò)一些列操作,現(xiàn)在您獲得了一個(gè)全新的 TextBox 控件。這個(gè)全新的控件可確保任何分配給 Text 屬性的超過(guò)最大長(zhǎng)度的文本都能被檢測(cè)到并得以刪除。您會(huì)在 ASP.NET 頁(yè)中使用此控件嗎?只需向每個(gè)頁(yè)面注冊(cè)該控件并替換出現(xiàn)的所有原始文本框即可。
在 ASP.NET 2.0 中,將以下配置腳本添加到 <pages> 塊的 <controls> 節(jié)下的 web.config 文件中,這樣您可以節(jié)省不少時(shí)間。
<add tagPrefix=”x” namespace=”Samples” assembly=”TextBox” />這段腳本保證 web.config 文件控制的所有頁(yè)面均可自動(dòng)注冊(cè)指定的標(biāo)記和控件。
但目前仍存在一個(gè)問(wèn)題,而且是個(gè)很大的問(wèn)題。那就是如何在新舊文本框之間切換?幸運(yùn)的是,ASP.NET 2.0 中的配置文件內(nèi)提供了一個(gè) <tagMapping> 節(jié):
<pages><tagMapping><add tagType=”System.Web.UI.WebControls.TextBox”mappedTagType=”Dino.Samples.TextBox” /></tagMapping></pages>
<tagMapping> 節(jié)允許您在編譯時(shí)將一種控件類(lèi)型重新映射到另一種控件類(lèi)型。通過(guò)這種重新映射,我們使用被映射的類(lèi)型替代了受配置文件控制的全部頁(yè)面和用戶(hù)控件的原始類(lèi)型根據(jù)前面給出的代碼,任何引用了系統(tǒng) TextBox 的地方均將使用 Dino.Samples.TextBox。您要做的只是編寫(xiě)新控件并編輯 web.config 文件。這種簡(jiǎn)單的做法是不是有些不可思議?但確實(shí)是非常有效的。
毫無(wú)疑問(wèn),重新映射的類(lèi)型必須為繼承自原始類(lèi)型的類(lèi)。還要指出的是,ASP.NET 團(tuán)隊(duì)在 ASP.NET AJAX Extensions 1.0 的 pre-RTM build 中使用了此功能,以便使用可與 UpdatePanel 控件很好兼容的新驗(yàn)證程序控件來(lái)替換原始控件。
最終的解決方案
客戶(hù)最終正確地診斷出 ASP.NET TextBox 控件及其處理已發(fā)布數(shù)據(jù)的方式存在問(wèn)題。他們通過(guò)創(chuàng)建新的 TextBox 控件令人滿(mǎn)意地修復(fù)了問(wèn)題。由于開(kāi)發(fā)人員之前并不了解有更好的方法或通過(guò)聲明的方式來(lái)替換整個(gè)站點(diǎn)的控件,因此他們手動(dòng)替換了所有出現(xiàn)的控件,并將其打包放入一個(gè)用戶(hù)控件中。這樣做是為了盡量降低將來(lái)的更改可能造成的影響。
有了 tagMapping 功能,找到解決方案簡(jiǎn)直易如反掌。使用 tagMapping 這一技巧比較靈活,可以用來(lái)替換錯(cuò)誤的控件或者為現(xiàn)有控件添加新功能。但是請(qǐng)注意,如果重新映射的控件具有了新的屬性或方法,您需要修改源代碼才能使用這些新屬性和方法。
(提到以聲明的方式進(jìn)行映射,ASP.NET 2.0 還具有一個(gè)特性,即包含 <urlMappings> 節(jié)。它是 <configuration> 的直接子級(jí)。<urlMappings> 節(jié)在 ASP.NET 2.0 中是聲明性的,它對(duì)應(yīng)的是 HttpContext 對(duì)象上的 RewritePath 方法。)
總之,您要注意,在設(shè)置了 MaxLength 之后,原始 ASP.NET TextBox 控件將無(wú)法對(duì) Text 屬性的任何發(fā)布值進(jìn)行裁剪。但本專(zhuān)欄通過(guò)修改控件解決了這一限制,應(yīng)該對(duì)您解決這一問(wèn)題有所幫助。您可以在 web.config 文件中新加一行簡(jiǎn)單的代碼,通過(guò)聲明的方式將其插入應(yīng)用程序。