2013年9月27日 星期五

ASP.NET MVC 防範 CSRF 攻擊 - 在 AJAX 裡使用 AntiForgeryToken 的處理

在 ASP.NET MVC 裡要防範 CSRF 攻擊可以在檢視頁面上加入使用 AntiFrogeryToken,並且在後端所對應的 Action 方法加上 ValidateAntiForgeryToken Attribute,這樣就可以防止 CSRF 的攻擊,相關的資訊我之前有寫過依篇文章來說明「ASP.NET MVC - ValidateAntiForgeryToken 與 自定 HandleError 處理顯示客製的錯誤訊息頁」。

但如果網站的前端與後端互動大多都是經由 AJAX 方式的話,似乎就無法加上 AntiForgeryToken 來防護 CSRF 的攻擊,有些朋友是認為因為 Javascript 操作是不能跨網域的,所以就不用刻意對 AJAX 的操作加入 AntiForgeryToken 防範 CSRF,但我則是認為還是有必要去做這一層的防範,外在的攻擊手法是我們無法可以預測的,我們對於網站還是有必要去做到一些根本的防護,至少是多一層保障。

而這一篇則是跟大家說明如何在 AJAX 的操作裡使用 AntiForgeryToken。

 


在「Preventing Cross-Site Request Forgery (CSRF) Attacks : The Official Microsoft ASP.NET Site」這一篇文章裡有提到了「Anti-CSRF and AJAX」,作者「Mike Wasson」示範了如何在檢視頁面裡去建立一個 Razor @functions 「TokenHeaderValue」,然後在頁面裡使用 jQuery.Ajax 操作時在 headers 去加入 RequestVerificationToken 的內容,然後當 Request 傳送到後端 Controller Action 方法時再去驗證前端所傳送的 Token 是否正確,但是該文章最後所提供的方法並沒有完整的說明該用在何處,而且如果直接在 Controller Action 方法裡調用的話,還需要做調整,而且如果能夠跟原來使用 ValidateAntiForgeryToken Attribute 方式一樣的話會比較好,直接在 Action 方法上加個 Attribute 標籤而不是直接在方法裡再去加入程式。

於是在不久前看到了另一篇文章「AngularJS and AntiForgeryToken in ASP.NET MVC | TechBrij」,這篇文章雖然是說明 ASP.NET MVC 以及使用 AngularJS 的情況下使用 AntiForgeryToken 的方式,雖然前端使用了 AngularJS,不過重點在於後端有另外建立了「MyValidateAntiForgeryTokenAttribute」,這樣就可以直接在 Action 方法前加入這個標籤,然後在 MyValidateAntiForgeryTokenAttribute Filter 類別裡去對前端放在 Request Header 裡的 token 做驗證。

以下將上面兩篇文章的實作做個整理,如下:

 

Step.1

在專案根目錄下建立「App_Code」目錄,並且新增 CommonRazorFunctions.cshtml 檢視檔案,

image

在 CommonRazorFunctions.cshtml 裡加入以下的 Razor @functions,

image

一般 Razor @functions 如果是在個別檢視頁面裡是不需要加入 static 修飾詞,但如果作為可共用的方法時,就需要加上 static 修飾詞,如此才能再有需要使用的檢視頁面裡使用該 Razor @functions。

 

Step.2

加入 Action 方法與檢視頁面,

image

image

檢視頁面的內容:

@{
    Layout = null;
}
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Backend - Logon</title>
    <!-- Le styles -->
    <link href="~/Content/bootstrap.css" rel="stylesheet" type="text/css" />
    <link href="~/Content/bootstrap-responsive.css" rel="stylesheet" type="text/css" />
    <link href="~/Content/jquery.pnotify.default.css" rel="stylesheet" type="text/css" />
    <link href="~/Content/jquery.pnotify.default.icons.css" rel="stylesheet" type="text/css" />
    <link href="~/Content/Logon.css" rel="stylesheet" />
    <!-- HTML5 shim, for IE6-8 support of HTML5 elements -->
    <!--[if lt IE 9]>
      <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
    <![endif]-->
