[食譜好菜] 常在面試出現的題目:CSRF

「你怎麼避免 CSRF?」這個題目也是面試時的常客,通常會跟著 XSS 一起出現,也是一個很基本的網站安全問題,網路上的相關文章俯拾即是,我將其整理下來給自己參考。

我參考的文章有一大堆,可見得 CSRF 的問題受到多大的重視,大家可以直接去閱讀這些文章。

CSRF (Cross-site request forgery)

Wiki 的定義是這樣的:CSRF 被稱為 one-click attack 或者 session riding,通常縮寫為 CSRF 或者 XSRF, 是一種挾制用戶在當前已登錄的Web應用程式上執行非本意的操作的攻擊方法。跟跨網站指令碼(XSS)相比,XSS 利用的是用戶對指定網站的信任,CSRF 利用的是網站對用戶網頁瀏覽器的信任。

從以上描述各位就可以知道為什麼 CSRF 跟 XSS 通常在面試的時候會一起出現了吧,接著我用一個簡單的範例來 demo 什麼是 CSRF 攻擊。

身為疼惜老婆的軟體主廚,每天要匯 1000 塊給老婆,因此軟體主廚登入了一個匯款的網頁,準備匯 1000 塊給老婆。

有一天軟體主廚依舊登入匯款的網頁,準備要匯 1000 塊給老婆,不過他覺得網頁怎麼怪怪的,但是也不疑有他,就照著步驟照樣匯 1000 塊給老婆,當他匯出去之後才驚覺原來他中了圈套,剛剛那個匯款網頁是假的,不管匯給誰都會匯給壞人!

假網頁有一個名為 target 的 hidden field,value 永遠是「壞人」,所以不管使用者要匯款給誰,永遠都會匯款給「壞人」,這個就是一個 CSRF 攻擊。

<form id="form1" action="http://localhost:9361/Home/Transfer" method="post">
    <p>從 <input id="source" name="source" type="text" /></p>
    <p>匯 <input id="money" name="money" type="text" /></p>
    <p>給 <input id="faketarget" name="faketarget" type="text" /><input id="target" name="target" type="hidden" value="壞人" /></p>
    <p><input id="Submit1" type="submit" value="submit" /></p>
</form>
一想到就頭皮發麻,任何有心人士都可以做一個網頁,誘拐你來操作,偽造成合法的使用者對目標系統做操作。

如何防範 CSRF 攻擊?

一種是檢查 Referer 欄位,在 HTTP Headers 裡傳送 Referer 資訊,以便讓網站辨別這是來自於合法 domain 的 Request。

但是 Referer 有被篡改的風險,所以使用第二種方式 - 檢查票證(Token) 是比較推薦的方式,從網站直接 Hash 一個值當成 Token 發給使用者端,而使用者端要發 Request 時必須帶入這個 Token 以提供給網站檢查,有點認票不認人的味道。

1. ASP.NET MVC 防範 CSRF 攻擊的方法

我們先在網頁的 form 之中加入 @Html.AntiForgeryToken() 這行程式碼。

<form id="form1" action="http://localhost:9361/Home/Transfer" method="post">
    @Html.AntiForgeryToken()
    <p>從 <input id="source" name="source" type="text" /></p>
    <p>匯 <input id="money" name="money" type="text" /></p>
    <p>給 <input id="faketarget" name="faketarget" type="text" /><input id="target" name="target" type="hidden" value="壞人" /></p>
    <p><input id="Submit1" type="submit" value="submit" /></p>
</form>

加入這行程式碼後,我們會取得一個 Cookie __RequestVerificationToken,之後要發 Request 時都要帶上這個 Cookie。

然後在 Action 加上 [ValidateAntiForgeryToken] 這個 Filter,只要沒有帶上 VerificationToken 的 Request 都視為是偽造的。

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Transfer(string source, int money, string target)
{
    …
}

2. 自行打造防範 CSRF 攻擊的方法

剛剛是利用 AntiForgeryToken Helper 以及 ValidateAntiForgeryToken Filter 幫我們建立起防範 CSRF 攻擊的機制,如果我要自己做的話怎麼辦?

首先我們必須自己撰寫 Hash Token 的邏輯

private static string GetAntiForgeryToken()
{
    string cookieToken;
    string formToken;

    // 這邊我用 AntiForgery 這個 Helper 來幫我產生 Token。
    // 我們也可以自己撰寫自己想要的邏輯,用 MD5、SHAxxx…等方式產生一個 Hash 過的 Token。
    AntiForgery.GetTokens(null, out cookieToken, out formToken);

    return string.Concat(cookieToken, ":", formToken);
}

接著就把這個 Token 交給前端並且保存好,我這邊為了方便起見,把 Token 存到一個 hidden field。

<form id="form1" action="/Home/Transfer" method="post">
    <input type="hidden" id="verificationToken" name="verificationToken" value="@ViewBag.VerificationToken" />
    <p>從 <input id="source" name="source" type="text" /></p>
    <p>匯 <input id="money" name="money" type="text" /></p>
    <p>給 <input id="target" name="target" type="text" /></p>
    <p><input id="Submit1" type="button" value="submit" /></p>
</form>

再來,客製一個 Action Filter 來檢查 Token。

public class CustomValidateAntiForgeryToken : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        try
        {
            ValidateAntiForgeryToken(filterContext.HttpContext.Request.Headers["VerificationToken"]);
        }
        catch (HttpAntiForgeryException ex)
        {
            throw new HttpAntiForgeryException("注意!您現在正在操作一個偽造的網頁!");
        }
    }

    private static void ValidateAntiForgeryToken(string verificationToken)
    {
        string cookieToken = "";
        string formToken = "";

        if (!string.IsNullOrEmpty(verificationToken))
        {
            string[] tokens = verificationToken.Split(':');
            if (tokens.Length == 2)
            {
                cookieToken = tokens[0].Trim();
                formToken = tokens[1].Trim();
            }
        }

        // 因為我用 AntiForgery Helper 幫我產生 Token,這邊一樣用 AntiForgery 來做 Validation。
        // 如果 Token 是自己 Hash 的,就自己另外寫檢查的邏輯。
        AntiForgery.Validate(cookieToken, formToken);
    }
}

然後把我們客製的 CustomValidateAntiForgeryToken 掛到 Action 上

[HttpPost]
[CustomValidateAntiForgeryToken]
public ActionResult Transfer(string source, int money, string target)
{
    …
}

最後我發一個 AJAX Request 執行我原先的商業行為,並且把 VerificationToken 放到 Headers 提供給網站驗證。

$.ajax({
    url: '/Home/Transfer',
    method: 'POST',
    data: {
        source: $("#source").val(),
        money: $("#money").val(),
        target: $("#target").val()
    },
    headers: {
        "VerificationToken": $("#verificationToken").val()
    }
})
.done(function (data) {
    alert(JSON.stringify(data));
});

如果是沒有帶有 VerificationToken 的 Request 就會收到我自定義的錯誤訊息

 < Source Code >

相關資源

C# 指南
ASP.NET 教學
ASP.NET MVC 指引
Azure SQL Database 教學
SQL Server 教學
Xamarin.Forms 教學