2013年1月30日 星期三

ASP.NET MVC - ValidateAntiForgeryToken 與 自定 HandleError 處理顯示客製的錯誤訊息頁

在 ASP.NET MVC 裡為了要防止 CSRF (Cross-Site Request Forgery) 跨站偽造請求的攻擊,我們可以在 View 的表單中加入「@Html.AntiForgeryToken」然後在對應的後端 Action 方法加上「ValidateAntiForgeryToken」Attribute 來防止 CSRF 的攻擊,相關 CSRF 的資訊可以參考以下的連結:

Wiki :  Cross-site request forgery
TWISC@NTUST網路應用安全知識庫 : 跨網站的偽造要求(CSRF)
宅學習 : CSRF攻擊及防禦方法
ibm : CSRF 攻击的应对之道

而當網站接收到沒有 AntiForgeryToken 的 POST 請求時就會返回錯誤訊息,如果沒有指定錯誤頁的話大多會是使用預設的 Error Page,比較好的錯誤訊息顯示方式是不要透漏太多訊息,以免讓有心者可以去鑽漏洞,但如果真的想要指定錯誤頁的話,也是有方法的,這篇文章當中就跟各位來說明。


什麼是 CSRF ( Cross-Site Request Forgery ) 跨站偽造請求呢?最常看到的就是經由網站裡的表單利用 POST 來發動攻擊,一般我們做網站都是預設站內的所有 POST 需求都是經由站內的表單或是 Javascript 程式所發出的,但有心攻擊的人就會利用這一點來對網站做壞事,例如我有一個簡單的登入頁面,

image

有心想做壞事的人是不會一直在這個頁面上去做踹站的動作,而是會自己寫個偽造的登入頁面然後跨站傳送 POST 請求到我們網站來踹,

image

這只是一個單純的 HTML 檔案,只是在表單的 Action 是使用測試網站的網址,

image

在這個偽造的網頁表單中輸入正確的帳號與密碼,就可以長驅直入網站當中,

image

image

所以不管我們在網站登入頁做的多麼嚴謹或是再多防護,都有可能因為這樣的疏忽而大開方便之門。

 

MSDN : HtmlHelper.AntiForgeryToken 方法

MSDN : ValidateAntiForgeryTokenAttribute 類別

為了避免這樣的情況發生,我們可以在 View 頁面的表單中加入「@Html.AntiForgeryToken」,然後在對應的 Action 方法上標註「ValidateAntiForgeryToken」Attribute,如此就可以避免跨站偽造請求的攻擊,

image

image

 

在 View 頁面的表單裡加上「@Html.AntiForgeryToken」,產出頁面的原始碼會是以 Hidden 欄位放置 Token,

image

並且也會產生 Cookie「__RequestVerificationToken」來放置 Request Verification Token,

image

如果是經由偽造的頁面所傳送過來的 POST Request,那麼系統就會顯示錯誤,

image

 


在上面未加入 AntiForgeryToken 的情況下會返回一個錯誤頁,而這個錯誤頁不應該出現在一般使用者的眼前,因為這樣的錯誤訊息內容透露了很多系統內部細節,所以我在之前的文章也強調過應該要使用專屬的錯誤訊息頁來顯示,而錯誤訊息頁的內容也不要透露太多的細節,一般的情況下,我們都會在 Web.Config 的 System.Web Section 加入 customError 的內容,如下:

image

那麼當遇到 CSRF 傳送時就會返回系統預設的錯誤頁內容,

image

image

 

如果你想要讓 CSRF 所返回的網頁不是用預設的 Error.cshtml 的話,可以在 ~/View/Shared 目錄下建立一個新的錯誤頁,例如我另外建立一個「CSRF_Error.cshtml」

image

在 Controller 的 Action 方法上就必須要再去標註 HandleError Attribute,並在這個 Attribute 內指定 ExceptionType 以及返回的錯誤頁名稱,

image

後端沒有接收到前端 View 所 POST 回來的 AntiForgeryToken 時是會發生 HttpAntiForgeryException,所以在 HandleError 的要指定當發生 HttpAntiForgeryException 時所返回錯誤訊息所使用的 View 名稱,再一次重新傳送 CSRF POST 請求後,就會顯示指定的錯誤頁內容,

image

 

MSDN : HttpAntiForgeryException 類別

 

如果有安裝 elmah 來記錄系統錯誤的話,可以在 elmah Dashboard 裡看到的 HttpAntiForgeryException 錯誤,

image

 


而我在之前的文章裡曾經有介紹過在 Global.asax 的 Application_Error 事件中對系統發生錯誤時進行處理,並且依照錯誤的不同而顯示不同的錯誤頁面,

ASP.NET MVC + ELMAH 監控並記錄你的網站錯誤資訊 1

