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

初探CSRF在ASP.NET Core中的处理方式,第1张

概述前言前几天,有个朋友问我关于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过滤器实现:ValidateAntiforgeryTokenAuthorizationFilter和AutoValidateAntiforgeryTokenAuthorizationFilter其中 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/ant

<h2 ID="前言">前言

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

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

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

那么什么是CSRF呢?

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

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

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

更加详细的内容可以参考维基百科:

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

视图层面控制器层面
@Html.AntiForgeryToken()

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

当在表单中添加了上面的代码后,页面会生成一个隐藏域,隐藏域的值是一个生成的token(防伪标识),类似下面的例子

其中的name="__RequestVerificationToken"是定义的一个const变量,value=XXXXX是根据一堆东西进行base64编码,并对base64编码后的内容进行简单处理的结果,具体的实现可以参见

生成上面隐藏域的代码在AntiforgeryExtensions这个文件里面,github上的源码文件:

其中重点的方法如下:

public void WriteTo(TextWriter writer,HtmlEncoder encoder){    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) &amp;&amp; ShouldValidate(context))    {        try        {            await _antiforgery.ValidateRequestAsync(context.HttpContext);        }        catch (AntiforgeryValidationException exception)        {            _logger.AntiforgeryTokenInvalid(exception.Message,exception);            context.Result = new BadRequestResult();        }    }}

}

完整实现可参见github源码:

当然这里的过滤器只是一个入口,相关的验证并不是在这里实现的。而是在Antiforgery这个项目上,其实说这个模块可能会更贴切一些。

由于是面向接口的编程,所以要知道具体的实现,就要找到对应的实现类才可以。

在Antiforgery这个项目中,有这样一个扩展方法AntiforgeryServiceCollectionExtensions,里面告诉了我们相对应的实现是DefaultAntiforgery这个类。其实Nancy的源码看多了,看一下类的命名就应该能知道个八九不离十。

  services.TryAddSingleton();