</head>
<body>
    <div class="container" style="padding-top: 50px;">
        <form class="form-signin" id="FormLogon" method="post" action="@Url.Action("AccountEdit", "Home")">
            <fieldset>
                <legend>帳號編輯</legend>
            </fieldset>
            <label for="account">帳號</label>
            @Html.TextBox("account", null, new { id = "TextBox_Account", @class = "input-block-level", placeholder = "Account", AutoComplete = "Off", tabindex = "1" })
 
            <label for="name">名稱</label>
            @Html.TextBox("name", null, new { id = "TextBox_Name", @class = "input-block-level", placeholder = "Name", AutoComplete = "Off", tabindex = "2" })
 
            <div style="padding-top: 10px;">
                <button type="button" class="btn btn-primary" id="Button_Submit" tabindex="3">送出</button>
                <button type="button" class="btn" id="Button_Reset" tabindex="4">取消</button>
            </div>
        </form>
    </div>
    <!-- /container -->
 
</body>
</html>

 

Step.3

接著就是 jQuery Ajax 裡去加入 Request Header,這裡就會使用到在 Step.1 所建立的 GetAntiForgeryToken()

image

 

Step.4

在網站專案根目錄下的 Filters 目錄裡新增「AjaxValidateAntiForgeryTokenAttribute.cs」

image

AjaxValidateAntiForgeryTokenAttribute 類別的程式內容:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Helpers;
using System.Web.Mvc;
 
namespace MvcApplication2.Filters
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
    public class AjaxValidateAntiForgeryTokenAttribute : FilterAttribute, IAuthorizationFilter
    {
        public void OnAuthorization(AuthorizationContext filterContext)
        {
            try
            {
                if (filterContext.HttpContext.Request.IsAjaxRequest())
                {
                    ValidateRequestHeader(filterContext.HttpContext.Request);
                }
                else
                {
                    filterContext.HttpContext.Response.StatusCode = 404;
                    filterContext.Result = new HttpNotFoundResult();
                }
            }
            catch (HttpAntiForgeryException e)
            {
                throw new HttpAntiForgeryException("Anti forgery token cookie not found");
            }
        }
 
        private void ValidateRequestHeader(HttpRequestBase request)
        {
            string cookieToken = String.Empty;
            string formToken = String.Empty;
            string tokenValue = request.Headers["RequestVerificationToken"];
            if (!String.IsNullOrEmpty(tokenValue))
            {
                string[] tokens = tokenValue.Split(':');
                if (tokens.Length == 2)
                {
                    cookieToken = tokens[0].Trim();
                    formToken = tokens[1].Trim();
                }
            }
            AntiForgery.Validate(cookieToken, formToken);
        }
 
    }
}

AjaxValidateAntiForgeryTokenAttribute 只限定用於接收 AJAX 請求的 Action 上,所以一般的 Action 就還是使用 ValidateAntiForgeryTokenAttribute。

:與「AngularJS and AntiForgeryToken in ASP.NET MVC | TechBrij」裡面所提供的「MyValidateAntiForgeryTokenAttribute.cs」作法不同,我這邊的作法是希望能夠有所區分,讓 AjaxValidateAntiForgeryTokenAttribute 只會出現在接收 AJAX Request 的 Action 方法上,而不是另外去建立一個新的 ValidateAntiForgeryTokenAttribute 類別然後把兩種操作都給涵蓋。

 

Step.5

使用 AjaxValidateAntiForgeryTokenAttribute

image

 

以上我並沒有提供全部頁面與 Javascript 的全部實作內容,但還是有提供了重點,例如:Razor @functions, 如何在 jQuery Ajax 裡使用以及 AjaxValidateAntiForgeryTokenAttribute.cs 的程式內容。

 

觀察