雖然 HttpAntiForgeryException 是 Http Statu Code 500 的 Internal Error,但並不會返回我們所指定的 InternalError 錯誤頁,所以我這邊的作法是自己再另外建立一個 HandleError Attribute,然後在引發 Exception 時去判斷 Exception 的類別,然後依照類別去做不同的處理,例如遇到 HttpAntiForgeryExceptoipn 這個類別的 Exception 就顯示指定的錯誤訊息頁,

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Web;
using System.Web.Http.Filters;
using System.Web.Mvc;
 
namespace MvcApplication1.Base
{
    public class CustomHandleErrorAttribute : System.Web.Mvc.FilterAttribute, System.Web.Mvc.IExceptionFilter
    {
        public void OnException(ExceptionContext filterContext)
        {
            if (!filterContext.IsChildAction 
                && (!filterContext.ExceptionHandled && filterContext.HttpContext.IsCustomErrorEnabled))
            {
                var innerException = filterContext.Exception;
                if ((new HttpException(null, innerException).GetHttpCode() == 500))
                {
                    var controllerName = (string)filterContext.RouteData.Values["controller"];
                    var actionName = (string)filterContext.RouteData.Values["action"];
                    var viewName = "Error";
 
                    if (typeof(HttpAntiForgeryException).IsInstanceOfType(innerException))
                    {
                        controllerName = "ErrorPage";
                        actionName = "AntiForgeryError";
                        viewName = "~/Views/ErrorPage/AntiForgeryError.cshtml";
                    }
                    else
                    {
                        controllerName = "ErrorPage";
                        actionName = "InternalError";
                        viewName = "~/Views/ErrorPage/InternalError.cshtml";
                    }
                    
                    var model = new HandleErrorInfo(filterContext.Exception, controllerName, actionName);
                    var result = new ViewResult
                    {
                        ViewName = viewName,
                        ViewData = new ViewDataDictionary<HandleErrorInfo>(model),
                        TempData = filterContext.Controller.TempData
                    };
 
                    filterContext.Result = result;
                    filterContext.ExceptionHandled = true;
                    filterContext.HttpContext.Response.Clear();
                    filterContext.HttpContext.Response.StatusCode = 500;
                    filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
 
                    var version = Assembly.GetExecutingAssembly().GetName().Version;
                    filterContext.Controller.ViewData["Version"] = version.ToString();
                }
            }
        }
 
    }
}

其中的 viewName 需要指定絕對路徑,如果不把路徑名稱寫清楚,就只會在 ~/Views/Shared 目錄裡找,找不到就會引發 InvalidOperationException 的例外。

建立好 CustomHandleErrorAttribute 後,原本標註在 Login 方法上的 HandleError Attribute 就可以拿掉,而接著要到 ~/App_Start 目錄下的「FilterConfig.config」做修改,

image

把原本所使用的 HandleErrorAttribute 置換為我們所建立的 CustomHandleErrorAttribute, 修改如下,

using System.Web;
using System.Web.Mvc;
using MvcApplication1.Base;
 
namespace MvcApplication1
{
    public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new CustomHandleErrorAttribute());
            //filters.Add(new HandleErrorAttribute());
        }
    }
}

修改完成後,重新建置方案並重新傳送 CSRF 請求,就可以看到我們所指定的錯誤內容,

SNAGHTML160a8c89

 


雖說可以讓我們自己依據不同的錯誤或是例外來執行不同的處理或是顯示不同的錯誤訊息頁,但還是不要也不宜做得太細、太多,還是要避免過多的訊息暴露在外而讓人有機可趁。

 

參考連結:

The Code Project - Exception Handling in ASP.NET MVC  by After2050

StackOverflow  - HandleErrorAttribute not working

StackOverflow - ASP.net MVC - Custom HandleError Filter - Specify View based on Exception Type

 

延伸閱讀:

Steven Sanderson's blog : Prevent Cross-Site Request Forgery (CSRF) using ASP.NET MVC’s AntiForgeryToken() helper

隱匿角落 : 关于CSRF攻击及mvc中的解决方案 [ValidateAntiForgeryToken]

 

以上

