ASP.NET 安全性

保護 ASP.NET 應用程式的安全

Adam Tuliper

在上一期中,我討論了構建 Web 應用程式安全性的重要性,並介紹了包括 SQL 注入和參數篡改在內的一些攻擊類型,以及如何防範這些類型的攻擊 (msdn.microsoft.com/magazine/hh580736)。 在本文中,我將深入探討以下兩種更常見的攻擊,以説明完善我們的應用程式保護體系:跨網站腳本 (XSS) 和跨網站請求偽造 (CSRF)。

You might be wondering: 只使用生產安全掃描程式不就行了嗎? 掃描程式是查找容易實現的目標的絕佳工具,它們尤其適合查找應用程式和系統組態問題,但它們無法像您一樣瞭解您的應用程式。 因此,您必須熟悉潛在的安全問題,花點時間檢查應用程式並為您的軟體發展生命週期構建安全性。

跨網站腳本

簡介 XSS 攻擊是指將腳本惡意注入使用者的流覽會話,這通常在使用者不知情的情況下發生。 因為一些事故的發生,許多人已經非常熟悉這些類型的攻擊,在這些事故中,某大型社交網站受到攻擊,其使用者發佈了他們未授權的消息。 如果攻擊者發佈惡意腳本並且他可以讓流覽器執行該腳本,則該腳本將在受害者會話的上下文中執行,這基本上使攻擊者能夠對 DOM 執行其所需的任何操作,包括顯示偽造的登錄對話方塊或盜取 Cookie。 此類攻擊甚至可以在當前頁面上安裝 HTML 鍵記錄器,以便持續將來自該視窗的輸入內容發送到遠端網站。

參數篡改的使用方式 攻擊者通過多種方法利用 XSS,所有這些方法都需要有未封裝或封裝不當的輸出。 下麵我們以需要向最終使用者顯示簡單的狀態訊息的應用程式為例。 通常,該消息通過查詢字串傳遞,如圖 1 所示。

Query String Message
圖 1 查詢字串消息

該方法通常在重定向之後使用以便向使用者顯示某種狀態,例如圖 1 中的“Profile Saved”(設定檔已保存)消息。 該消息從查詢字串讀取並輸出到頁面中。 如果輸出內容未經過 HTML 編碼,任何人都可以輕鬆地注入 JavaScript 來代替狀態訊息。 此類攻擊被視為反射 XSS 攻擊,因為無論查詢字串中有什麼內容,它都會呈現在頁面中。 在持續性攻擊中,惡意腳本通常存儲在資料庫或 Cookie 中。

圖 1 中,您可以看到此 URI 使用了一個 msg 參數。 此 URI 的網頁將包含如下代碼以便將變數寫入頁面而不進行編碼:

    <div class="messages"> <%=Request.QueryString["msg"]%></div>

如果將“Profile Saved”替換為圖 2 中顯示的腳本,流覽器中將會彈出查詢字串中包含的腳本的警報功能。 此處的漏洞利用的關鍵在於結果沒有經過 HTML 編碼,因此流覽器實際上將 <script> 標記解析為有效 JavaScript 並執行。 這顯然不是開發人員的初衷。

Injecting Script into the URI
圖 2 將腳本注入 URI 中

那就彈出警報吧—有什麼大不了的? 下麵我們進一步看看該示例,如圖 3 所示。 請注意,為了便於說明,我已經縮短了此處的攻擊;此語法並不完全正確,但些許的修改會使其成為真正的攻擊。

Creating a Malicious Attack
圖 3 創建惡意攻擊

類似這樣的攻擊可能導致向使用者顯示偽造的登錄對話方塊,使用者會輕易地在其中輸入他們的憑據。 在本例中,將會下載一個遠端腳本,預設流覽器安全設置通常允許這種行為。 理論上,只要可以將尚未編碼或淨化的字串回顯給使用者,就可能會發生此類攻擊。

圖 4 顯示了在開發網站上使用類似腳本的攻擊,該網站允許使用者留下有關其產品的意見。 不過,有些人不是留下真正的產品評論,而是輸入了惡意 JavaScript 作為意見。 該腳本現在將向訪問該網頁的每個使用者顯示一個登錄對話方塊,收集憑據然後將憑據發送至遠端網站。 這是一種持續性 XSS 攻擊;腳本存儲在資料庫中,並對每個訪問該頁面的人重複執行。

A Persistent XSS Attack Showing a Fake Dialog
圖 4 顯示偽造對話方塊的持續性 XSS 攻擊

