|
作者:Scott Hanselman 首席工程師 Corillian Corporation
2003 年 11 月
摘要: ASP.NET 1.1 新增了 ValidateRequest 屬性來保護您的站台,以防跨站台執行指令碼。但如果您的網站仍在執行 ASP.NET 1.0 該怎麼辦呢?Scott Hanselman 向您解說如何新增類似的功能到您的 ASP.NET 1.0 站台。(列印共 12 頁)
目錄
問題所在 給 IL 傢伙 C# 表達能力 HttpModule 程式設計師的用意 安裝和組態 最終結果 結論
問題所在
我有一個客戶把網站部署在 Microsoft® ASP.NET 和 Microsoft® .NET Framework 1.0 上。這是個規模很大的網站,且他們是大型的客戶,大型客戶的作業速度想當然是較為「緩慢」。在 ASP.NET/Framework 1.1 推出時,我們正值大型的部署過程中。工作團隊認為在快完工時遷移到 ASP.NET/Framework 1.1 過於冒險,因此我們決定在當年稍晚再移轉到 ASP.NET/Framework 1.1。然而,既然我們建置的複雜電子銀行網站跨越了許多營業線,而且處理的是客戶的資產,安全性理所當然是首要工作。客戶的要求之一,就是要我們積極防範跨站台執行指令碼 (通常稱為「XSS」) 的攻擊。
XSS 的入侵十分要命,其中的 133t hx0r (高明的駭客) 或「小型指令碼」會嘗試透過在 Web Form 中輸入 JavaScript,或透過將指令碼編碼至 URL 的參數中,來擷取個人資訊,蒙騙網站,使它進行一些不該做的事。其中一個簡單的例子就是含有單一文字方塊和按鈕的 Web Form。使用者會將名稱輸入至文字方塊並送出該表單。頁面接著經由字串串連、String.Format、Response.Write 或透過伺服器端的標籤指出「Hello,firstname (名字)」。

[圖 1] 輸入文字;看起來好像蠻安全的
既然頁面會拿使用者的輸入直接「重述」,如果我輸入一個咒罵的字,則也會獲得截然不同的問候!但要是使用者輸入像是「<script>alert('bad stuff happens');</script>」的指令碼,而不是輸入他們的名稱,這會發生什麼事呢?背後的程式碼如下: if (this.IsPostBack) Response.Write("Hello " + this.TextBox1.Text);
您可以看到文字方塊的內容會直接被寫出回應資料流,而 JavaScript 會在使用者的瀏覽器上進行評估。這只是一個普通的例子,但假設惡意的 JavaScript 內所包含的程式碼是要存取使用者的 Cookie 集合,或是將發佈的表單重新導向到另一個站台,結果又如何呢?

[圖 2] 在預期文字的地方輸入 JavaScript

[圖 3] JavaScript 會在獲得回應之後執行
為單純起見,我們不希望建置特別複雜的 Web 層或商業邏輯來處理有人在表單欄位輸入 JavaScript 或其他矇騙方式的情況。我們希望以更集中化的方式,像是篩選器,在 HTTP 工作者要求鏈早期,肯定是在實際頁面執行之前來處理 XSS。而 ASP.NET 1.1 就包含了一個新的 @Page 指示詞可以這麼做!在預設情況下,輸入驗證會啟動,而且可使用 @Page 指示詞的 ValidateRequest 屬性加以控制。 <%@ Page language="c#" Codebehind="WebForm1.aspx.cs"
ValidateRequest="true" AutoEventWireup="false"
Inherits="Junk.WebForm1" %>
ASP.NET 1.1 要求驗證會抓出 Cookie 集合、QueryString 和表單張貼 (Form Post) 中的惡意指令程式碼。它會按照一份具有潛在危險的清單來檢查所有的輸入資料。若您擔心這類的驗證會影響到使用功能,我只能說,如果使用者會在您的表單欄位內輸入 JavaScript,他們也不會是您想要的那種使用者。ValdidateRequest=true 完全不會阻礙系統對於使用者的回應。如果在部份輸入資料內偵測到惡意指令碼,即會擲出 HttpRequestValidationException。您一定可以在 Global.asax 中發現到此錯誤,並將預設的錯誤頁面取代成您要發出的警告訊息 (如果想要的話)。
ASP.NET 1.1 免費提供此強大的篩選器雖然很棒,但對我和客戶即將推出的 ASP.NET 1.0 網站來說,卻一點幫助也沒有。我該怎麼在客戶完成升級之前,有效保護 ASP.NET 1.0 對抗跨站台執行指令碼呢?我們考慮了一些辦法,像是撰寫一些規則運算式以及搜尋 Application_BeginRequest 的 HTTP 標頭,但沒有一個是理想的。另一個考量是我們身為一家電子金融公司,不應該將心血花費在製造預防跨站台執行指令碼攻擊的工作上。所以我沒有必要嘗試重新自創。
此時,我才發現眼前就有現成的解決方案 - ASP.NET 1.1 已經解決了此問題,所以我只需要反向操作就可以了。於是我決定將現有的 ASP.NET 1.1 反向移植為 ASP.NET 1.0。
給 IL 傢伙 C# 表達能力
為了探索 ASP.NET 1.1 內部的運作,我需要一個比 ILDASM.EXE 稍微高階的工具,即包含在 .NET Framework SDK 中的 .NET 反組譯工具。如果我更聰明的話,或許可以僅使用 ILDASM 就將 System.Web 分解,但讀取 IL 太辛苦了,而且我的時間不多。我在 Lutz Roeder's Reflector 找到了該項工具。Reflector 是一種物件瀏覽器,給予您「基底類別庫 (BCL)」提供的所有命名空間和類別的絕佳樹狀檢視。

