转载 http://kevintsengtw.blogspot.co.nz/2013/01/aspnet-mvc-validateantiforgerytoken.html
在 ASP.NET MVC 裡為了要防止 CSRF (Cross-Site Request Forgery) 跨站偽造請求的攻擊,我們可以在 View 的表單中加入「@Html.AntiForgeryToken」然後在對應的後端 Action 方法加上「ValidateAntiForgeryToken」Attribute 來防止 CSRF 的攻擊,相關
而當網站接收到沒有 AntiForgeryToken 的 POST 請求時就會返回錯誤訊息,如果沒有指定錯誤頁的話大多會是使用預設的 Error Page,比較好的錯誤訊息顯示方式是不要透漏太多訊息,以免讓有心者可以去鑽漏洞,但如果真的想要指定錯誤頁的話,也是有方法的,這篇文章當中就跟各位來說明。
什麼是 CSRF ( Cross-Site Request Forgery ) 跨站偽造請求呢?最常看到的就是經由網站裡的表單利用 POST 來發動攻擊,一般我們做網站都是預設站內的所有 POST 需求都是經由站內的表單或是 Javascript 程式所發出的,但有心攻擊的人就會利用這一點來對網站做壞事,例如我有一個簡單的登入頁面,
有心想做壞事的人是不會一直在這個頁面上去做踹站的動作,而是會自己寫個偽造的登入頁面然後跨站傳送 POST 請求到我們網站來踹,
這只是一個單純的 HTML 檔案,只是在表單的 Action 是使用測試網站的網址,
在這個偽造的網頁表單中輸入正確的帳號與密碼,就可以長驅直入網站當中,
所以不管我們在網站登入頁做的多麼嚴謹或是再多防護,都有可能因為這樣的疏忽而大開方便之門。
為了避免這樣的情況發生,我們可以在 View 頁面的表單中加入「@Html.AntiForgeryToken」,然後在對應的 Action 方法上標註「ValidateAntiForgeryToken」Attribute,如此就可以避免跨站偽造請求的攻擊,
在 View 頁面的表單裡加上「@Html.AntiForgeryToken」,產出頁面的原始碼會是以 Hidden 欄位放置 Token,
並且也會產生 Cookie「__RequestVerificationToken」來放置 Request Verification Token,
如果是經由偽造的頁面所傳送過來的 POST Request,那麼系統就會顯示錯誤,
,
在上面未加入 AntiForgeryToken 的情況下會返回一個錯誤頁,而這個錯誤頁不應該出現在一般使用者的眼前,因為這樣的錯誤訊息內容透露了很多系統內部細節,所以我在之前的文章也強調過應該要使用專屬的錯誤訊息頁來顯示,而錯誤訊息頁的內容也不要透露太多的細節,一般的情況下,我們都會在 Web.Config 的 System.Web Section 加入 customError 的內容,如下:
那麼當遇到 CSRF 傳送時就會返回系統預設的錯誤頁內容,
如果你想要讓 CSRF 所返回的網頁不是用預設的 Error.cshtml 的話,可以在 ~/View/Shared 目錄下建立一個新的錯誤頁,例如我另外建立一個「CSRF_Error.cshtml」
在 Controller 的 Action 方法上就必須要再去標註 HandleError Attribute,並在這個 Attribute 內指定 ExceptionType 以及返回的錯誤頁名稱,
後端沒有接收到前端 View 所 POST 回來的 AntiForgeryToken 時是會發生 HttpAntiForgeryException (MSDN : HttpAntiForgeryException 類別),所以在 HandleError 的要指定當發生 HttpAntiForgeryException 時所返回錯誤訊息所使用的 View 名稱,再一次重新傳送 CSRF POST 請求後,就會顯示指定的錯誤頁內容,
如果有安裝 elmah 來記錄系統錯誤的話,可以在 elmah Dashboard 裡看到的 HttpAntiForgeryException 錯誤,
而我在之前的文章裡曾經有介紹過在 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」做修改,
把原本所使用的 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 請求,就可以看到我們所指定的錯誤內容,
雖說可以讓我們自己依據不同的錯誤或是例外來執行不同的處理或是顯示不同的錯誤訊息頁,但還是不要也不宜做得太細、太多,還是要避免過多的訊息暴露在外而讓人有機可趁。
參考連結:
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