ASP.NET Core CSRF defence with Antiforgery
Cross Site Request Forgery (aka CSRF or XSRF) is one of the most common attacks in which the user is tricked into executing an unwanted action through his browser on his behalf, in one of the sites he is currently authenticated.
ASP.Net Core contains an Antiforgery package that can be used to secure your application against this particular risk. For those who have used earlier versions of ASP.Net will see that things have changed a bit in the new framework.
This article is published from the DNC Magazine for Developers and Architects. Download this magazine from here [PDF] or Subscribe to this magazine for FREE and download all previous and current editions.
Brief CSRF overview
CSRF attacks rely on the fact that most authentication systems will store credentials in the browser (such as the authentication cookie) which are automatically sent with any requests for that specific website domain/sub domain.
An attacker can then trick the user through social media, emails and other ways into a malicious or infected site, where they can send a request on their behalf to the attacked or targeted site. If the user is authenticated on the targeted site, this request will include his credentials, and the site won’t be able to distinguish it from a legit request.
For example, hackers might send a link to the user which opens a malicious site. In this site, they will trick the user into clicking a button that posts a hidden form against the target site where the user is authenticated. The attacker will have forged this form to be posted against a URL like a fund transfers URL, and contain data like transferring some money to the attacker’s account!
Figure 1 visually depicts CSRF in action.
Figure 1: CSRF overview
It is important to understand that for these requests to be of any benefit to the attacker, they have to change some state in the application. Simply retrieving some data will be useless to them, as is for the users and their browsers who send the request and receive the response (even if they don’t know about it).
Of course the impact of the attack depends on the particular application and the actual user privileges, but it could be used to attempt fund transfers or purchases on behalf of the user, send messages on their behalf, change email/passwords, or do an even greater damage for administrative users.
OWASP maintains a page with recommended prevention measures for this attack which include:
- Verifying the request origin
- Including a CSRF token on every request (which should be a cryptographically strong pseudorandom value so the attacker cannot forge the token itself)
You can read more about this attack and the recommended technology agnostic prevention measures on the OWASP page.
The Open Web Application Security Project (OWASP) is a worldwide not-for-profit charitable organization focused on improving the security of software, that defines its core purpose as “Be the thriving global community that drives visibility and evolution in the safety and security of the world’s software.” You can read more about them on their about page.
How ASP.Net Antiforgery works
ASP.Net Core includes a package called Antiforgery which can be used to protect your website against CSRF attacks. This package implements the CSRF token measure recommended by the OWASP site.
More specifically, it implements a mixture of the Double Submit Cookie and Encrypted Token Pattern described in the OWASP cheat sheet.
This basically means that it provides a stateless defence mechanism composed of 2 items (or token set) that should be found on any request being validated by the Antiforgery package:
- An antiforgery token included as a cookie, generated as a pseudorandom value and encrypted using the new Data Protection API
- An additional token included either as a form field, header or cookie. This includes the same pseudorandom value, plus additional data from the current user’s identity. It is also encrypted using the Data Protection API.
These tokens will be generated server-side and propagated along with the html document to the user’s browser. The cookie token will be included by default whenever the browser sends a new request while the application needs to make sure the request token is also included. (We will see in the next sections how to do this)
A request will be then rejected if:
- any of the 2 tokens is missing or have an incorrect format/encryption
- their pseudorandom values are different
- the user data embedded in the second token doesn’t match the currently authenticated user
An attacker won’t be able to forge these tokens himself. As they are encrypted, he would need to break the encryption before being able to forge the token.
If you are in a web farm scenario, it is important for you to check how the Data Protection API stores the keys and how they are isolated by default using an application identifier. This is important as when two individual instances of your web farm use different keys, they won’t be able to understand the secrets from other instances. Probably this isn’t what you want in your web farm and you will need to configure the Data Protection API so the instances share the same keys, and are thus able to understand secrets from each other. See the asp docs for an example with the authentication cookie.
Figure 2: Antiforgery in legit requests Vs CSRF attack requests
Additionally, whenever the tokens are generated by the server and included in a response, the X-FRAME-OPTIONS header is included with the value SAMEORIGIN. This prevents the browser from rendering the page in an iframe inside a malicious website that might attempt a Clickjacking attack.
The following sections will describe in detail how to enforce Antiforgery validation in your controller actions, and how to make sure the two tokens are included within the requests.
Using Antiforgery in your ASP.NET Core application
Adding Antiforgery to the project
The Microsoft.AspNetCore.Antiforgery package is already included as a dependency by Microsoft.AspNetCore.Mvc. Similarly, the required Antiforgery services are automatically registered within the DI container by calling services.addMvc() in your Startup.ConfigureServices() method.
There are still cases where you might want to manually add Antiforgery to your project:
- If you need to override the default Antiforgery options, then you need to manually call services.AddAntiforgery(opts => {opts setup}) so you can provide your specific option settings. These options include things like the cookie name for the cookie token, and the form field or header name for the request token.
- If you are writing an ASP.Net Core application from scratch without using the MVC framework, then you will need to manually include the Antiforgery package and register the services.
Bear in mind that this is just registering Antiforgery within your project and setting the options. It does not automatically enable any kind of request validation! You will manually need to do that as per the following sections.
Protecting controller actions
Even after Antiforgery has been added to your project, the tokens will not be generated, nor validated on any request, until you tell them to. This section describes how to enable the tokens validation as part of the request processing, while the following sections describes how to generate the tokens and make sure they are included in the requests sent by the browser.
You could manually add some middleware where you require an instance of IAntiforgery through dependency injection, and then call ValidateRequestAsync(httpContext), catching any AntiforgeryValidationException and returning a 400 in that case:
try { await _antiforgery.ValidateRequestAsync(context); await next.Invoke(); } catch (AntiforgeryValidationException exception) { context.Response.StatusCode = 400; }
However if you are using MVC, then there are Antiforgery specific authorization filters that will basically handle that for you. You just need to make sure your controller actions are protected by using a combination of the following attributes:
- [AutoValidateAntiforgeryToken] – This adds the Antiforgery validation on any “unsafe” requests, where unsafe means requests with a method other than GET, HEAD, TRACE and OPTIONS. (As the other HTTP methods are meant for requests that change the server state). It will be applied globally (when added as a global filter) or at class level (when added to a specific controller class).
- [ValidateAntiForgeryToken] – This is used to add the Antiforgery validation to a specific controller action or class. It does not take into account the request HTTP method. So if added to a class, it will add the validation to all actions, even those with methods GET, HEAD, TRACE or OPTIONS.
- [IgnoreAntiforgeryToken] – Disables the Antiforgery validation in a specific action or controller. For example, you might add the Antiforgery validation globally or to an entire controller class, but you might still want to ignore the validation in specific actions.
You can mix and match these attributes to suit your project needs and team preference. For example:
- You could add AutoValidateAntiforgeryToken as a global filter and use IgnoreAntiforgeryToken in rare cases where you might need to disable it for an “unsafe” request.
- Alternatively you could add AutoValidateAntiforgeryToken just to your WebAPI-style of controllers (either manually or through a convention) and use the ValidateAntiforgeryToken in MVC-style of controllers handling many GET requests returning views with a few unsafe requests like PUT and POST actions.
Find the approach that works best for your project. Just make sure the validation is applied on every action where you need it to be.
Now let’s take a look at how to make sure the tokens are included within the requests sent by the browser.
Including the tokens in server side generated forms
For the antiforgery validation to succeed, we need to make sure that both the cookie token and the request token are included in requests that will go through the validation.
You will be relieved to hear that whenever you generate a form in a razor view using the new tag helpers, the request token is automatically included as a hidden field with the name __RequestVerificationToken (you can change it by setting a different name in the options when adding the Antiforgery services).
For example the following razor code:
<form asp-controller="Foo" asp-action="Bar"> … <button type="submit">Submit</button> </form>
Will generate the following html:
<form action="/Foo/Bar" method="post"> … <button type="submit">Submit</button> <input name="__RequestVerificationToken" type="hidden" value="CfDJ8P4n6uxULApNkzyVaa34lxdNGtmIOsdcJ7SYtZiwwTeX9DUiCWhGIndYmXAfTqW0U3sdSpzJ-NMEoQPjxXvx6-1V-5sAonTik5oN9Yd1hej6LmP1XcwnoiQJ2dRAMyhOMIYqbduDdRI1Uxqfd0GszvI"> </form>
You just need to make sure the form includes some of the asp-* tags, so it is interpreted by razor as a tag helper, and not just a regular form element.
- If for whatever reasons you want to omit the hidden field from a form, you can do so by adding the attribute asp-antiforgery="false" to the form element.
- If you use the html helpers in your razor views (as in @Html.BeginForm()), you can still manually generate the hidden input field using @Html.AntiForgeryToken().
So that’s how you generate a request token as a hidden field in the form, which will be then included within the posted data.
What about the cookie token?
Well, Antiforgery treats both the request and cookie tokens as a token set. When the IAntiforgery method GetAndStoreTokens(httpContext) is executed (which is what happens behind the scenes when generating the form hidden field), it returns the token set including both request and cookie tokens. And not only that, it will also make sure that:
- The cookie token is added as an http only cookie to the current HttpContext response.
- If the token set was already generated for this request (i.e. GetAndStoreTokens has already been called with the same httpContext), it will return the same token set instead of regenerating it again. This is important as it allows multiple elements of the page to get posted and also pass the validation, like having multiple forms (as the cookie would be the same for all forms, if every form had a different token, then only the form with the token matching the cookie one would pass the validation!).
Many of the cookie properties can be changed through the options:
- The cookie is always set as an http only cookie (so JavaScript doesn’t have access to it)
- The default cookie name is generated for each application as “.AspNetCore.AntiForgery.“ followed by a hash of the application name. It can be overridden by providing a CookieName in the options.
- No cookie domain is set by default, so the browser assumes the request domain. You can specify one through the CookieDomain option.
- The cookie path is defaulted from the current request path base (typically “/”), but can be forced with the CookiePath option.
- You can also enable the flag RequireSsl in the options, which will set the Secure flag of the cookie as true.
As the browser will send the cookies with subsequent request, whenever the form is posted, the request will contain both the cookie token (in the cookie) and the request token (in the form field).
In short, as long as you somehow generate the request token, the cookie token will also be generated and automatically added to the response.
Including the tokens in AJAX requests
In the previous section, we have seen how to generate a hidden field with the request token inside forms. But what about when sending data as part of an AJAX request, without any server-side generated form involved?
Well, this depends a lot on what JavaScript framework you are using for structuring your client-side code. Angular provides CSRF support out of the box and is so commonly used these days that even the Antiforgery repo contains an example for it. I will too take a look at the Angular-Antiforgery integration and later I will explain how a similar approach could be manually introduced for simple jQuery requests.
CSRF - The Angular case
In the case of Angular, you will be using their $http service for sending AJAX requests. This service will automatically include a header with the name X-XSRF-TOKEN if it can find the token value as a cookie with the name XSRF-TOKEN. So the easiest way is to play the way Angular wants us to, and create some middleware that will get the request token, and store its value as the XSRF-TOKEN cookie.
Even if it is added as a cookie, this is still the request token and not the cookie token! It might sound confusing, so let me try to clarify it:
- The application will send back to the browser a cookie XSRF-TOKEN with the request token and another cookie .AspNetCore.Antiforgery.* with the cookie token.
- Whenever Angular sends an Ajax request, the request will include a header X-XSRF-TOKEN with the request token and the cookie .AspNetCore.Antiforgery.* with the cookie token.
- The Antiforgery validation will make sure that both tokens are valid and share the same secret, etc.
Figure 3: CSRF tokens with Angular
Since the default header name for the request token is RequestVerificationToken, we need to change it and make sure Antiforgery searches for the request token in a header with name X-XSRF-TOKEN. Let’s just manually add Antiforgery and setup the options in the ConfigureServices method:
services.AddAntiforgery(opts => opts.HeaderName = "X-XSRF-Token");
Now we need to make sure we generate the tokens and include the request token in a cookie with name XSRF-TOKEN so Angular $http service can read it and include it as the header.
- This cannot be an http only cookie, since Angular code needs to read the cookie value so it can be included as a header in subsequent requests!
We will be interested in doing so every time we generate a full html document, so we could create a new Result Filter that basically does this if the result is a ViewResult:
public class AngularAntiforgeryCookieResultFilter: ResultFilterAttribute { private IAntiforgery antiforgery; public AngularAntiforgeryCookieResultFilter(IAntiforgery antiforgery) { this.antiforgery = antiforgery; } public override void OnResultExecuting(ResultExecutingContext context) { if (context.Result is ViewResult) { var tokens = antiforgery.GetAndStoreTokens(context.HttpContext); context.HttpContext.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken, new CookieOptions() { HttpOnly = false }); } } }
The only remaining bit is to configure it as a global filter. This way every time we render a full html document, the response will also include cookies for both the cookie token and the request token. You will do so again in the ConfigureServices method:
services.AddAntiforgery(opts => opts.HeaderName = "X-XSRF-Token"); services.AddMvc(opts => { opts.Filters.AddService(typeof(AngularAntiforgeryCookieResultFilter)); }); services.AddTransient< AngularAntiforgeryCookieResultFilter >();
And that’s it, now your Angular code can use the $http service and the tokens will be included within the request. You can check an example with a simple TODO Angular application in GitHub.
CSRF - The manual case with jQuery
If you are using a framework other than Angular (or no framework at all), the way tokens are handled might be different. However the underlying principles in every case will be the same, as you always need to make sure that:
- The tokens are generated server side.
- The request token is made available to the client JavaScript code.
- The client JavaScript code includes the request token either as a header or as a form field.
So let’s say your client code sends AJAX requests using jQuery. Since we have already created a Result Filter that includes the request token as a XSRF-TOKEN cookie, and have configured Antiforgery to look for the request token in a header named X-XSRF-TOKEN, we can reuse the same approach.
You will need to get the request token from the cookie and include it within your requests (be careful if you use something like $.ajaxSetup() as the token would be included on any requests, including those from 3rd party code using jQuery):
var token = readCookie(‘XSRF-TOKEN‘); $.ajax({ url: ‘/api/todos‘, method: ‘PUT‘, data: JSON.stringify(todo), contentType: ‘application/json‘, headers: { ‘X-XSRF-TOKEN‘: token }, success: onSuccess });
Where readCookie can be something like the jQuery cookies plugin or just your own utility for reading the value of a cookie (taken from Stack Overflow):
function readCookie(name) { name += ‘=‘; for (var ca = document.cookie.split(/;\s*/), i = ca.length - 1; i >= 0; i--) if (!ca[i].indexOf(name)) return ca[i].replace(name, ‘‘); }
But we are just using those cookie and header names because we set the server this way for Angular before. If you are not using Angular, you could use whatever name you like for the cookie with the request token (as long as the middleware adding that cookie uses the same name) and the default header name (as long as you don’t specify a different one in the options):
var token = readCookie(‘AnyNameYouWant‘); $.ajax({ … headers: { ‘RequestVerificationToken‘: token }, … });
In fact, you don’t necessarily need to use a cookie to propagate the request token to your JavaScript code. As long as you are able to do so, and the token is included within your AJAX requests, then the validation will work. For example, you could also render an inline script in your razor layout that adds the request token to some variable, which is later used by your JavaScript code executing jQuery AJAX requests:
@*In your layout*@ @inject Microsoft.AspNetCore.Antiforgery.IAntiforgery antiforgery; @{ var antiforgeryRequestToken = antiforgery.GetAndStoreTokens(Context).RequestToken; } <script> //Render the token in a property readable from your client JavaScript app.antiforgeryToken = @Json.Serialize(antiforgeryRequestToken); </script> //In your client JavaScript $.ajax({ … headers: { ‘RequestVerificationToken‘: app.antiforgeryToken }, … });
Validate the request origin
You might have noticed that OWASP recommended another protection measure, validating the request origin. The way they recommend validating it is by:
1. Get the request origin (by looking at the Origin and Referer headers)
2. Get the target origin (looking at the Host and X-Forwarded-Host headers)
3. Validate that either the request and target origins are the same, or the request origin is included in a whitelisted list of additional origins.
You can implement an IAuthorizeFilter that goes through those steps for all unsafe requests (any request with a method other than GET, HEAD, TRACE and OPTIONS) and when the validation fails, sets the result as a 400:
context.Result = new BadRequestResult();
It shouldn’t be too hard writing such a filter and including it as a global filter. If you want to see an example including options for the whitelisted extra origins, check the sample project in GitHub.
Conclusion
Security is a critical aspect for any non-trivial application. Knowing the different types of attacks and how to protect against them, is an art in itself. Luckily this entire security process is simplified for us with mature frameworks that provide more and better security features.
When it comes to the particular CSRF attack, ASP.Net Core gives you the tools to protect your application but still requires some (minimum) effort to be properly configured and setup.
You can find a sample project with Antiforgery validations on GitHub.
ASP.NET Core CSRF defence with Antiforgery