攻擊者利用 XSS 的另一方法是通過使用 HTML 元素,例如 HTML 標記中允許使用動態文本時,如下所示:

    <img onmouseover=alert([user supplied text])>

如果攻擊者注入“onmouseout=alert(docu­ment.cookie)”之類的文本,這將在訪問 Cookie 的流覽器中創建以下標記:

    <img onmouseover=alert(1) onmouseout=alert(document.cookie) >

沒有用於潛在地篩選輸入內容的“<script>”標記,也沒有需要封裝的內容,但這是可以讀取 Cookie(可能是一個身份驗證 Cookie)的完全有效的 JavaScript。 可根據具體情況採取具體措施提高安全性,但由於存在風險,最好禁止任何使用者輸入訪問此處的這些內嵌代碼。

防範 XSS 的方法 嚴格遵守以下規則將有助於防範應用程式中的大多數(即使不是全部)XSS 攻擊:

  1. 確保所有輸出內容都經過 HTML 編碼。

  2. 禁止使用者提供的文本進入任何 HTML 元素屬性字串。

  3. 根據 msdn.microsoft.com/library/3yekbd5b 中的概述,檢查 Request.Browser,以阻止應用程式使用 Internet Explorer 6。

  4. 瞭解您的控制項的行為以及其輸出是否經過 HTML 編碼。 如果未經過 HTML 編碼,則對進入控制項的資料進行編碼。

  5. 使用 Microsoft 防跨網站腳本庫 (AntiXSS) 並將其設置為您的預設 HTML 編碼器。

  6. 在將 HTML 資料保存到資料庫之前,使用 AntiXSS Sanitizer 物件(該庫是一個單獨的下載檔案,將在下文仲介紹)調用 GetSafeHtml 或 GetSafeHtmlFragment;不要在保存資料之前對資料進行編碼。

  7. 對於 Web 表單,不要在網頁中設置 EnableRequestValidation=false。 遺憾的是,Web 上的大多數使用者組文章都建議在出現錯誤時禁用該設置。 該設置的存在是有原因的,例如,如果向伺服器發送回“<X”之類的字元組合,該設置將阻止請求。 如果您的控制項將 HTML 發送回伺服器並收到圖 5 所示的錯誤,那麼理想情況下,您應該在將資料發佈到伺服器之前對資料進行編碼。 這是 WYSIWYG 控制項的常見情形,現今的大多數版本都會在將其 HTML 資料發佈回伺服器之前對該資料進行正確編碼。

    Server Error from Unencoded HTML
    圖 5 未編碼的 HTML 返回的伺服器錯誤

  8. 對於 ASP.NET MVC 3 應用程式,當您需要將 HTML 發佈回模型時,不要使用 ValidateInput(false) 來關閉請求驗證。 只需向模型屬性中添加 [AllowHtml] 即可,如下所示:

public class BlogEntry
{
  public int UserId {get;set;}
  [AllowHtml]
  public string BlogText {get;set;}
}

有些產品會嘗試檢測字串中的 <script> 和其他字片語合或規則運算式模式,以試圖檢測 XSS。 這些產品可提供額外的檢查但並不完全可靠,因為攻擊者創建了許多變體。 看看 ha.ckers.org/xss.html 中的 XSS 速查表,瞭解一下檢測的困難程度。

為了理解修復方法,假設攻擊者通過查詢字串或此處所示的表單欄位注入了一些腳本,這些腳本最終進入我們應用程式的變數中:

string message = Request.QueryString["msg"];

or:

string message = txtMessage.Text;

請注意,儘管 TextBox 控制項對其輸出內容進行 HTML 編碼,但當您從代碼中讀取其 Text 屬性時,它不會對該屬性進行編碼。 無論採用上面哪行代碼,消息變數中都會得到以下字串:

message = "<script>alert('bip')</script>"

在包含類似如下代碼的網頁中,只是因為將以下文本寫入頁面,JavaScript 就將在使用者的流覽器中執行:

    <%=message %>

對輸出內容進行 HTML 編碼會終止此類攻擊。 6 顯示了用於對危險資料進行編碼的主要選項。

這些選項可防範示例中所示的攻擊類型,應在您的應用程式中使用這些選項。

圖 6 HTML 編碼選項

ASP.NET(MVC 或 Web 表單) <%=Server.HtmlEncode(message) %>
Web 表單(ASP.NET 4 語法) <%: message %>
ASP.NET MVC 3 Razor @message
資料綁定

