Overview
Today, we will learn about Cross-Site Request Forgery attacks and how to prevent them in ASP.NET Core, JavaScript, and Angular.
Introduction
Cross-Site Request Forgery, also known as CSRF (pronounced as “See-Surf”), XSRF, One-Click Attack, and Session Riding, is a type of attack where the attacker forces the user to execute unwanted actions in an application that the user is logged in. The attacker tricks the user into performing actions on their behalf. The impact of this attack depends on the level of permissions that the user has. For example, in a vulnerable bank website, the attacker could transfer an amount of money from the victim’s account or take ownership of the whole account.
Source code
The source code for the samples of this article is available on GitHub at the following
link.
You have five projects in the repository,
- The attack sample projects, AttackSample.AttackerApp and AttackSample.VulnerableApp shows a valid CSRF attack to a vulnerable app.
- The secure sample project, SecureSample.SecureApp and SecureSample.AttackerApp shows a failed CSRF attack to a protected app.
- The Angular project, SecureSample.AngularApp, which stands on its own.
CSRF Example
If we take the bank website example, the attacker may trick the user into loading some website or link that contains a script that makes a forged request to the bank website to transfer some funds. If the user is currently logged on to the bank website, and the website is vulnerable to such attacks, the attacker may succeed.
For example, if the bank website allows the following request for transfers,
The attacker could plug his bank account number into the “to” field and sends the user to a page that has a script that makes the above request or encourages him/her into clicking a certain link, for example,
- <a href="http://vulnerable-bank.com/transfer?amount=1000&to=98765">Read More!</a>
Or embeds the request into a fake 0x0 image,
- <img src="http://vulnerable-bank.com/transfer?amount=1000&to=98765" width="0" height="0" />
The attacker could include his code in a site that the user usually visits (e.g., putting a link into some download forums), or distribute his page with some help of social engineering such as sending a link via email or chat.
If the user is already logged on to the bank account, when he/she opens the page or clicks the fake link, the browser will automatically include target website cookies and other data and performs the forged request, results in a successful transfer request.
Anatomy of an Attack
To summarize, a successfully CSRF attack consists of,
- A vulnerable website.
- A user who is currently logged on to that website.
- Session and other user cookies that the browser may include in requests.
- Easy-to-predict request parameter.
- User visits a harmful page or clicks on a fake link that executes a forged request to the vulnerable website.
Protecting Your Web Application
To protect your web application against such attacks, always,
- Use unpredictable parameters. Make it difficult for an attacker to simulate or construct a request to your application. An example of unpredictable parameters of the use of tokens, which are going to be explained soon.
- Strictly validate in every case and in every step.
CSRF Attacks in Action
In our code sample, we have two AttackSample projects, one is for the vulnerable app, and the other is for the attacker app. In the vulnerable app, the user is authenticated, and a cookie is created to save user data,
- [HttpGet]
- public bool IsAuthenticated()
- {
- return Context.Request.Cookies["IsAuthenticated"] == "1";
- }
-
- [HttpPost]
- public IActionResult Login()
- {
- Context.Response.Cookies.Append("IsAuthenticated", "1");
- return RedirectToAction(nameof(Index), "Home");
- }
-
- [HttpPost]
- public IActionResult Logout()
- {
- Context.Response.Cookies.Delete("IsAuthenticated");
- return RedirectToAction(nameof(Index), "Home");
- }
The application maintains a balance object and allows easy-to-predict requests for debit and credit operations,
- [HttpGet]
- public int Balance()
- {
- return CurrentBalance;
- }
-
- [HttpPost]
- public int Debit(int amount)
- {
- CurrentBalance -= amount;
- return Balance();
- }
-
- [HttpPost]
- public int Credit(int amount)
- {
- CurrentBalance += amount;
- return Balance();
- }
On the other hand, the attacker tricks the user into executing the fake request by promising him with a gift when he clicks the link,
- <form method="post" action="http://localhost:62833/api/Debit">
- <input type="hidden" name="amount" value="50" />
- <button type="submit">Click here to win a free iPhone!</button>
- </form>
The user is tricked into executing the fake request and resulting in a successful amount transfer.
Anti-Forgery Tokens
The key to protecting your website against those kinds of attacks, is by using unpredictable request parameters. One of those unpredictable parameters is the anti-forgery tokens.
An anti-forgery token, also called CSRF token, is a unique, secret, unpredictable parameter generated by a server-side application for a subsequent HTTP request made by the client. When that request is made, the server validates this parameter against the expected value and rejects the request if the token is missing or invalid.
So, basically, the following request,
Will be extended with a third argument:
That token is huge and impossible-to-guess. The server will include that token for the subsequent request only and will generate a new one each time a page/form is served.
Technically speaking, the anti-forgery token is not an argument that is being sent in the query string. In fact, it is a cookie represented as hidden field that you generate inside your form. When the form is submitted, this value will be included along with the request as a header. The server code will check request and validate the value sent from client.
Anti-Forgery in ASP.NET Core
By default, the new ASP.NET Core Razor engine will include an anti-forgery token for the page forms, and all you need is to add the corresponding validation. Despite this, the next few sections will let you know how to generate the anti-forgery tokens in your app and how to validate them.
Token Generation: The Manual Way
There are two ways to generate and validate anti-forgery tokens, we will start by the manual, uncomfortable way. This can be done by using the IAntiForgery service.
- @inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Csrf
- @functions {
- public string GenerateCsrfToken()
- {
- return Csrf.GetAndStoreTokens(Context).RequestToken;
- }
- }
- <form method="post">
- <input type="hidden" id="RequestVerificationToken" name="RequestVerificationToken" value="@GenerateCsrfToken()" />
- </form>
The GetAndStoreTokens method generates a request token and stores the token in a response cookie. You get access to the generated token using the RequestToken property.
The generated hidden field will be like the following (the token has been abbreviated for clarity),
- <input type="hidden" id="RequestVerificationToken" name="RequestVerificationToken" value="CfDJ8El14QZHDe5Dtl0m3qOu6_PbEHcKAJ5ZjSRj6iF...">
Another, clearer way, for manually generating CSRF tokens is by using the MVC HTML helper,
- <form method="post">
- @Html.AntiForgeryToken()
- </form>
You can check the generated cookie using Chrome DevTools,
Token Generation: The Automatic Way
As we said earlier, the new ASP.NET Core Razor engine will always generate CSRF tokens for you, however, you still have the control over the token generation process.
You can customize the token generation process for Razor pages using the AddAntiforgery method which can be called in your Startup.ConfigureServices method.
- services.AddAntiforgery(options =>
- {
- options.FormFieldName = "AntiForgeryFieldName";
- options.HeaderName = "AntiForgeryHeaderName";
- options.Cookie.Name = "AntiForgeryCookieName";
- });
The previous code will generate a hidden field for the token with the specified name, and the token will be sent along with the request with the specified header name.
- <input name="AntiForgeryFieldName" type="hidden" value="CfDJ8N1DZWaKEuhDio...">
By default, the generated cookie name in ASP.NET core is “.AspNetCore.Antiforgery.<hash>”, the field name is “__RequestVerificationToken”, and the header name is “RequestVerificationToken”.
Token Validation
Now comes the next step, the token validation. Let us start by the normal, uncomfortable way. In your target action, you may use the following code for token validation:
- private Microsoft.AspNetCore.Antiforgery.IAntiforgery Csrf { get; set; }
- public ApiController(Microsoft.AspNetCore.Antiforgery.IAntiforgery csrf)
- {
- this.Csrf = csrf;
- }
-
- private async Task<bool> ValidateAntiForgeryToken()
- {
- try
- {
- await Csrf.ValidateRequestAsync(this.HttpContext);
- return true;
- }
- catch (Microsoft.AspNetCore.Antiforgery.AntiforgeryValidationException)
- {
- return false;
- }
- }
-
-
- [HttpPost]
- public async Task<ActionResult<int>> Debit(int amount)
- {
- if (false == await ValidateAntiForgeryToken())
- return BadRequest();
-
-
- }
In the previous code, we started by defining our IAntiforgery service. This service allows you to validate a given request using the ValidateRequestAsync method which throws AntiforgeryValidationException exception when the token is invalid. We called this function the first thing in our Debit method and returned a 400 Bad Request response for invalid tokens.
Another, clearer way, to validate a CSRF token is by using the ValidateAntiForgeryToken attribute,
- [HttpPost]
- [ValidateAntiForgeryToken]
- public async Task<ActionResult<int>> Debit(int amount)
The above code will perform the same functionality as the manual IAntiforgery service code.
Now, when we run our application, we can see the difference. The app has been protected against CSRF attacks and the attacker cannot perform a request to your app.
Worth mentioning that ValidateAntiForgeryToken can be applied to the controller class and will cause CSRF validation on all endpoints. We also have some alternatives to using ValidateAntiForgeryToken attribute,
- AutoValidateAntiForgeryToken attribute: Will automatically validate endpoints for all HTTP methods except GET, HEAD, OPTIONS and TRACE.
- IgnoreAntiforgeryToken attribute: Will ignore a method from validation if the parent class is decorated with ValidateantiForgeriyToken or AutoValidateAntiForgeryToken.
Anti-Forgery in JavaScript
Let us take another example. Assume that you are accessing your API using JavaScript and you use the following code to call the credit method,
- function request(url) {
- let url = location.origin + "/api/credit?amount=10";
-
- var request = {};
- request.url = url;
- request.type = 'POST';
- request.success = function (balance) {
- $('#balance')[0].innerText = balance;
- };
- request.error = function (xhr) {
- alert(`${xhr.status} ${xhr.statusText}`);
- };
-
- $.ajax(request);
- }
When the anti-forgery validation is in action, you will receive a 400 bad request error, and this is expected because the ASP.NET Core engine cannot find the CSRF token header.
For this to work, we must add our CSRF token manually to our request headers list. A small change in our code will do the trick,
- function request(url) {
- let url = location.origin + "/api/debit?amount=10";
-
- var request = {};
- request.url = url;
- request.type = 'POST';
- request.headers = {
- 'RequestVerificationToken': $('input[name="__RequestVerificationToken"]').val()
- };
- request.success = function (balance) {
- $('#balance')[0].innerText = balance;
- };
- request.error = function (xhr) {
- alert(`${xhr.status} ${xhr.statusText}`);
- };
-
- $.ajax(request);
- }
As we said before, the default header name for CSRF tokens is “RequestVerificationToken”. If you, for any reason, changed the default header name,
- services.AddAntiforgery(options =>
- {
- options.HeaderName = "AntiForgeryHeaderName";
- });
Then you will have to change the value in your JavaScript code,
- request.headers = {
- 'AntiForgeryHeaderName': $('input[name="__RequestVerificationToken"]').val()
- };
Anti-Forgery in Angular
Normally, when accessing a CSRF-protected endpoint from an Angular app, you will receive 400 bad request if you did not specify the CSRF header.
To handle this, you must know the following,
- Angular will recognize a CSRF token only if it is stored as a cookie under Angular’s dedicated name, which is “XSRF-TOKEN”.
- Angular will always send the cookie token as a header under the dedicated name “X-XSRF-TOKEN”.
- Your app must be able to generate the CSRF cookie under Angular’s dedicated name and to validate the CSRF header also under Angular’s dedicated name.
Let us see how we do this,
- public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IAntiforgery antiforgery)
- {
- app.Use((context, next) =>
- {
-
- string path = context.Request.Path.Value;
-
- if (path.IndexOf("/api/", StringComparison.OrdinalIgnoreCase) != -1)
- {
- var tokens = antiforgery.GetAndStoreTokens(context);
- context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken,
- new CookieOptions() { HttpOnly = false });
- }
-
- return next();
- });
- }
The code is fairly easy, it does the following,
- It starts by defining a middleware that will be executed for each request. This middleware will be responsible for generating the CSRF cookie with the dedicated name.
- In this middleware, it checks the requested path. You do not need to generate the cookie for all requests, you need only for the API-targeted requests. Technical speaking, you need to generate this cookie for requests that targets our API controller, “/api/”
- Next, the code uses the IAntiforgery service to generate and store CSRF token in a cookie using the GetAndStoreTokens method. We used this mechanism before in the manual token generation section. Unless you change the default cookie name using AddForgery method (mentioned earlier,) the default name for the generated token will start with “.AspNetCore.Antiforgery” as we said earlier.
- Finally, the code reads the generated token and stores it in a cookie for the going request. It uses the dedicated Angular cookie name, “XSRF-TOKEN”.
The next step is to configure the generated the app to read the correct header. As we said earlier, we must preserve the default Angular CSRF header name, “X-XSRF-TOKEN”.
- services.AddAntiforgery(opts =>
- {
- opts.HeaderName = "X-XSRF-TOKEN";
- });
Now, run the app and see the charm. We are no longer receiving the 400 bad request error. And when investigating the request using Chrome DevTools or Fiddler, we can clearly see the generated cookie and header name.
Angular Absolute Paths Issue
The above code will work like charm if the requested path is relative,
- debit(amount: number): Observable<number> {
-
- let url = `/api/debit?amount=${amount}`;
- return this.request(url);
- }
-
- request(url: string): Observable<number> {
- let result: number;
- return this.http.post<number>(url, { });
- }
However, when the requested path is absolute, even if it is same host, Angular is not smart enough to include the CSRF token in the request. You have to manually include it in your request. The following code, for example, will not work,
- credit(amount: number): Observable<number> {
-
- let url = this.baseUrl + `api/credit?amount=${amount}`;
- return this.request(url);
- }
The solution to such case is to add the header manually to the request, or to use an HTTP interceptor to automatically add it to all requests. Let us see how to define our interceptor. We will start by the interceptor class itself.
- import { Injectable } from "@angular/core";
- import { HttpInterceptor, HttpXsrfTokenExtractor } from "@angular/common/http";
- import { HttpEvent, HttpHandler, HttpRequest } from "@angular/common/http";
- import { Observable } from "rxjs";
-
-
- @Injectable()
- export class XsrfInterceptor implements HttpInterceptor {
- constructor(private xsrfTokenExtractor: HttpXsrfTokenExtractor) {
- }
-
- intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
-
- let xsrfToken = this.xsrfTokenExtractor.getToken();
-
- if (xsrfToken != null) {
-
-
- const authorizedRequest = req.clone({
- withCredentials: true,
- headers: req.headers.set('X-XSRF-TOKEN', xsrfToken)
- });
-
- return next.handle(authorizedRequest);
- } else {
- Return next.handle(req);
- }
- }
- }
The above code simply extracts the token using the HttpXsrfTokenExtractor service which handles the token extraction from the cookie. It then copies the current request and appends the CSRF header using the dedicated name, “X-XSRF-TOKEN”. It finally, passes the request through the pipe to the next handler.
To be to use the HttpXsrfTokenExtractor service, you need to include its module in the app imports.
- imports: [
- HttpClientModule,
- ],
Finally, you must include your interceptor in the injectable objects of the application.
- providers: [
- { provide: HTTP_INTERCEPTORS, useClass: XsrfInterceptor, multi: true }
- ],
Now you can run the application and see how it operates smoothly for all requests.
Worth mentioning, that if your Angular app is contained inside a Razor CSHTML file, you can simply generate the CSRF token using the regular way,
- <div>
- <app-root></app-root>
- @Html.AntiForgeryToken()
- </div>
And in the Angular code, you can use the jQuery-style to get the value of the token and include it in your request using code like this,
- declare var $: any;
-
- const httpOptions = {
- headers: new HttpHeaders({
- 'X-XSRF-Token': $('input[name=__RequestVerificationToken]').val()
- })
- };
- this.http.post(url, httpOptions);
Summary
By the end of this article, I think you have full knowledge of how CSRF attacks work and how to protect your app from those attacks.
Again, the source code for the samples is available on GitHub at the following
link.
You have five projects in the repository,
- The attack sample projects, AttackSample.AttackerApp and AttackSample.VulnerableApp which show a valid CSRF attack to a vulnerable app.
- The secure sample projects, SecureSample.SecureApp and SecureSample.AttackerApp which show a failed CSRF attack to a protected app.
- The Angular project, SecureSample.AngularApp, which stands at its own.