Understanding multiple anti-forgery tokens in ASP.NET MVC
Understanding multiple anti-forgery tokens in ASP.NET MVC
The MVC helper “Html.AntiForgeryToken()” can be used to protect your application against cross-site request forgery (CSRF). This will generate both a hidden field and a cookie that contains matching values that are validated on the server.
Our website utilizes multiple forms on the same view and each form contains an anti-forgery token. However, each call to “Html.AntiForgeryToken()” generates a different value. For example, multiple calls such as:
1
2
3
4
5
6
7
8
|
< html > < head ></ head > < body > @Html.AntiForgeryToken() @Html.AntiForgeryToken() @Html.AntiForgeryToken() </ body > </ html > |
… will generate multiple hidden fields that look like this:
1
2
3
|
< input name = "__RequestVerificationToken" type = "hidden" value = "iAdQj5D0qrMuTggD8WpnOZPlVOfHg_qmPIEjnULAYd1h56cV2cL51rcaY8_UgxQbav5_6KTAtyE52ir1X6GmaS9ZPgw1" /> < input name = "__RequestVerificationToken" type = "hidden" value = "Shvi8Bxe6-a8zfCfDGnxkaC-IETsbjkR9iIylwn-2VRWQ-GtQkdowdFw1biU7dN3j-xPJZHYQPe-hNfWspYjy_ZcCCY1" /> < input name = "__RequestVerificationToken" type = "hidden" value = "ZhaVFngUMLo88jmTIx___BTWlYFyKh1GalwEeffRl0-o3Gu7_m98k6aQjO7IysZIdXxVx6TqL6QIfX19Uwq3Ia6dghA1" /> |
I wanted to learn how it works under the cover, so I used ReSharper and dotPeek to decompile the code to understand it better.
At the heart of the helper “Html.AntiForgeryToken()” is the method “GetFormInputElement” used to generate the hidden field.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public TagBuilder GetFormInputElement(HttpContextBase httpContext) { this .CheckSSLConfig(httpContext); AntiForgeryToken cookieTokenNoThrow = this .GetCookieTokenNoThrow(httpContext); AntiForgeryToken newCookieToken; AntiForgeryToken formToken; this .GetTokens(httpContext, cookieTokenNoThrow, out newCookieToken, out formToken); if (newCookieToken != null ) this ._tokenStore.SaveCookieToken(httpContext, newCookieToken); if (! this ._config.SuppressXFrameOptionsHeader) httpContext.Response.AddHeader( "X-Frame-Options" , "SAMEORIGIN" ); TagBuilder tagBuilder = new TagBuilder( "input" ); tagBuilder.Attributes[ "type" ] = "hidden" ; tagBuilder.Attributes[ "name" ] = this ._config.FormFieldName; tagBuilder.Attributes[ "value" ] = this ._serializer.Serialize(formToken); return tagBuilder; } |
To understand why different values are generated in the hidden fields, we need to first take a look at the internal class “AntiForgeryToken” that represents the verification token.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
internal sealed class AntiForgeryToken { private BinaryBlob _securityToken; public BinaryBlob SecurityToken { get { if ( this ._securityToken == null ) this ._securityToken = new BinaryBlob(128); return this ._securityToken; } set { this ._securityToken = value; } } // Removed other properties, fields, and methods. // ... // ... } |
The token is represented by a “BinaryBlob”. Digging down into this class shows it uses “RNGCryptoServiceProvider” to randomly generate a 16 byte array.
1
2
3
4
5
6
|
private static byte [] GenerateNewToken( int bitLength) { byte [] data = new byte [bitLength / 8]; BinaryBlob._prng.GetBytes(data); return data; } |
Looking back at the “GetFormInputElement” method, we can see the code checks for the existence of a token from the cookies collection using the method “GetCookieTokenNoThrow”. Drilling into the code shows it’s not complicated. If it exists, it deserializes into an “AntiForgeryToken”, otherwise it returns null.
1
2
3
4
5
6
7
8
9
|
public AntiForgeryToken GetCookieToken(HttpContextBase httpContext) { HttpCookie cookie = httpContext.Request.Cookies[ this ._config.CookieName]; if (cookie == null || string .IsNullOrEmpty(cookie.Value)) return (AntiForgeryToken) null ; return this ._serializer.Deserialize(cookie.Value); } |
So what does it do with this deserialized token? We can see it’s passed to the method “GetTokens”. Drilling into this method eventually leads to the method “GenerateFormToken”.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
private void GetTokens(HttpContextBase httpContext, AntiForgeryToken oldCookieToken, out AntiForgeryToken newCookieToken, out AntiForgeryToken formToken) { newCookieToken = (AntiForgeryToken) null ; if (! this ._validator.IsCookieTokenValid(oldCookieToken)) oldCookieToken = newCookieToken = this ._validator.GenerateCookieToken(); formToken = this ._validator.GenerateFormToken(httpContext, AntiForgeryWorker.ExtractIdentity(httpContext), oldCookieToken); } // From the validator's class public AntiForgeryToken GenerateFormToken(HttpContextBase httpContext, IIdentity identity, AntiForgeryToken cookieToken) { AntiForgeryToken antiForgeryToken = new AntiForgeryToken() { SecurityToken = cookieToken.SecurityToken, IsSessionToken = false }; // Removed some code related to identities and additional data. // ... // ... return antiForgeryToken; } |
So from what we can see, if a token can be deserialized from the request’s cookie collection, it’ll reuse that token instead of generating a new one. If a token doesn’t exist in the cookie collection, it’ll instantiate a new instance of “AntiForgeryToken” and randomly generate a new 16 byte array to represent the token.
Going back to the method “GetFormInputElement”, we can see it calls the method “SaveCookieToken” after generating or reusing the existing token.
1
2
3
4
5
6
7
8
9
10
11
12
|
public void SaveCookieToken(HttpContextBase httpContext, AntiForgeryToken token) { HttpCookie cookie = new HttpCookie( this ._config.CookieName, this ._serializer.Serialize(token)) { HttpOnly = true }; if ( this ._config.RequireSSL) cookie.Secure = true ; httpContext.Response.Cookies.Set(cookie); } |
After generating the first token and saving it to the cookie collection, all subsequent calls to the helper method “Html.AntiForgeryToken()” will follow the same steps and reuse the existing token from the cookie collection instead of generating a new value. Since it is a session cookie, this means the anti-forgery token’s value is generated only once during a browser session and is reused for all subsequent calls.
So why are the hidden field values different from one another if they are reusing the same token? To answer that, we have to look at the token serializer.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public string Serialize(AntiForgeryToken token) { using (MemoryStream memoryStream = new MemoryStream()) { using (BinaryWriter binaryWriter = new BinaryWriter((Stream) memoryStream)) { binaryWriter.Write(( byte ) 1); binaryWriter.Write(token.SecurityToken.GetData()); binaryWriter.Write(token.IsSessionToken); // Removed some code related to identities and additional data. // ... // ... binaryWriter.Flush(); return this ._cryptoSystem.Protect(memoryStream.ToArray()); } } } |
The “Protect” method uses the internal class “AspNetCryptoServiceProvider” to encrypt the token using our specified machineKey in the config. So while the encrypted values may look different, the decrypted values are the same. To test this, we can use the decompiled code from dotPeek to “Unprotect” the encrypted values.
1
2
3
|
byte [] one = MachineKey45CryptoSystem.Instance.Unprotect( "iAdQj5D0qrMuTggD8WpnOZPlVOfHg_qmPIEjnULAYd1h56cV2cL51rcaY8_UgxQbav5_6KTAtyE52ir1X6GmaS9ZPgw1" ); byte [] two = MachineKey45CryptoSystem.Instance.Unprotect( "Shvi8Bxe6-a8zfCfDGnxkaC-IETsbjkR9iIylwn-2VRWQ-GtQkdowdFw1biU7dN3j-xPJZHYQPe-hNfWspYjy_ZcCCY1" ); byte [] three = MachineKey45CryptoSystem.Instance.Unprotect( "ZhaVFngUMLo88jmTIx___BTWlYFyKh1GalwEeffRl0-o3Gu7_m98k6aQjO7IysZIdXxVx6TqL6QIfX19Uwq3Ia6dghA1" ); |
Comparing all three byte arrays reveals they are identical.
In summary, the verification tokens generated from “Html.AntiForgeryToken()” are all identical within a browser session, regardless how many times we call it. The values appear different because they’re encrypted using our machineKey.
相关文章
- ASP.NET MVC 3:缓存功能的设计问题
- 无法激活服务,因为它不支持 ASP.NET 兼容性
- ASP.NET MVC学习系列(二)-WebAPI请求
- 1.Getting Started with ASP.NET MVC 5
- ASP.NET MVC下的四种验证编程方式
- Custom Roles Based Access Control (RBAC) in ASP.NET MVC Applications - Part 1 (Framework Introduction)
- Secure a Web API with Individual Accounts and Local Login in ASP.NET Web API 2.2
- EF+LINQ事物处理 C# 使用NLog记录日志入门操作 ASP.NET MVC多语言 仿微软网站效果(转) 详解C#特性和反射(一) c# API接受图片文件以Base64格式上传图片 .NET读取json数据并绑定到对象
- 第七节:语法总结(1)(自动属性、out参数、对象初始化器、var和dynamic等) 图片放大镜 JavaScript-基础 用javascript写原生ajax(笔记) 初遇 Asp.net MVC 数据库依赖缓存那些事儿 前端JS 与 后台C# 之间JSON序列化与反序列化(笔记)
- ASP.NET MVC:通过 FileResult 向 浏览器 发送文件
- C#中的函数式编程:递归与纯函数(二) 学习ASP.NET Core Razor 编程系列四——Asp.Net Core Razor列表模板页面
- ASP.NET MVC与ASP.NET WebForm
- asp.net mvc 增加WebApi
- ASP.NET MVC请求处理管道生命周期的19个关键环节(1-6)
- 对一个前端使用AngularJS后端使用ASP.NET Web API项目的理解(4)
- 在ASP.NET MVC中使用typeahead.js支持预先输入,即智能提示
- ASP.NET MVC生命周期
- 《精通 ASP.NET MVC 5》----1.4 本书的预备知识
- 《精通 ASP.NET MVC 4》----1.4 谁该使用ASP.NET MVC
- 基于ASP.NET MVC+MySQL开发的一套(Web)图书管理系统【100010294】
- Asp.net MVC中Html.Partial, RenderPartial, Action,RenderAction 区别和用法【转发】
- asp.net 域名欺骗式开发
- asp.net 后台获取flv视频地址进行播放【转】
- asp.net MVC提高开发速度(创建项目模板)
- 在 ASP.NET Core 中 使用 Serilog
- ASP.NET的版本?