Advertisement

#13 Preventing Cross Site Request Forgery Attacks

There are of course plenty of different attack vectors against any web application. In this article we will try to eliminate one of them called cross-site request forgery (CSRF). This attack happens when a user is logged in to our application and visits a malicious site which then sends a request back to our application. The fundamental way that we will deal with this type of attack is to require each request to contain a token that we can use to make sure that any request that comes into our application also originated from our application..

You can view a completed example of the authenticator component here.

We need to create and require a CSRF token

  • WebUi
    • Controllers
      • AccountController
    • Pages
      • _Layout.cshtml

In order to help prevent cross-site request forgery attacks (CSRF) we will make use of built in functionality of Asp.Net. Our approach will be to include a token on all of our pages that we can send in with a request where the server will validate the token before executing the request. Since we will be making xhr requests all throughout the application we will place a call to @Html.AntiForgeryToken() (a), which creates a hidden input with a value that is our token, within our Layout.cshtml file so that we can be sure that there will be a token contained within each page. Similarly we will place an attribute on the CheckUsernameAvailability as shown in (b). This attribute is the mechanism that requires the token that is sent in with the request to be validated.

_Layout.cshtml

...
<body>
    @Html.AntiForgeryToken()
    ...
</body>
...
(a) Placing the call to the anti-forgery token helper method will place a hidden input on our page that contains the CSRF token.

AccountController.cs

...
namespace WebUi.Controllers
{
    [Route("account")]
    public class AccountController : Controller
    {
        ...
        [ValidateAntiForgeryToken]
        public IActionResult CheckUsernameAvailability()
        {
            return Ok(new
            {
                Message = "Hello World!"
            });
        }
    }
}
(b) Including the attribute on our actions that respond to a user request requires that the CSRF token be validated before the request is processed.

With these changes made when we attempt to make a request of the server we now receive a bad request response. This is due to the fact that the ValidateAntiForgeryToken attribute that we placed on our action checks to make sure that the anti-forgery token is being included with the request. An example of an anti-forgery token is shown in (c).

An example of a hidden input that contains a CSRF token.
(d) An example of a hidden input that contains a CSRF token.
Once the [ValidateAntiForgeryToken] attribute
        is applied to an action any request that does not contain the correct CSRF token will result in a 400 bad request response.
(e) Once the [ValidateAntiForgeryToken] attribute is applied to an action any request that does not contain the correct CSRF token will result in a 400 bad request response.

Reading the token

  • WebUi
    • Source
      • services
        • dom-reader.service.ts

In order to include the anti-forgery token with our request we need to be able to read the value of input that we have previously started including in the DOM. We will accomplish this by adding a couple of additional methods to our DOM reader service. The first method we will add is a generic find method that will search the DOM for an element using a provided query selector. Once we have the element we need to extract its value, and since this will be a common task we will also make a helper function that we can use to get the value of an input element again using a provided query selector. Both of these methods can be found in (f).

dom-reader.service.ts

export class DOMReaderService {
    public getValueByQuerySelector(querySelector: string, allowNil: boolean = false): string {
        const input = this.find<HTMLInputElement>(querySelector, allowNil);
        if (typeof input === "undefined" || input === null) {
            return null;
        }
        return input.value;
    }

    public find<T extends HTMLElement>(querySelector: string, allowNil: boolean = false) {
        const element = document.querySelector(querySelector) as T;
        this.checkForNil(querySelector, element, allowNil);
        return element;
    }
    ...
    public findChild<T extends HTMLElement>(parent: HTMLElement, querySelector: string, allowNil: boolean = false) {
        const child = parent.querySelector(querySelector) as T;
        this.checkForNil(querySelector, child, allowNil);
        return child;
    }

    private checkForNil = (querySelector: string, element: HTMLElement, allowNil: boolean, parent: HTMLElement = null) => {
        if (allowNil || (typeof element !== "undefined" && element !== null)) {
            return;
        }
        if (parent !== null) {
            console.error(parent);
        }
        const container = parent === null ? "Document" : "Parent";
        throw new Error(`${container} does not contain an element with query selector: ${querySelector}.`);
    }
    ...
}
(f) Modification of our service to allow us to find an element using a query selector as well as an additional method that makes it easier to extract the value of an input again using a query selector to find it.

By placing console.log(this._domReader.getValueByQuerySelector("[name='__RequestVerificationToken']")); in the constructor of our component we can test to make sure that we are indeed able to read the value of the request verification token. And if everything has worked as it is supposed to you should see something similar to (g) in the console.

Console log showing that we are able to read the value of the request
            verification token.
(g) Console log showing that we are able to read the value of the request verification token.
Advertisement

Include the CSRF token with our requests

  • WebUi
    • Source
      • components
        • authenticator
          • authenticator-http.service.ts
      • services
        • http.service.ts

Now that we have access to the anti-forgery token we will return to our base http class and modify the constructor so that any request made will contain the required information (h). The instance of the dom reader that we need in our http service will be provided by any service that inherits from it. In our present case we will modify our authenticator http service so that angular will inject an instance of the dom reader into it so that it can be passed to the base class (i). We will use the dom reader to extract the value of the CSRF token and assign it to the headers object as the value of the 'X-XSRF-TOKEN' property. By including the request options in the get request our token will be sent with any get request that we make.

