• 初探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攻击方式

  • 相关阅读:
    mysql乐观锁总结和实践
    linux使用文本编辑器vi常用命令
    python高级特性-sorted()
    python高级特性-filter
    Python函数式编程-map/reduce
    centos 7.3 快速安装ceph
    python高级特性-迭代器
    python高级特性-生成器
    python高级特性-列表生成
    python高级特性-迭代
  • 原文地址:https://www.cnblogs.com/catcher1994/p/6720212.html
Copyright © 2020-2023  润新知