其中还涉及到了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 (!<a href="https://m.jb51.cc/tag/cookie/" target="_blank" >cookie</a>Token.Is<a href="https://m.jb51.cc/tag/cookie/" target="_blank" >cookie</a>Token || requestToken.Is<a href="https://m.jb51.cc/tag/cookie/" target="_blank" >cookie</a>Token){    message = Resources.AntiforgeryToken_TokensSwapped;    return false;}// Are the s<a href="https://www.jb51.cc/tag/ecurity/" target="_blank" >ecurity</a> tokens em<a href="https://www.jb51.cc/tag/bed/" target="_blank" >bed</a>ded in each incoming token <a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a>entical?if (!object.Equals(<a href="https://m.jb51.cc/tag/cookie/" target="_blank" >cookie</a>Token.S<a href="https://www.jb51.cc/tag/ecurity/" target="_blank" >ecurity</a>Token,requestToken.S<a href="https://www.jb51.cc/tag/ecurity/" target="_blank" >ecurity</a>Token)){    message = Resources.AntiforgeryToken_S<a href="https://www.jb51.cc/tag/ecurity/" target="_blank" >ecurity</a>TokenMismatch;    return false;}// Is the incoming token meant for the current user?var currentUser<a href="https://m.jb51.cc/tag/name/" target="_blank" >name</a> = string.Empty;BinaryBlob currentCl<a href="https://www.jb51.cc/tag/aim/" target="_blank" >aim</a>U<a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a> = n<a href="https://m.jb51.cc/tag/ul/" target="_blank" >ul</a>l;var authenticated<a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a>entity = GetAuthenticated<a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a>entity(<a href="https://m.jb51.cc/tag/http/" target="_blank" >http</a>Context.User);if (authenticated<a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a>entity != n<a href="https://m.jb51.cc/tag/ul/" target="_blank" >ul</a>l){    currentCl<a href="https://www.jb51.cc/tag/aim/" target="_blank" >aim</a>U<a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a> = GetCl<a href="https://www.jb51.cc/tag/aim/" target="_blank" >aim</a>U<a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a>Blob(_cl<a href="https://www.jb51.cc/tag/aim/" target="_blank" >aim</a>U<a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a>Extractor.ExtractCl<a href="https://www.jb51.cc/tag/aim/" target="_blank" >aim</a>U<a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a>(<a href="https://m.jb51.cc/tag/http/" target="_blank" >http</a>Context.User));    if (currentCl<a href="https://www.jb51.cc/tag/aim/" target="_blank" >aim</a>U<a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a> == n<a href="https://m.jb51.cc/tag/ul/" target="_blank" >ul</a>l)    {        currentUser<a href="https://m.jb51.cc/tag/name/" target="_blank" >name</a> = authenticated<a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a>entity.<a href="https://m.jb51.cc/tag/name/" target="_blank" >name</a> ?? string.Empty;    }}// Open<a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a> and other similar authentication schemes use URIs for the user<a href="https://m.jb51.cc/tag/name/" target="_blank" >name</a>.// These sho<a href="https://m.jb51.cc/tag/ul/" target="_blank" >ul</a>d be treated as case-sensitive.var comparer = StringComparer.Ordina<a href="https://m.jb51.cc/tag/li/" target="_blank" >li</a>g<a href="https://www.jb51.cc/tag/nor/" target="_blank" >nor</a>eCase;if (currentUser<a href="https://m.jb51.cc/tag/name/" target="_blank" >name</a>.StartsWith("<a href="https://m.jb51.cc/tag/http/" target="_blank" >http</a>://",StringComparison.Ordina<a href="https://m.jb51.cc/tag/li/" target="_blank" >li</a>g<a href="https://www.jb51.cc/tag/nor/" target="_blank" >nor</a>eCase) ||    currentUser<a href="https://m.jb51.cc/tag/name/" target="_blank" >name</a>.StartsWith("<a href="https://m.jb51.cc/tag/http/" target="_blank" >http</a>s://",StringComparison.Ordina<a href="https://m.jb51.cc/tag/li/" target="_blank" >li</a>g<a href="https://www.jb51.cc/tag/nor/" target="_blank" >nor</a>eCase)){    comparer = StringComparer.Ordinal;}if (!comparer.Equals(requestToken.User<a href="https://m.jb51.cc/tag/name/" target="_blank" >name</a>,currentUser<a href="https://m.jb51.cc/tag/name/" target="_blank" >name</a>)){    message = Resources.Form<a href="https://www.jb51.cc/tag/atan/" target="_blank" >atan</a>tiforgeryToken_User<a href="https://m.jb51.cc/tag/name/" target="_blank" >name</a>Mismatch(requestToken.User<a href="https://m.jb51.cc/tag/name/" target="_blank" >name</a>,currentUser<a href="https://m.jb51.cc/tag/name/" target="_blank" >name</a>);    return false;}if (!object.Equals(requestToken.Cl<a href="https://www.jb51.cc/tag/aim/" target="_blank" >aim</a>U<a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a>,currentCl<a href="https://www.jb51.cc/tag/aim/" target="_blank" >aim</a>U<a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a>)){    message = Resources.AntiforgeryToken_Cl<a href="https://www.jb51.cc/tag/aim/" target="_blank" >aim</a>U<a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a>Mismatch;    return false;}// Is the AdditionalData val<a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a>?if (_additionalDataProv<a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a>er != n<a href="https://m.jb51.cc/tag/ul/" target="_blank" >ul</a>l &amp;&amp;    !_additionalDataProv<a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a>er.Val<a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a>ateAdditionalData(<a href="https://m.jb51.cc/tag/http/" target="_blank" >http</a>Context,requestToken.AdditionalData)){    message = Resources.AntiforgeryToken_AdditionalDataCheck<a href="https://www.jb51.cc/tag/Failed/" target="_blank" >Failed</a>;    return false;}message = n<a href="https://m.jb51.cc/tag/ul/" target="_blank" >ul</a>l;return true;

}

注:验证前还有一个反序列化的过程,这个反序列化就是从cookie中拿到要判断的cookietoken和requesttoken

前面粗略介绍了一下其内部的实现,下面再用个简单的例子来看看具体的使用情况:

先在视图添加一个Form表单

@H_301_80@

在控制器添加一个Action

[ValIDateAntiForgeryToken][httpPost]public IActionResult AntiForm(string message){    return Content(message);}

来看看生成的HTML是不是如我们前面所说,将@HTML.AntiForgeryToken()输出为一个name为__RequestVerificationToken的隐藏域:

再来看看cookie的相关信息:

可以看到,一切都还是按照前面所说的执行。在输入框输入信息并点击按钮也能正常显示我们输入的文字。

表单:

@H_301_80@

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错误):

相信大家也都发现了问题的所在了!!隐藏域的相关内容并没有一起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",headers:        {            "RequestVerificationToken": token        },success: function (result) {            alert(result);        },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",headers:    {        "CUSTOMER-CSRF-header": token //注意header要修改    },scnd) {        alert(err.statusText);    }});

下面是效果:

Form表单:

cookie:

href="http://www.cnblogs.com/hyddd/archive/2009/04/09/1432744.HTML">浅谈CSRF攻击方式

总结

以上是内存溢出为你收集整理的初探CSRF在ASP.NET Core中的处理方式全部内容,希望文章能够帮你解决初探CSRF在ASP.NET Core中的处理方式所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/1256004.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-06-07
下一篇 2022-06-07

发表评论

登录后才能评论

评论列表(0条)

保存