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