來看看經過以上的操作處理後的實據運作狀況,

一個簡單的編輯操作畫面,表單裡的帳號與名稱在編輯後送出並不是整個頁面做 POST 送出,而是使用 jQuery Ajax 的方式將資料傳送到 Controller 的 ActionEdit action 方法,

image

觀察頁面原始碼內容,在 jQuery Ajax 裡可以看到經由 Razor @funtions「GetAntiForgeryToken()」所產生的 Token,

image

表單欄位填好資料後送出資料,在 Firefox 瀏覽器的 Firebug 裡觀察送出的 Request Header 內容,

image

在 AjaxValidateAntiForgeryTokenAttribute 的 ValidateRequestHeader 方法裡從 Request Headers 取得 RequestVerficationToken 的內容,

image

取得 cookieToken 與 formToken 後再使用 AntiForgery.Validate() 驗證前端所送過來的 Token 是否正確。

image

 

以上就是在 jQuery AJAX 裡使用 AntiForgeryToken 的方式。

 

P.S.
如果將頁面上的 Javascript 程式抽出為 JS 檔案,則上面的作法就不適用了,在此特別說明。

 


參考連結:

MSDN – AntiForgery.GetTokens 方法

MSDN – AntiForgery.Validate 方法

Preventing Cross-Site Request Forgery (CSRF) Attacks : The Official Microsoft ASP.NET Site

AngularJS and AntiForgeryToken in ASP.NET MVC | TechBrij

 

以上

5 則留言:

  1. 作者已經移除這則留言。

    回覆刪除
  2. 你好~想請問一下,
    我看實際產生在畫面上的是cookieToken+FormToken,
    為什麼不將cookieToken存在session中呢?

    這樣即使FormToken被有心人取得, SERVER side還有cookie token可以擋著?

    小弟拙見 請伺教QQ

    回覆刪除
    回覆
    1. 去看前一篇並且去瞭解 CSRF 和 AntiForgeryToken 就可以瞭解了
      http://kevintsengtw.blogspot.tw/2013/01/aspnet-mvc-validateantiforgerytoken.html

      刪除
    2. 前一篇是在Html中加入AntiForgeryToken,同時會在Cookie中產生一組Token 發送要求時一併給SERVER端,
      當要求到達SERVER端,Action上的Filter標籤會檢核Token來決定此次要求是否合法。
      ↑小弟的理解有錯誤嗎@@?

      這篇是Ajax發送要求的方式,在Header中產生這組組合的Token,驗證方式亦同前一篇
      我在測試時發現,拿到這組組合Token後,以Postman發送要求也是能驗證過關,不就代表有了令牌就能為所欲為
      ↑這是我對這篇的理解 (不包含後續Handle錯誤頁的部分)

      但看完我還是太笨不瞭解..
      是因為跨站偽造要求時,我不應該假設這個網站會有這組Token嗎?


      刪除
    3. 原本的 ASP.NET MVC 是沒有提供前端 AJAX 所使用的 AntiForgeryToken 的支援做法,所以這邊所使用的方式就是希望可以採用如同 Form 所使用的 AntiForgeryToekn 的方式也讓前端 AJAX 有同樣的功能,Web 的 Request 是無狀態的,當然單靠一組 token 是無法防範與驗證是否為同一人,而 Cookie 裡面所存放的資訊就有包含一些基本資料以識別一開始所 render 的網頁與之後同個網頁所回來的是同一組,至於更為進階的 AntiforgeryToken 資料可以去翻原始碼,或是有任何意見就發 issues 給 asp.net mvc 開發團隊
      https://github.com/ASP-NET-MVC/aspnetwebstack/tree/master/src/System.Web.WebPages/Helpers/AntiXsrf
      有關驗證與安全性的做法還有其他的做法,而不是只有這一種,況且這一篇文章已經是快要四年了,這四年之間的前後端技術就已經翻了好幾輪,所以... 嗯... 就這樣...

      刪除

提醒

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