#13 Preventing Cross Site Request Forgery Attacks
Friday, December 29, 2017
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..
Parts
- Part 30: User Database
- Part 29: Canonical Url
- Part 28: Razor Page Handlers
- Part 27: Send Screen Finished
- Part 26: Stroke Dashoffset
- Part 25: Send Screen
- Part 24: Forgot Link Behaviour
- Part 23: Animating Controls (cont.)
- Part 22: Animating Controls
- Part 21: Hiding Control Groups
- Part 20: ModelState Errors (cont.)
- Part 19: ModelState Errors
- Part 18: Animating Info Pages (cont.)
- Part 17: Animating Info Pages
- Part 16: Keyboard Navigation
- Part 15: Accessing the DOM
- Part 14: All About the Username
- Part 13: CSRF Attacks
- Part 12: Http Requests
- Part 11: HMR
- Part 10: Color Inputs And Buttons
- Part 9: Animating Sub-Actions
- Part 8: Form Validation (cont.)
- Part 7: Form Validation
- Part 6: Form Group
- Part 5: Authenticator Validators
- Part 4: Authenticator Inputs
- Part 3: First Angular Component
- Part 2: Webpack
- Part 1: Beginning
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
-
Controllers
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>
...
AccountController.cs
...
namespace WebUi.Controllers
{
[Route("account")]
public class AccountController : Controller
{
...
[ValidateAntiForgeryToken]
public IActionResult CheckUsernameAvailability()
{
return Ok(new
{
Message = "Hello World!"
});
}
}
}
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).
Reading the token
-
WebUi
-
Source
-
services
- dom-reader.service.ts
-
services
-
Source
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}.`);
}
...
}
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.
Include the CSRF token with our requests
-
WebUi
-
Source
-
components
-
authenticator
- authenticator-http.service.ts
-
authenticator
-
services
- http.service.ts
-
components
-
Source
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), ...);
}
}
authenticator-http.service.ts (1)
...
import { DOMReaderService } from "../../services/dom-reader.service";
...
export class AuthenticatorHttpService extends HttpService {
...
public constructor(domReader: DOMReaderService, ...) {
super(domReader, ...);
}
...
}
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).
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");
...
}
}
}
Now that the server knows where to look for the token when we make a request we receive a 200 response.
What username?
-
WebUi
-
Source
-
components
-
authenticator
- authenticator-http.service.ts
-
authenticator
-
services
- http.service.ts
-
components
-
Source
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}`;
}
}
authenticator-http.service.ts (2)
...
export class AuthenticatorHttpService extends HttpService {
...
public checkUsernameAvailability = (...) => {
this.get(..., { username: username }, ...);
}
}
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
});
}
}
}