10 則留言:

  1. 「在這個偽造的網頁表單中輸入正確的帳號與密碼」
    請教如果對方都知道帳密了,那不就直接做他可以做的事了嗎?為何還要透過偽造的網頁登入?

    回覆刪除
    回覆
    1. 這是指釣魚,一開始做偽造網站的人並不會知道所有人的帳密,但可以做一個偽造網站讓使用者在不清楚的狀況下輸入帳密,於是做假網站的人就可以獲得使用者的正確帳密。

      「在這個偽造的網頁表單中輸入正確的帳號與密碼」這裡指的情境是一般使用者不清楚所輸入的網頁是否為正確的,有時候使用者並不是很清楚登入的網站是否為真,有可能會是一個長得很像甚至一模一樣的網站。

      刪除
    2. 如果我是黑客 我做了个 比如 www.baldu.com,当有鱼上钓时,他必然会输入账密,这时候不已经达到目的了吗(得到账密),又何必多此一举的去做CSFR攻击

      刪除
    3. XSRF/CSRF Prevention in ASP.NET MVC and Web Pages
      http://www.asp.net/mvc/overview/security/xsrfcsrf-prevention-in-aspnet-mvc-and-web-pages

      刪除
    4. 竊取帳號密碼是目的,但是要讓使用者在輸入帳號密碼之後還能夠都入到原本的網站,讓使用者沒有發現異狀,如果原本的網站沒有做任何防護,讓網站底下的所有 Form 都可以接受其他站點所送出的資料,那麼網站還有什麼安全可言。
      .
      帳號密碼只是列出的一個案例,網站裡的 Form 表單裡加入 AntiForgeryToken 就是給一把鑰匙,然後 Controller 的 Action 方法加上檢查前端的 Token,驗證前端所送回的資料是否包含 Token,多做一層防護。
      .
      你說的有人做了一個跟 baidu 一樣的網站後騙到使用者的帳號密碼,這跟 baidu 有什麼關係呢?外面的偽站層出不窮,baidu 能一個個去抓出來嗎?這也關係到使用者是否有察覺到網頁的真假,偽站與使用者未能明辨真假,這並不是程式就可以放範。
      .
      也許舉帳號密碼的例子不是很好,使用 AntiForgeryToken 的主要用意就是防止跨網站偽造攻擊,如果有一個頁面的 Form 是關係到金錢、個人利益,如果沒有防止 CSRF 的攻擊,那麼只要有心人士就可以將資料直接 POST 到網站後端,沒有防護就讓使用者所竄改的資料直接送回後端資料庫,這樣的網站還能用嗎?
      AntiForgeryToken 就是要防護這樣的事情,防止沒有加上 AntiForgeryToken 的資料直接進入網站裡。

      刪除
    5. AntiForgeryToken 主要是避免網站遭受跨網站偽造攻擊 CSRF。
      避免外面網站的資料想要直接送進我的網站。AntiForgeryToken 是一到令牌,
      有這個令牌的資料才能被接受,沒有令牌的資料就一概擋住。

      刪除
  2. Hi Kevin,
    如果 黑客 获取了 __RequestVerificationToken 的对应 cookie 那么发动请求的时候添加到request header,能否攻破

    回覆刪除
    回覆
    1. 你做了相關的測試嗎? 動手做個測試,然後將過程與結果告訴我。

      刪除
  3. 我對留言的提問跟回答的解讀是這樣:
    使用者A不知道哪邊點了黑客B的網頁b,輸入帳密,『可以』把頁面導入到使用者欲連結目標(C)的網頁c,此時使用者覺得「操作很正常」。
    對使用者A來說,輸入了「正確的帳密」有連結到網頁c。
    對黑客B來說,獲取A的資訊同時A跟C毫無感覺。
    對目標C來說,A登入了。

    下一次,黑客B可以使用A的資料直接登入c,好比金錢,直接轉帳轉走,對吧?
    在這之後,使用者A發現錢噴了,理應會找的目標應該是C,C的系統無法預防「錢被盜走」,C就得認賠,對吧?

    所以本文的內容就是在說...
    C可以透過某種方式阻止某種黑客的行為如上述及內文。
    在這個過程當中,A可以察覺到有異,必須要更改密碼了。
    盲點在於『B確實拿到A的帳密了』。
    所以C較佳的做法應該是擋掉以後再強迫A改密碼?

    回覆刪除
    回覆
    1. Hello, 你好
      其實你的解讀是把整個行為給說明得更加複雜了。
      .
      簡單說,就是在我們自己的網頁裡裝上一個令牌,有這個令牌的網頁表單將資料 POST 回到後端處理時,後端處理 POST 的程式會先檢查有沒有這個令牌,沒有就是偽造,擋回去,有正確的令牌才能繼續處理。
      .
      至於令牌能不能被偷走以及是否有方法可以判斷令牌的真偽,其實是有提供支援的方法,而在這一篇文章裡並沒有再繼續說明。
      .

      刪除

提醒

千萬不要使用 Google Talk (Hangouts) 或 Facebook 及時通訊與我聯繫、提問,因為會掉訊息甚至我是過了好幾天之後才發現到你曾經傳給我訊息過,請多多使用「詢問與建議」(在左邊,就在左邊),另外比較深入的問題討論,或是有牽涉到你實作程式碼的內容,不適合在留言板裡留言討論,請務必使用「詢問與建議」功能(可以夾帶檔案),謝謝。