[圖 4] 在 Reflector 中查看 CrossSiteScriptingValidation 類別

[圖 5] 探索 CrossSiteScriptingValidation 類別的原始程式碼
不過,Reflector 真正出眾的地方在於它解編 (Decompile) .NET 組件和呈現結果的能力,它所呈現的結果不會是 IL,而是會以相等於 C# 或 Microsoft®Visual Basic®.NET 的程式碼。當然部份的詳細內容會在過程中遺失,例如本機變數名稱,但這就是現實生活 (也是程式碼),有利就有弊。
於是,我在 System.Web 中搜尋,便找到一個叫做 CrossSiteScriptingValidation 的內部類別。這看起來應該是我想要找的。這就是執行驗證的地方,像是 IsDangerousString 或 IsDangerousScriptString。CrossSiteScriptingValidation 中的所有方法都會傳回布林值;如果傳回多項「True」,就表示字串具有危險性。但我們評估的是什麼字串,還有這個公用程式類別是由誰呼叫的?對我來說解答應該是落在 HttpRequest 身上,因為我們是嘗試要驗證所有的要求。
HttpRequest 包含 Form 變數、Cookies,以及 QueryString 的集合。這些物件是型別 NameValueCollection (Cookies 實際上是 HttpCookieCollection,這另外還有一些瑣碎的東西),所以如果您的 URL 是 http://localhost/junk/test.aspx?id=3,則 QueryString 集合會包含值為 3 的名稱 ID 項目。HttpRequest 對此集合有一個公用 get 屬性,因此當您編碼 Request.QueryString 時,等於是在存取該屬性。這裡正是發生所有一切的地方。當對第一個名稱存取該集合時,會透過 ValidateNameValueCollection 來檢查危險的字串。如果未擲出 HttpRequestValidationException,會傳回現在有效的 QueryString,而且會設定旗標來避免再度耗用資源來檢查集合。 if (this._flags[1] != null)
{
this._flags[1] = 0;
this.ValidateNameValueCollection(this._queryString,
"Request.QueryString");
}
return this._queryString;
像這類的驗證程式碼都是經過 ASP.NET 1.1 中的 HttpRequest 集合。毫無疑問地,既然我要一個執行在 ASP.NET 1.0 上的解決方案,而且不能覆寫 Forms、QueryString 和 Cookie 集合的行為,則我就需要另外在呼叫堆疊中找機會來驗證該集合。
HttpModule
HttpModule 看似是完美的選擇。這是一個實作 IHttpModule 的簡單自訂公用類別。IHttpModule 介面只包含兩種方法,Init() 和 Dispose()。ASP.NET 會將 HttpApplication 用作唯一的參數呼叫 Init() 一次,而這正是我將任何事件處理常式勾搭上應用程式的機會。基於效能理由,我希望確認我的跨站台執行指令碼驗證程式碼只執行一次,並在頁面和相關的商業邏輯之前獨立執行。
HttpApplication 具有這些事件,且會依以下所示的順序引發:
- BeginRequest
- AuthenticateRequest
- AuthorizeRequest
- ResolveRequestCache
- [此時會建立一個處理常式 (對應於要求 URL 的頁面)。]
- AcquireRequestState
- PreRequestHandlerExecute
- [會執行處理常式。在我們案例中,即為 Page]
- PostRequestHandlerExecute
- ReleaseRequestState
- [回應篩選器 (如果有的話),並篩選輸出。]
- UpdateRequestCache
- EndRequest
看起來執行驗證程式的時機是在 PreRequestHandlerExecute 事件處理常式期間,就在呈現頁面本身之前。如果我發現有潛在危險的東西而擲出例外,該頁面將永遠不會執行。這是預期的行為。
因此,我建立了一個實作 IHttpModule、名為 ValidateInput 的類別,並在 Init() 針對 PreRequestHandlerExecute 連上 EventHandler 來呼叫我的自訂函式 ValidateRequest。它將位於 ValidateRequest 內部,我將在當中呼叫從 ASP.NET 1.1 轉移過來的函式。
我也會加入一個快速的版本檢查功能,來確保沒有人嘗試在 ASP.NET 1.1 上使用此模組。我可不希望有人在我們升級到 1.1 的時候忘了移除這個模組。 public class ValidateInput : IHttpModule
{
HttpContext context;
HttpApplication application;
public ValidateInput(){}
public void Init(HttpApplication app)
{
Version v = System.Environment.Version;
if (v.Major != 1 && v.Minor != 0)
throw new NotSupportedException(@"The ValidateInput HttpModule is
not supported on this version of ASP.NET.
Remove it from your Web.config file!");
app.PreRequestHandlerExecute += new EventHandler(this.ValidateRequest) ;
}
我將 PreRequestHandlerExecute 連上類別的 ValidateRequest 方法。既然我無法攔截至 Forms、QueryString 和 Cookies 集合,就需要在此處進行所有的要求驗證,來確保只讓通過驗證的要求傳遞到我的 Page 處理常式。 public void ValidateRequest(Object src, EventArgs e)
{
//Store away what may be useful during this Request...
application = (HttpApplication)src;
context = application.Context;
this.ValidateNameValueCollection(context.Request.Form, "Request.Form");
this.ValidateNameValueCollection(context.Request.QueryString,
"Request.QueryString");
this.ValidateCookieCollection(context.Request.Cookies);
}
在 ValidateRequest 中,我呼叫了我本身的 ValidateNameValueCollection 和 ValidateCookieCollection 實作。每個都會轉到已經過剖析且表示 Form POST 資料的集合,包括預先剖析的 Cookies 和 QueryString。
知道剖析這項 HTTP 標頭資料並組織成 NameValueCollections 沒什麼危險這點很重要,因為如此才能確定要求中任何潛在的惡意資料還沒到達 Page 處理常式或瀏覽器。除此之外,如果我選擇 BeginRequest 應用程式事件來取代 PreRequestHandlerExecute,就會需要自行剖析未經處理的 HTTP 要求。所以,我是魚與熊掌兼得,沉悶的剖析工作已經為我完成 (而且已經是測試得當的程式碼),而頁面還沒有執行,讓我有時間可以擲出例外並中止要求的執行。
接下來我將所有其他的 Helper 函式提取至新類別中,包括 Reflector 的 IsDangerousExpressionString、IsDangerousOnString、IsDangerousScriptString、IsDangerousString 和 IsAtoZ。另外值得一提的是 Reflector 所解編的 C# 程式碼,實際上是包含在組件內之 IL 的新 C# 表示。本機變數名稱已經過變更,而過去曾是迴圈的東西現在可能是一系列 goto 和 if 陳述式。所以請不要根據此 IL 表示,斷章取義地批評程式碼的作者!記住編譯器在產生最終的 IL 時,需要有點彈性,最重要的是程式設計師的用意。我稍後會在下面談到這點。

[圖 6] 瞧瞧 IsAtoZ 方法
現在,我們將需要一個從 ApplicationException (適當地命名為 HttpRequestValidiationException) 衍生出來的 Exception 類別。這恰巧是 ASP.NET 1.1 使用的相同名稱,但是在不同的命名空間裡。如果有任何看起來有潛在危險的指令碼出現在 HttpRequest 中,將會擲出此例外。您可以選擇是否要顯示例外頁面或記錄該例外。有些人可能會覺得潛在的指令碼攻擊是件大事,而會選擇以不同的方式來處理此例外。不管採取什麼方式,請規劃例外處理對策。
程式設計師的用意
我想要稍微談一下有關程式設計師的用意。此處實際解編的是程式設計師的用意。我們實際上並不是在呈現 C# 原始程式碼 (原始作者所編寫的內容)。在解編成 IL,接著轉換成 IL 的 C# 表示時,事實上並不一樣。舉例來說,從 IsDangerousOnString 的一小段程式碼在 Reflector 中看起來像這樣: goto L_0045;
L_0040:
index = (index + 1);
L_0045:
if (index >= len)
{
goto L_005E;
}
if (CrossSiteScriptingValidation.IsAtoZ(s[index]))
{
goto L_0040;
}
這對一般的程式設計師來說很難懂,但卻正確地傳達了程式設計師的用意。但該用意到底是什麼?我們能「回溯」的程式碼有限。甚至,它可能只是在呼叫 String.IndexOf 內嵌程式。不過,我們可以將它重寫成如下 (或數十種其他方法),以便我們了解: //Programmer intent: look for non-alphas...
while (index < len)
{
if (!CrossSiteScriptingValidation.IsAtoZ(s[index]))
break;
index++;
}
請記住,只有您能夠評估「Goto 的風險」,而非編譯器!也請注意此程式碼也可以表示為「For」迴圈或一些其他的迴圈建構函式,但還是能正確傳達用意。
安裝和組態
為了將 ValidateInputASPNET10 安裝在 Web 伺服器上,我們只需要將之新增到在我們的 web.config 中所設定的 httpModules 清單中。而組件,在我們的案例中即 ValidateInputASPNET10.dll,需要存留在站台 (以及任何其他我們希望在領區內保護的站台) 的 \bin 資料夾中。 <configuration>
<system.web>
<httpModules>
<add name="ValidateInput"
type="Corillian.Web.ValidateInput,ValidateInputASPNET10" />
</httpModules>
</system.web>
</configuration>
最終結果
當我將 HttpModule 加入 web.config 後,我不需要重新編譯便能夠發行相同的 ASP.NET 應用程式,因為 HttpModule 是它自己的組件和 Microsoft®Visual Studio®.NET 專案。在啟動之時,ASP.NET 會在新的 ValidateInputASPNET10 HttpModule 上呼叫 Init(),而且會鏈結到 PreRequestHandlerExecute 事件。假若我像以前一樣嘗試輸入 JavaScript 到 Form (或 QueryString 或 Cookies 集合) 中,會出現這則錯誤訊息,宣告 HttpRequestValidationException。請注意部份的 JavaScript 會顯示出來,但只是一部份;我們可不希望錯誤訊息輸出並執行我們要正嘗試對抗的 JavaScript。

[圖 7] 保護您的網站以防指令碼輸入
注意 請記住,解編主要應該用於偵錯及個人教育用途。請確實注意智慧財產權的相關規定,並記住不要因為組件比 C++ 應用程式更容易解編,就表示我們能自由偷竊程式碼。如果您關心您的程式碼和智慧財產權,請參考一下 Visual Studio .NET 2003 隨附的 Dotfuscator Community Edition。
結論
跨站台執行指令碼是在建立 ASP.NET 網站時,需要多加注意的多種入侵方式之一。駭客可利用這項技術在伺服器上執行程式碼,或許會導致資料遺失,或更嚴重的是客戶資訊遭竊。防禦性的程式設計的觀念,就是要保衛自己以對抗這些攻擊。在輸入中加入驗證,如本文所示般,是邁向保護 貴網站的第一步。
關於作者
Scott Hanselman 是 Corillian Corporation,一家提供電子金融服務的公司的首席設計師。他在以 C、C++、Visual Basic、COM 及目前的 Visual Basic .NET 和 C# 開發軟體方面擁有超過十年的相關經驗。Scott 非常榮幸在過去三年來受任 MSDN 俄勒岡州的波特蘭地區主管一職,並同時在波特蘭和西雅圖兩地發展 Developer Days 及 Visual Studio .NET 產品推出活動的內容並發表演說。Scott 另外還在四個城市的 Microsoft®Windows Server®2003 和 Visual Studio .NET 2003 產品推出活動中演講。他在國際間陸續發表跟 Microsoft 技術有關的演講,並與其他作者合著了兩本 Wrox Press 的書。在 2001 年,Scott 於全國 15 個城市巡迴演講有關 Microsoft、Compaq 和 Intel 採用 Microsoft 技術並傳佈良好的設計實例。今年 Scott 在太平洋西岸 4 個城市的 Windows Server 2003 產品推出活動、美國及馬來西亞的 TechED,以及奧蘭多的 ASPLive 中發表演說。有關他對於 .NET, 程式設計和 Web 服務的各項領悟,請參閱 http://www.computerzen.com/。
|