zoukankan      html  css  js  c++  java
  • 初探CSRF在ASP.NET Core中的处理方式

    前言

    前几天,有个朋友问我关于AntiForgeryToken问题,由于对这一块的理解也并不深入,所以就去研究了一番,梳理了一下。

    在梳理之前,还需要简单了解一下背景知识。

    AntiForgeryToken 可以说是处理/预防CSRF的一种处理方案。

    那么什么是CSRF呢?

    CSRF(Cross-site request forgery)是跨站请求伪造,也被称为One Click Attack或者Session Riding,通常缩写为CSRF或者XSRF,是一种对网站的恶意利用。

    简单理解的话就是:有人盗用了你的身份,并且用你的名义发送恶意请求

    最近几年,CSRF处于不温不火的地位,但是还是要对这个小心防范!

    更加详细的内容可以参考维基百科:Cross-site request forgery

    下面从使用的角度来分析一下CSRF在 ASP.NET Core中的处理,个人认为主要有下面两大块

    • 视图层面
    • 控制器层面

    视图层面

    用法

    @Html.AntiForgeryToken()
    

    在视图层面的用法相对比较简单,用的还是HtmlHelper的那一套东西。在Form表单中加上这一句就可以了。

    原理浅析

    当在表单中添加了上面的代码后,页面会生成一个隐藏域,隐藏域的值是一个生成的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攻击方式

  • 相关阅读:
    第二十一章流 1流的操作 简单
    第二十章友元类与嵌套类 1友元类 简单
    第十九章 19 利用私有继承来实现代码重用 简单
    第二十章友元类与嵌套类 2嵌套类 简单
    第十九章 8链表类Node 简单
    第二十一章流 3用cin输入 简单
    第十九章 10 图书 药品管理系统 简单
    第十九章 11图书 药品管理系统 简单
    第二十一章流 4文件的输入和输出 简单
    第十九章 12 什么时候使用私有继承,什么时候使用包含 简单
  • 原文地址:https://www.cnblogs.com/catcher1994/p/6720212.html
Copyright © 2011-2022 走看看