遺憾的是,資料綁定語法並不包含內置的編碼語法;在下一版本的 ASP.NET 中,它將以 <%#:%>. 到那時,使用:

<%# Server.HtmlEncode(Eval("PropertyName")) %>

更好地進行編碼

從 Microsoft.Security.Application 命名空間中的 AntiXSS 庫:

Encoder.HtmlEncode(message)

必須瞭解您的控制項,這一點很重要。 哪些控制項對您的資料進行 HTML 編碼?哪些控制項不進行編碼? 例如,TextBox 控制項會對呈現的輸出內容進行 HTML 編碼,而 LiteralControl 則不進行編碼。 這是一個重要的區別。 分配有以下內容的文字方塊:

yourTextBoxControl.Text = "Test <script>alert('bip')</script>";

正確地通過以下形式將文本呈現給頁面:

    Test &lt;script&gt;alert(&#39;bip&#39;)&lt;/script&gt;

與此相反:

yourLiteralControl.Text = "Test <script>alert('bip')</script>";

導致頁面上顯示一個 JavaScript 警報,從而確認 XSS 漏洞。 修復代碼非常簡單:

yourLiteralControl.Text = Server.HtmlEncode(
    "Test <script>alert('bip')</script>");

在 Web 表單中使用資料綁定時稍微複雜一些。 請看以下示例:

    <asp:Repeater ID="Repeater1" runat="server">
        <ItemTemplate>
          <asp:TextBox ID="txtYourField" Text='<%# Bind("YourField") %>'
            runat="server"></asp:TextBox>
        </ItemTemplate>
      </asp:Repeater>

這是否存在漏洞? No, it’s not. 儘管內嵌代碼看起來好像能夠輸出腳本或中斷控制項引用,但它確實經過編碼。

出現這種情況該怎麼辦:

    <asp:Repeater ID="Repeater2" runat="server">
      <ItemTemplate>
        <%# Eval("YourField") %>
      </ItemTemplate>
    </asp:Repeater>

簡介 是的,確實如此。 資料綁定語法 <%# %> 不執行 HTML 編碼。 這是修復代碼:

    <asp:Repeater ID="Repeater2" runat="server">
      <ItemTemplate>
        <%#Server.HtmlEncode((string)Eval("YourText"))%>
      </ItemTemplate>
    </asp:Repeater>

請注意,如果在該方案中使用 Bind,則由於 Bind 在後臺作為兩個單獨的調用進行編譯的方式不同,因此您將無法打包 Server.HtmlEncode。 這將失敗:

    <asp:Repeater ID="Repeater2" runat="server">
      <ItemTemplate>
        <%#Server.HtmlEncode((string)Bind("YourText"))%>
      </ItemTemplate>
    </asp:Repeater>

如果使用 Bind 並且不將文本分配給 HTML 編碼的控制項(如 TextBox 控制項),可考慮改用 Eval,以便可以像在上例中那樣,將調用打包到 Server.HtmlEncode 中。

ASP.NET MVC 中不存在這種資料綁定概念,因此您需要知道 HTML 説明程式是否進行編碼。 標籤和文字方塊的説明程式會執行 HTML 編碼。 例如,下麵的代碼:

@Html.TextBox("customerName", "<script>alert('bip')</script>")
@Html.Label("<script>alert('bip')</script>")

呈現為:

    <input id="customerName" name="customerName" type="text"
      value="&lt;script>alert(&#39;bip&#39;)&lt;/script>" />
    <label for="">&lt;script&gt;alert(&#39;bip&#39;)&lt;/script&gt;</label>

我在前面提到過 AntiXSS。 AntiXSS 庫目前處於版本 4.1 Beta 1 階段,它經歷了一次非常棒的重新編寫過程,並且就安全性而言,它提供了比 ASP.NET 附帶的編碼器更好的 HTML 編碼器。 並不是說 Server.HtmlEncode 有什麼問題,只是它側重于相容性而不是安全性。 AntiXSS 使用不同的方法進行編碼。 有關詳細資訊,請訪問 msdn.microsoft.com/security/aa973814

可從 bit.ly/gMcB5K 處獲得 Beta 版。 檢查 AntiXSS 的 Beta 階段是否已結束。 如果沒有,您需要下載代碼並進行編譯。 Jon Galloway 在 bit.ly/lGpKWX 中發佈了有關此內容的精彩文章。

若要使用 AntiXSS 編碼器,您只需進行以下調用即可:

    <%@ Import Namespace="Microsoft.Security.Application" %>
    ...
    ...
    <%= Encoder.HtmlEncode(plainText)%>

ASP.NET MVC 4 添加了一個非常不錯的新功能,它允許您替代預設 ASP HTML 編碼器,並且您可以使用 AntiXSS 編碼器來代替。 在撰寫本文時,您需要版本 4.1;因為它當前處於 Beta 階段,您必須下載代碼,進行編譯,然後將庫作為引用添加到應用程式中—總共需要五分鐘時間。 然後,在您的 web.config 中,向 <system.web> 部分添加下麵一行:

<httpRuntime encoderType=
  "Microsoft.Security.Application.AntiXssEncoder, AntiXssLibrary"/>

現在,通過圖 6 中列出的任意語法(包括 ASP.NET MVC 3 Razor 語法)進行的任何 HTML 編碼調用都將通過 AntiXSS 庫進行編碼。 可插入功能的情況如何?

該庫還包括一個可用於在將 HTML 存儲到資料庫之前清理 HTML 的 Sanitizer 物件,如果向使用者提供用於編輯 HTML 的 WYSIWYG 編輯器,則該物件將非常有用。 該調用會嘗試將腳本從字串中刪除:

using Microsoft.Security.Application;
...
...
string wysiwygData = "before <script>alert('bip ')</script> after ";
string cleanData = Sanitizer.GetSafeHtmlFragment(wysiwygData);
This results in the following cleaned string that can then be saved to the database:
cleanData = "before  after ";

跨網站請求偽造 (CSRF)

簡介 跨網站請求偽造,即 CSRF(讀作 sea-surf),是指有人利用您的流覽器和網站之間的信任關係使用無辜使用者的會話執行命令時發生的攻擊。 如果不了解詳情,很難想像這種攻擊是如何發生的,因此我們要詳細介紹一下。

參數篡改的使用方式 假設小王以 PureShoppingHeaven 網站管理員的身份通過驗證。 PureShoppingHeaven 有一個 URL 僅限管理員訪問並允許在 URL 中傳遞資訊以執行操作,例如創建新使用者,如圖 7 所示。

Passing Information on the URL
圖 7 在 URL 中傳遞資訊

如果攻擊者可以通過任何方法讓小王請求此 URL,攻擊者的流覽器將從伺服器請求此 URL 併發送小王的流覽器中可能已經緩存或正在使用的任何身份驗證資訊(例如身份驗證 Cookie 或其他身份驗證權杖,包括 Windows 身份驗證)。

這是一個簡單的示例,但 CSRF 攻擊可能比這複雜得多,除 GET 請求外,還可以納入表單 POST,並且可以同時利用 XSS 之類的其他攻擊。

假設小王訪問一個有漏洞並且漏洞已經被利用的社交網站。 或許攻擊者通過 XSS 漏洞在頁面上放置了一些 JavaScript,這些 JavaScript 現在使用小王的會話請求 AddUser.aspx URL。 小王訪問網頁後來自 Fiddler (fiddler2.com) 的以下轉儲表明,流覽器還發送一個自訂的網站身份驗證 Cookie:

GET http://pureshoppingheaven/AddUser.aspx?userName=hacked&pwd=secret HTTP/1.1
Host: pureshoppingheaven
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)
Cookie: CUSTOMERAUTHCOOKIE=a465bc0b-e1e2-4052-8292-484d884229ab

這一切都在小王不知情的情況下發生。 必須明白,流覽器將發送任何有效 Cookie 或身份驗證資訊,這是設計使然。 您是否注意到,您的電子郵件用戶端預設情況下通常不載入圖像? 其中一個原因就是為了防範 CSRF。 如果您收到一封 HTML 格式的電子郵件(其中包含類似如下的內嵌影像標記),則此 URL 將會被請求,並且伺服器將會執行該操作(如果您已經通過該網站的身份驗證):

    <img src='yoursite/createuser.aspx?id=hacked&pwd=hacked' />

如果您碰巧是“yoursite”上已通過身份驗證的管理員,流覽器將會順暢地隨任何憑據一起發送該 GET 請求。 伺服器將其視為已驗證使用者的有效請求,並將在您不知情的情況下執行該請求,因為沒有有效的圖像回應需要呈現在您的電子郵件用戶端中。

防範 CSRF 的方法 若要防範 CSRF,您首先需要遵循一些規則:

  1. 確保攻擊者不能通過簡單地按一下 GET 請求連結來重播請求。 針對 GET 請求的 HTTP 規範意味著 GET 請求應該只用于檢索,而不應該用於狀態修改。
  2. 確保在攻擊者已使用 JavaScript 類比表單 POST 請求的情況下不能重播請求。
  3. 阻止通過 GET 執行的任何操作。 例如,禁止通過 URL 創建或刪除記錄。 理想情況下,這些措施需要一些使用者交互。 儘管這種方法不能防範更精明的、基於表單的攻擊,但它可以限制大量較容易實現的攻擊,例如電子郵件圖像示例中描述的攻擊類型以及 XSS 遭到破壞的網站中嵌入的基本連結。

通過 Web 表單防範攻擊的處理方式與 ASP.NET MVC 略有不同。 使用 Web 表單,可以對 ViewState MAC 屬性進行簽名,這有助於防範偽造,只要您不設置 EnableViewStateMac=false。 您還需要使用當前使用者會話對 ViewState 進行簽名,並防止將 ViewState 傳遞到查詢字串中以阻止所謂的一鍵式攻擊(參閱圖 8)。

圖 8 防範一鍵式攻擊

void Page_Init(object sender, EventArgs e)
{
  if (Session.IsNewSession)
  {
    // Force session to be created;
    // otherwise the session ID changes on every request.
Session["ForceSession"] = DateTime.Now;
  }
  // 'Sign' the viewstate with the current session.
this.ViewStateUserKey = Session.SessionID;
  if (Page.EnableViewState)
  {
    // Make sure ViewState wasn't passed on the querystring.
// This helps prevent one-click attacks.
if (!string.IsNullOrEmpty(Request.Params["__VIEWSTATE"]) &&
      string.IsNullOrEmpty(Request.Form["__VIEWSTATE"]))
    {
      throw new Exception("Viewstate existed, but not on the form.");
    }
  }
}

我在此處分配隨機會話值的原因是確保建立會話。 您可以使用任何臨時會話識別字,但在您實際創建會話之前,ASP.NET 會話 ID 會隨每個請求而發生變化。 此處不能讓會話 ID 隨每個請求發生變化,因此您必須通過創建新會話將其固定下來。

ASP.NET MVC 包含它自己的一組內置説明程式,以使用隨請求傳入的獨特權杖來防範 CSRF。 這些説明程式不僅使用必需的隱藏表單欄位,而且使用一個 Cookie 值,從而增加了偽造請求的難度。 這些保護措施很容易實現,而且絕對有必要融入您的應用程式中。 若要在視圖中的 <form> 內添加 @Html.AntiForgery­Token(),請執行以下操作:

@using (Html.BeginForm())
{
  @Html.AntiForgeryToken();
  @Html.EditorForModel();
  <input type="submit" value="Submit" />
}
Decorate any controllers that accept post data with the [Validate­AntiForgeryToken], like so:
[HttpPost]
[ValidateAntiForgeryToken()]
public ActionResult Index(User user)
{
  ...
}

瞭解漏洞

本文介紹了跨網站腳本和跨網站請求偽造,這是駭客攻擊 Web 應用程式的兩種常見方法。 結合上個月介紹的兩種攻擊方法(SQL 注入和參數篡改),您現在應該對應用程式如何受到攻擊有了充分的瞭解。

您還看到為應用程式構建安全性以防範一些最常見的攻擊是多麼容易。 如果您已經為軟體發展生命週期構建了安全性,那太好了! 如果您還沒有這麼做,那麼現在開始構建是最好不過的了。 您可以基於每個頁面/每個模組審核您的現有應用程式,並且大多數情況下,您都可以非常輕鬆地重構現有應用程式。 使用 SSL 保護您的應用程式以防止有人探查您的憑據。 記得在開發之前、開發過程中以及開發之後考慮安全問題。

Adam Tuliper 是 Cegedim 的軟體架構師,具有 20 多年的軟體發展經驗。他是國家 INETA 社區發言人,定期在各種會議和 .NET 使用者組中發表演講。您可以通過 Twitter (twitter.com/AdamTuliper)、博客 (completedevelopment.blogspot.com) 或新的 secure-coding.com 網站瞭解他的觀點。有關如何防範駭客攻擊您的 ASP.NET 應用程式的更多詳細資訊,請參閱他即將發佈的 Pluralsight 視頻系列。

衷心感謝以下技術專家審閱了本文:Barry Dorrans