初探CSRF在ASP.NET Core中的處理方式

前言

前幾天,有個朋友問我關於AntiForgeryToken問題,因爲對這一塊的理解也並不深刻,因此就去研究了一番,梳理了一下。html

在梳理以前,還須要簡單瞭解一下背景知識。git

AntiForgeryToken 能夠說是處理/預防CSRF的一種處理方案。github

那麼什麼是CSRF呢?ajax

CSRF(Cross-site request forgery)是跨站請求僞造,也被稱爲One Click Attack或者Session Riding,一般縮寫爲CSRF或者XSRF,是一種對網站的惡意利用。編程

簡單理解的話就是:有人盜用了你的身份,而且用你的名義發送惡意請求cookie

最近幾年,CSRF處於不溫不火的地位,可是仍是要對這個當心防範!app

更加詳細的內容能夠參考維基百科:Cross-site request forgeryasync

下面從使用的角度來分析一下CSRF在 ASP.NET Core中的處理,我的認爲主要有下面兩大塊ide

  • 視圖層面
  • 控制器層面

視圖層面

用法

@Html.AntiForgeryToken()

在視圖層面的用法相對比較簡單,用的仍是HtmlHelper的那一套東西。在Form表單中加上這一句就能夠了。post

原理淺析

當在表單中添加了上面的代碼後,頁面會生成一個隱藏域,隱藏域的值是一個生成的token(防僞標識),相似下面的例子

<input name="__RequestVerificationToken" type="hidden" value="CfDJ8FBn4LzSYglJpE6Q0fWvZ8WDMTgwK49lDU1XGuP5-5j4JlSCML_IDOO3XDL5EOyI_mS2Ux7lLSfI7ASQnIIxo2ScEJvnABf9v51TUZl_iM2S63zuiPK4lcXRPa_KUUDbK-LS4HD16pJusFRppj-dEGc" />

其中的name="__RequestVerificationToken"是定義的一個const變量,value=XXXXX是根據一堆東西進行base64編碼,並對base64編碼後的內容進行簡單處理的結果,具體的實現能夠參見Base64UrlTextEncoder.cs

生成上面隱藏域的代碼在AntiforgeryExtensions這個文件裏面,github上的源碼文件:AntiforgeryExtensions.cs

其中重點的方法以下:

public void WriteTo(TextWriter writer, HtmlEncoder encoder)
{
    writer.Write("<input name=\"");
    encoder.Encode(writer, _fieldName);
    writer.Write("\" type=\"hidden\" value=\"");
    encoder.Encode(writer, _requestToken);
    writer.Write("\" />");
}

至關的清晰明瞭!

控制器層面

用法

[ValidateAntiForgeryToken]
[AutoValidateAntiforgeryToken]
[IgnoreAntiforgeryToken]

這三個都是能夠基於類或方法的,因此咱們只要在某個控制器或者是在某個Action上面加上這些Attribute就能夠了。

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]

原理淺析

本質是Filter(過濾器),驗證上面隱藏域的value

過濾器實現:ValidateAntiforgeryTokenAuthorizationFilterAutoValidateAntiforgeryTokenAuthorizationFilter

其中 AutoValidateAntiforgeryTokenAuthorizationFilter是繼承了ValidateAntiforgeryTokenAuthorizationFilter,只重寫了其中的ShouldValidate方法。

下面貼出ValidateAntiforgeryTokenAuthorizationFilter的核心方法:

public class ValidateAntiforgeryTokenAuthorizationFilter : IAsyncAuthorizationFilter, IAntiforgeryPolicy
{
    public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (IsClosestAntiforgeryPolicy(context.Filters) && ShouldValidate(context))
        {
            try
            {
                await _antiforgery.ValidateRequestAsync(context.HttpContext);
            }
            catch (AntiforgeryValidationException exception)
            {
                _logger.AntiforgeryTokenInvalid(exception.Message, exception);
                context.Result = new BadRequestResult();
            }
        }
    }
}

完整實現可參見github源碼:ValidateAntiforgeryTokenAuthorizationFilter.cs

固然這裏的過濾器只是一個入口,相關的驗證並非在這裏實現的。而是在Antiforgery這個項目上,其實說這個模塊可能會更貼切一些。

因爲是面向接口的編程,因此要知道具體的實現,就要找到對應的實現類才能夠。

Antiforgery這個項目中,有這樣一個擴展方法AntiforgeryServiceCollectionExtensions,裏面告訴了咱們相對應的實現是DefaultAntiforgery這個類。其實Nancy的源碼看多了,看一下類的命名就應該能知道個八九不離十。

services.TryAddSingleton<IAntiforgery, DefaultAntiforgery>();

其中還涉及到了IServiceCollection,但這不是本文的重點,因此不會展開講這個,只是提出它在 .net core中是一個重要的點。

好了,迴歸正題!要驗證是不是合法的請求,天然要先拿到要驗證的內容。

var tokens = await _tokenStore.GetRequestTokensAsync(httpContext);

它是從Cookie中拿到一個指定的前綴爲.AspNetCore.Antiforgery.的Cookie,並根據這個Cookie進行後面相應的判斷。下面是驗證的具體實現:

public bool TryValidateTokenSet(
    HttpContext httpContext,
    AntiforgeryToken cookieToken,
    AntiforgeryToken requestToken,
    out string message)
{
    //去掉了部分非空的判斷

    // Do the tokens have the correct format?
    if (!cookieToken.IsCookieToken || requestToken.IsCookieToken)
    {
        message = Resources.AntiforgeryToken_TokensSwapped;
        return false;
    }

    // Are the security tokens embedded in each incoming token identical?
    if (!object.Equals(cookieToken.SecurityToken, requestToken.SecurityToken))
    {
        message = Resources.AntiforgeryToken_SecurityTokenMismatch;
        return false;
    }

    // Is the incoming token meant for the current user?
    var currentUsername = string.Empty;
    BinaryBlob currentClaimUid = null;

    var authenticatedIdentity = GetAuthenticatedIdentity(httpContext.User);
    if (authenticatedIdentity != null)
    {
        currentClaimUid = GetClaimUidBlob(_claimUidExtractor.ExtractClaimUid(httpContext.User));
        if (currentClaimUid == null)
        {
            currentUsername = authenticatedIdentity.Name ?? string.Empty;
        }
    }

    // OpenID and other similar authentication schemes use URIs for the username.
    // These should be treated as case-sensitive.
    var comparer = StringComparer.OrdinalIgnoreCase;
    if (currentUsername.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
        currentUsername.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
    {
        comparer = StringComparer.Ordinal;
    }

    if (!comparer.Equals(requestToken.Username, currentUsername))
    {
        message = Resources.FormatAntiforgeryToken_UsernameMismatch(requestToken.Username, currentUsername);
        return false;
    }

    if (!object.Equals(requestToken.ClaimUid, currentClaimUid))
    {
        message = Resources.AntiforgeryToken_ClaimUidMismatch;
        return false;
    }

    // Is the AdditionalData valid?
    if (_additionalDataProvider != null &&
        !_additionalDataProvider.ValidateAdditionalData(httpContext, requestToken.AdditionalData))
    {
        message = Resources.AntiforgeryToken_AdditionalDataCheckFailed;
        return false;
    }

    message = null;
    return true;
}

注:驗證前還有一個反序列化的過程,這個反序列化就是從Cookie中拿到要判斷的cookietoken和requesttoken

如何使用

前面粗略介紹了一下其內部的實現,下面再用個簡單的例子來看看具體的使用狀況:

使用一:常規的Form表單

先在視圖添加一個Form表單

<form id="form1" action="/home/antiform" method="post">
    @Html.AntiForgeryToken()
    <p><input type="text" name="message" /></p>
    <p><input type="submit" value="Send by Form" /></p>
</form>

在控制器添加一個Action

[ValidateAntiForgeryToken]
[HttpPost]
public IActionResult AntiForm(string message)
{
    return Content(message);
}

來看看生成的html是否是如咱們前面所說,將@Html.AntiForgeryToken()輸出爲一個name爲__RequestVerificationToken的隱藏域:

image

再來看看cookie的相關信息:

image

能夠看到,一切都仍是按照前面所說的執行。在輸入框輸入信息並點擊按鈕也能正常顯示咱們輸入的文字。

image

使用二:Ajax提交

表單:

<form id="form2" action="/home/antiajax" method="post">
    @Html.AntiForgeryToken()
    <p><input type="text" name="message" id="ajaxMsg" /></p>
    <p><input type="button" id="btnAjax" value="Send by Ajax" /></p>
</form>

js:

$(function () {
    $("#btnAjax").on("click", function () {
        $("#form2").submit();                
    });
})

這樣子的寫法也是和上面的結果是同樣的!

怕的是出現下面這樣的寫法:

$.ajax({
    type: "post",
    dataType: "html",
    url: '@Url.Action("AntiAjax", "Home")',
    data: { message: $('#ajaxMsg').val() },
    success: function (result) {
        alert(result);
    },
    error: function (err, scnd) {
        alert(err.statusText);
    }
});

這樣,正常狀況下確實是看不出任何毛病,可是實際確是下面的結果(400錯誤):

image

相信你們也都發現了問題的所在了!!隱藏域的相關內容並無一塊兒post過去!!

處理方法有兩種:

方法一:

在data中加上隱藏域相關的內容,大體以下:

$.ajax({
    //        
    data: { message: $('#ajaxMsg').val(), __RequestVerificationToken: $("input[name='__RequestVerificationToken']").val()}
});

方法二:

在請求中添加一個header

$("#btnAjax").on("click", function () {
    var token = $("input[name='__RequestVerificationToken']").val();
    $.ajax({
        type: "post",
        dataType: "html",
        url: '@Url.Action("AntiAjax", "Home")',
        data: { message: $('#ajaxMsg').val() },
        headers:
        {
            "RequestVerificationToken": token
        },
        success: function (result) {
            alert(result);
        },
        error: function (err, scnd) {
            alert(err.statusText);
        }
    });
});

這樣就能處理上面出現的問題了!

使用三:自定義相關信息

可能會有很多人以爲,像那個生成的隱藏域那個name能不能換成本身的,那個cookie的名字能不能換成本身的〜〜

答案是確定能夠的,下面簡單示範一下:

在Startup的ConfigureServices方法中,添加下面的內容便可對默認的名稱進行相應的修改。

services.AddAntiforgery(option =>
{
    option.CookieName = "CUSTOMER-CSRF-COOKIE";
    option.FormFieldName = "CustomerFieldName";
    option.HeaderName = "CUSTOMER-CSRF-HEADER";
});

相應的,ajax請求也要作修改:

var token = $("input[name='CustomerFieldName']").val();//隱藏域的名稱要改
$.ajax({
    type: "post",
    dataType: "html",
    url: '@Url.Action("AntiAjax", "Home")',
    data: { message: $('#ajaxMsg').val() },
    headers:
    {
        "CUSTOMER-CSRF-HEADER": token //注意header要修改
    },
    success: function (result) {
        alert(result);
    },
    error: function (err, scnd) {
        alert(err.statusText);
    }
});

下面是效果:

Form表單:

image

Cookie:

image

本文涉及到的相關項目:

關於CSRF相關的內容

Preventing Cross-Site Request Forgery (XSRF/CSRF) Attacks in ASP.NET Core

淺談CSRF攻擊方式

相關文章
相關標籤/搜索