http.service.ts (1)

import { Headers, ..., RequestOptions, ... } from "@angular/http";
...
import { DOMReaderService } from "./dom-reader.service";

export abstract class HttpService {
    private readonly _requestOptions: RequestOptions = null;
    
    protected constructor(protected readonly domReader: DOMReaderService, ...) {
        this._requestOptions = new RequestOptions({
            headers: new Headers({
                "Content-Type": "application/json",
                "X-XSRF-TOKEN": this.domReader.getValueByQuerySelector("[name='__RequestVerificationToken']")
            })
        });
    }
    
    protected get = (...) => {
        this.doPromise(this.http.get(..., this._requestOptions), ...);
    }
}
(h) We will define our request options to include the CSRF token so that we can easily include it with any request that we make.

authenticator-http.service.ts (1)

...
import { DOMReaderService } from "../../services/dom-reader.service";
...
export class AuthenticatorHttpService extends HttpService {
    ...
    public constructor(domReader: DOMReaderService, ...) {
        super(domReader, ...);
    }
    ...
}
(i) We need to pass in an instance of our DOM reader service, that will be provided by angular's dependency injecto, to the base http service.

With that done when we make a request to the server we will see in the browser's developer tools that our request does in fact contain the token (j). But even with the token being included we are still receiving a 400 bad request response (k).

Image showing that our request does include the CSRF token.
(j) Image showing that our request does include the CSRF token.
Although our request contained the CSRF token in the headers we are still receiving
            a 400 bad request response.
(k) Although our request contained the CSRF token in the headers we are still receiving a 400 bad request response.

Tell the server where the token is

  • WebUi
    • Startup.cs

The reason we are still receiving a 400 response to our request is because we still need to tell the server where to find the token. We can do this by modifying our Startup.cs file to tell the server to look for the token in the header of the request (l).

Startup.cs

...
namespace WebUi
{
    public class Startup
    {
        ...
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAntiforgery(x => x.HeaderName = "X-XSRF-TOKEN");
            ...
        }
    }
}
(l) We need to tell the server that the token will be included in the header under the X-XSRF-TOKEN property.

Now that the server knows where to look for the token when we make a request we receive a 200 response.

After specifying the location of the token are requests now receive a
            200 response.
(m) After specifying the location of the token are requests now receive a 200 response.

What username?

  • WebUi
    • Source
      • components
        • authenticator
          • authenticator-http.service.ts
      • services
        • http.service.ts

We can't check that a username is available without being told what that username is. In our present case, since we are performing a get request, we need to include the username that the user wants to use in the query string of our request. To make the process of creating a query string from an object and adding it to a url easier we will create a pair helper methods in our base http class (n). Once that is done we must also modify the get method to accept and object and call the urlWithQueryString method that we just created. Now that our base class is ready we will return to our authenticator http service and pass in an object to the get request that includes the username (o).

http.service.ts (2)

import loDashForOwn = require("lodash/forOwn");
...
export abstract class HttpService {
    ...
    protected get = (..., data: Object, ...) => {
        this.doPromise(this.http.get(this.urlWithQueryString(url, data), ...), ...);
    }
    ...
    private queryString = (data: Object) => {
        if (typeof data === "undefined" || data === null) {
            return "";
        }
        const str: string[] = [];
        loDashForOwn(data, (v: any, k: any) => {
            str.push(`${encodeURIComponent(k)}=${encodeURIComponent(v)}`);
        });
        return str.join("&");
    }

    private urlWithQueryString = (url: string, data: Object) => {
        let queryString = this.queryString(data);
        if (queryString !== "") {
            queryString = `?${queryString}`;
        }
        return `${url}${queryString}`;
    }
}
(n) Modification of the http service to make it easier to convert objects to query strings and to append those strings to a url.

authenticator-http.service.ts (2)

...
export class AuthenticatorHttpService extends HttpService {
    ...
    public checkUsernameAvailability = (...) => {
        this.get(..., { username: username }, ...);
    }
}
(o) We need to include the username with our get request so that it will be sent to the server.

AccountController.cs (2)

...
namespace WebUi.Controllers
{
    ...
    public class AccountController : Controller
    {
        ...
        public IActionResult CheckUsernameAvailability(string username)
        {
            if (username == "aaaaaaaa")
            {
                return BadRequest(new
                {
                    Message = username
                });
            }
            return Ok(new
            {
                Message = username
            });
        }
    }
}
(p) Modification of the CheckUsernameAvailability action so that the model binder will populate the username parameter from the request being sent to the server. As well as, creating the ability for us to generate both success and failure states for testing.
With all of our work done we are able to include a username with our
            request.
(q) With all of our work done we are able to include a username with our request.
The fact that the username that we sent was returned to us to be displayed
            in the console means the model binder is working correctly.
(r) The fact that the username that we sent was returned to us to be displayed in the console means the model binder is working correctly.
Exciton Interactive LLC
Advertisement