asp html表单没有csrf保护,CSRF在ASP.NET Core中的处理方法详解

前言

前几天,有个朋友问我关于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编码后的内容进行简单处理的结果,具体的实现可以参见Base64UrlTextEncoder.cs

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

其中重点的方法如下:

public void WriteTo(TextWriter writer, HtmlEncoder encoder)

{

writer.Write("

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();

}

}

}

}

当然这里的过滤器只是一个入口,相关的验证并不是在这里实现的。而是在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 (!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表单

@Html.AntiForgeryToken()

在控制器添加一个Action

[ValidateAntiForgeryToken]

[HttpPost]

public IActionResult AntiForm(string message)

{

return Content(message);

}

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

fb55941b0771b13e938c236fe7c13960.png

再来看看cookie的相关信息:

7223d4c65f2997ac1841fd7b1ec7a1ae.png

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

155fe0422e1e4e6d083b0e023155cdea.png

使用二:Ajax提交

表单:

@Html.AntiForgeryToken()

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

3b3906ad373406e404aaffb066b247c2.png

相信大家也都发现了问题的所在了!!隐藏域的相关内容并没有一起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表单:

7289d5f0841bf3bc3c961106715231d0.png

Cookie:

35463673b5f3c069ebd69457c797637e.png

本文涉及到的相关项目:

关于CSRF相关的内容

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对脚本之家的支持。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值