Let's go CSRF-ing!

Get your surfboard ready, we are heading to the sea ... and the less than relaxed and laid back world of CSRF (pronounced "sea surf") attacks!


What is CSRF?

Cross-Site Request Forgery (abbreviated as CSRF or XSRF) is an exploit which tricks a web application into sending a malicious request on behalf of an authenticated user. It is also known as one-click attack, session riding, hostile linking, or cross-site reference forgery. This allows an attacker to trick a web application into executing any actions of their choosing as if they were the authenticated user.

Fundamentally, a CSRF attack relies on user's identity on a given web application and the web application's server's trust in that identity. As the attacker does not receive the response of the malicious request, only requests with side effect present a risk vector (for example: a request that transfers funds, changes passwords, ...).

In order for a CSRF attack to be successful, an attacker needs to have knowledge of the APIs they are targeting to be able to craft a valid request. They also need to make use of social engineering to trick users to visit a web page in their control or open an email they sent, and in some cases, albeit not necessarily, interact with said page or email. The victims should also be logged into the specific service when the attack is performed. These conditions make such attacks somewhat complex, but in most severe cases (for example, targeting a user with administrative rights), CSRF attacks can potentially lead to the compromise of the entire web application.

Some major CSRF attacks include:

  • Netflix's website in 2006 (when Netflix was still renting DVDs!), which allowed attackers to order DVDs for a victim, change the shipping address, or fully compromise the account by changing the login credentials.
  • ING Direct's online banking web application, which allowed attackers to transfer money from victims' accounts.
  • YouTube's website in 2008, which allowed attackers to perform nearly all actions as a given user.

Any web application that accepts HTTP requests from an authenticated user and does not implement a verification mechanism to ensure that the request is unique to the user's session is potentially vulnerable.


How does a CSRF attack work?

The vulnerability lies in the fact that a web application will trust any request sent by the user's browser as legitimate, even if the request was not meant to be sent by the user, but crafted by a malicious actor. From the server perspective though, the request looks totally valid and legitimate as if it was sent by the user themselves. This allows a malicious actor to basically impersonate a user. This particular attack works because authentication tokens are usually stored in cookies, and most browsers will send said cookies with each request.

[1] Alice logs into her bank account online portal. This sets a session cookie (A) that will be automatically sent with every subsequent request Alice's browser makes to the bank web app.
[2] Alice sends money to Bob. Attached with the request sent by her browser is the session cookie (A) previously generated. That allows the bank's backend to authenticate Alice and ensure the request is legitimate.
[3] In the meantime, Mallory crafts a script that will send a similar request, but sending money to her account instead. See below for more details on how to craft such scripts.
[4] Using social engineering, Mallory tricks Alice into visiting her website, which then tricks Alice's browser into sending Mallory's request to the bank's backend.
[5] Because the request stemmed from Alice's browser, it has Alice's session cookie (A) attached to it. The bank application is then tricked into believing this request comes from Alice and is legitimate, hence transferring money to Mallory.

URL-based attack

The most basic form of CSRF attack is URL-based. An attacker crafts a GET request with the desired URL, and embeds said URL in an image, for example. That image can then be sent via email to the victim or hosted in a website owned by the attacker that the victim then visits.

Let's say that there exists a banking web application built solely using GET requests, which stores session tokens in cookies, and that has no CSRF prevention method implemented.

For Alice to send $100 to Bob, the app will make the following request:
GET https://some-random-bank.com/transfer?account=BOB&amout=100

With that information in mind, Mallory can craft a valid request that would send her $1,000, namely:
GET https://some-random-bank.com/transfer?account=MAL&amount=1000

Now, for the social engineering part of the attack, Mallory embeds that URL into a zero-size image that she attaches to an email she sends Alice:

<img src="https://some-random-bank.com/transfer?account=MAL&amount=1000" width="0" height="0" border="0" />

When opening the email, Alice won't see anything suspicious, but her browser will make that request, and if Alice is logged in her online banking web application, the transaction will be successful and Mallory will receive $1,000 from Alice!

This works because the session cookies that authenticate Alice from the bank's application's perspective will be automatically attached to and sent with the malicious request.

Form-based attack

Alright, so I guess we can agree that using GET requests to perform actions with side-effects is not ideal. Unfortunately, using POST requests won't save us!

It might take Mallory a couple of lines of code more, but it is still possible (and quite trivial) to craft a POST request that can take advantage of a CSRF vulnerability.

Let's keep our online banking application from the previous example, only this time, the request to make a transfer is:

POST https://some-random-bank.com/transfer

account=BOB&amount=100

Now, Mallory cannot simply use a link or an image, but she can use a form, which she can embed in a web page she controls.

<form action="https://some-random-bank.com/transfer" method="POST">
  <input type="hidden" name="account" value="MAL" />
  <input type="hidden" name="amount" value="1000" />
  <input type="submit" value="Click here" />
</form>

As with the URL-based attacks, Alice doesn't even need to interact with the web page that includes the malicious form, as Mallory can automatically submit it when Alice visits her web page:

<body onload="document.forms[0].submit()">
  ...
  <form ...
</body>

All cookies (including authentication ones) will again be sent with the request, and Mallory pockets yet again $1,000!

XHR-based attack

OK, this is great, but what if we use a JSON API, and actually use other HTTP verbs such as PUT or DELETE? Well, still no luck!

Let's keep using the same banking example. This time, the request to transfer money is as follows:

PUT https://some-random-bank.com/transfer

{ "account": "BOB", "amount": 100 }

In that case, Mallory will have to work a little bit harder, but it is still a handful of lines of code:

<script>
function put() {
  var x = new XMLHttpRequest();
  x.open("PUT", "https://some-random-bank.com/transfer", true);
  x.setRequestHeader("Content-Type", "application/json");
  x.send(JSON.stringify({ "account": "MAL", "amount": 1000 }));
}
</script>

<body onload="put()">
  ...
</body>

Fortunately, this request will not execute in modern browsers thanks to same-origin policy restrictions, which is enabled by default. Careful though with allowing cross-origin requests, as that can allow attackers to bypass those restrictions. In particular, using the following CORS header will make the above CSRF attack possible:
Access-Control-Allow-Origin: *.


How to protect a web app from CSRF attacks?

Now that we have a better understanding of the risks of CSRF attacks, how do we protect a web application from such vulnerabilities?

Methods that do NOT work

Let's first look at some methods that do not work in protecting a web application from CSRF attacks and why that is the case.

One way one might think of preventing CSRF is by using a secret cookie to store the session token. Unfortunately, this method fails because all cookies, including secret cookies, are sent with every request.

Only POST Requests

Some past CSRF vulnerabilities came from the fact that some web application were using GET request to perform side effects on the server. Besides being a poor practice, this made URL-based CSRF attacks trivial to implement.

Hence, can the solution be only using POST requests? Unfortunately, as seen in the previous section, it is still possible to craft CSRF attacks using POST (or any other HTTP) requests.

Multi-Step Transactions

Maybe using multi-step transactions then? For example, we can require a first request to make a bank transfer, and a second to confirm? Unfortunately, this method also fails, as long as the attacker can predict the steps needed and craft malicious requests.

Prevention methods

Let's now look at some prevention techniques that do work in protecting a web application from CSRF attacks and why that is the case.

Synchroniser Token Pattern

One of the most common prevention methods is to generate a token on the server. A token can be generated per request or per session, the latter being slightly less secure but more convenient. The token is then sent with each request and validated before performing said request. The token is usually embedded in a hidden form field, or in a custom header. This means that a malicious CSRF request will not possess the token and will fail validation on the server, as only cookies are sent automatically, and the attacker has no way of accessing data on the web page.

For example, the server-side rendered HTML for a form could look like:

<form action="/transfer" method="POST">
  <input type="hidden" name="CSRFToken" value="BfbhY4e/7Qa7iWUMV09r5lm0mAdXnDHGBdYfgHCMnKf8yuxVcULDdEYSDYotrpmoo2NKGzuDyHjzD74QUyfq5g==">
  ...
</form>

Taking another look at our previous example with Alice and Mallory, by implementing this method, Alice's request to transfer money to Bob will contain the CSRF token, whereas Mallory has no way of guessing its value (even if she knows that she must also send a token), hence her malicious request won't be valid from the server's perspective.

This method is what most popular web frameworks implement.

If maintaining state on the server side is an issue, we can use the double submit cookie technique. The idea here is to send a random value both in a cookie and as part of the request (in a parameter or a header). If both values match, the server accepts the request as legitimate and proceeds.

This method works because the attacker doesn't have access to the value of the token stored in the cookie. Thus, when crafting the malicious request, they cannot include the same value as part of the request. The value in the cookie will automatically be sent to the server, but the validation will fail.

As subdomains can write cookies to the parent domain over HTTP, this technique only works if all subdomains are properly secured and only accept HTTPS. It is also possible to secure the cookie by using the __Host- cookie prefix. Another way to enhance the security of this method is to use an encrypted cookie to store the token.

The SameSite cookie attribute aims to mitigate CSRF vulnerabilities by providing a hint to browsers if they should submit cookies with cross-origin requests.

Possible values are Strict, Lax, and None.

Strict prevents any cross-origin request to carry cookies. This means for example, that if you follow a link to a service where you are authenticated, the page that will be displayed won't be able to authenticate you, as no cookies will be submitted. This might not always be the intended user experience.

Lax, which is the default in some modern browsers, provides a better user experience while still ensuring that only top level navigation and safe HTTP method request are submitted with cookies.

This method is unfortunately not sufficient to fully protect users from CSRF attacks, and should instead be used in conjunction with previous methods.

Origin Headers

This method relies on examining HTTP request header values, in particular to find out the source origin (where is the request coming from) and the target origin (where is the request going to). If both values match, the server proceeds with the request as legitimate.

The reliability of the value in those headers come from the fact that they can only be set by the browser as they are in the forbidden headers list, meaning that they cannot be set programmatically.

The drawback of this method is that it can be difficult to accurately retrieve the values for source origin and target origin.

Custom Request Headers

An alternate method that works for AJAX or API endpoints is to set a custom request header, with the presence of this header being validated on the server. This method relies on same-origin policy to ensure that only JavaScript from the legitimate domain can set those headers.

This is a particularly attractive method for REST services, as it doesn't require the server to maintain any state. Unfortunately, this method doesn't cover vulnerabilities on <form>s.

The security of this method also depends on having robust CORS settings (as cross-origin requests with custom headers are pre-flighted and might expose the list of custom headers).

User Interaction Defense

Finally, we can also fend off CSRF attacks by altering the user interaction flow of certain actions. For example, we can ask the user to re-enter their password to confirm certain actions (like transferring funds).

This will impact the user experience though, so it might not make sense to solely rely on this technique to secure an entire web application.

As CSRF vulnerabilities basically exist in any web application with authentication, most web frameworks implement some sort of protection against them. Let's look at a few examples:

Django

Django implements a middleware and template tag to mitigate CSRF attacks. Note that "login CSRF" attacks are also covered. The CSRF middleware is activated by default.

For server-rendered markup, we can add the CSRF token in any form like follows:

<form method="post">{% csrf_token %}

For AJAX requests, a custom X-CSRFToken header needs to be appended to the requests. The value of the token can either be retrieved from a csrfToken cookie, or directly from the server-rendered markup:

{% csrf_token %}
<script>
  const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value;
</script>

For more details, including how to handle some edge cases, feel free to check the official documentation: https://docs.djangoproject.com/en/3.2/ref/csrf/

Laravel

Laravel automatically generates CSRF tokens for each user session. It also uses a middleware by default to check the validate of said tokens.

The token can be accessed on the server via the following methods:

use Illuminate\Http\Request;

Route::get('/token', function (Request $request) {
    $token = $request->session()->token();
	// or
    $token = csrf_token();
});

For server-rendered markup, the following code allows to embed the token in forms:

<form method="POST" action="/profile">
    @csrf
    <!-- Equivalent to... -->
    <input type="hidden" name="_token" value="{{ csrf_token() }}" />
</form>

For AJAX request, the token can be retrieved from a meta tag and sent as a custom X-CSRF-TOKEN header:

<meta name="csrf-token" content="{{ csrf_token() }}">
$.ajaxSetup({
    headers: {
        'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
    }
});

Finally, the token is also set in a secure cookie XSRF-TOKEN.

For more details, including how to handle some edge cases, feel free to check the official documentation: https://laravel.com/docs/8.x/csrf

Express

Express doesn't implement mitigation for CSRF attacks by default, but provides an npm package: csurf.

That package can be used to implement either the synchroniser token pattern (which requires a session middleware such as express-session), or the double submit cookie method (which requires the cookie-parser middleware).

The value of the token can be retrieved via the req object:

req.csrfToken();

For server-rendered markup, the following code can be used:

var cookieParser = require('cookie-parser')
var csrf = require('csurf')
var bodyParser = require('body-parser')
var express = require('express')

// setup route middlewares
var csrfProtection = csrf({ cookie: true })
var parseForm = bodyParser.urlencoded({ extended: false })

// create express app
var app = express()

// parse cookies
// we need this because "cookie" is true in csrfProtection
app.use(cookieParser())

app.get('/form', csrfProtection, function (req, res) {
  // pass the csrfToken to the view
  res.render('send', { csrfToken: req.csrfToken() })
})

app.post('/process', parseForm, csrfProtection, function (req, res) {
  res.send('data is being processed')
})
<form action="/process" method="POST">
  <input type="hidden" name="_csrf" value="{{ csrfToken }}">
  
  Favorite color: <input type="text" name="favoriteColor">
  <button type="submit">Submit</button>
</form>

For AJAX request, the token can be retrieved from a meta tag and sent as a custom CSRF-Token header:

<meta name="csrf-token" content="{{ csrfToken }}">
// Read the CSRF token from the <meta> tag
var token = document.querySelector('meta[name="csrf-token"]').getAttribute('content')

// Make a request using the Fetch API
fetch('/process', {
  credentials: 'same-origin', // <-- includes cookies in the request
  headers: {
    'CSRF-Token': token // <-- is the csrf token as a header
  },
  method: 'POST',
  body: {
    favoriteColor: 'blue'
  }
})

Finally, in some cases it might also be possible to send the token via a cookie, most notably for single-page applications:

app.all('*', function (req, res) {
  res.cookie('XSRF-TOKEN', req.csrfToken())
  res.render('index')
})

For more details, including how to handle some edge cases, feel free to check the official documentation: http://expressjs.com/en/resources/middleware/csurf.html

Spring

Spring provides CSRF mitigation by default since Spring Security 4.0.

For server-rendered markup, the following example shows how to embed a CSRF token to a form:

<c:url var="logoutUrl" value="/logout"/>
<form action="${logoutUrl}"
	method="post">
<input type="submit"
	value="Log out" />
<input type="hidden"
	name="${_csrf.parameterName}"
	value="${_csrf.token}"/>
</form>

For AJAX requests, the token can be embedded in a meta tag and retrieved via JavaScript on the client:

<html>
<head>
	<meta name="_csrf" content="${_csrf.token}"/>
	<!-- default header name is X-CSRF-TOKEN -->
	<meta name="_csrf_header" content="${_csrf.headerName}"/>
	<!-- ... -->
</head>
<!-- ... -->
$(function () {
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
$(document).ajaxSend(function(e, xhr, options) {
	xhr.setRequestHeader(header, token);
});
});

It is also possible to persist the CSRF token in a cookie, by default XSRF-TOKEN, and expect the value back in a custom X-XSRF-TOKEN header.

For more details, including how to handle some edge cases, feel free to check the official documentation: https://docs.spring.io/spring-security/site/docs/5.0.x/reference/html/csrf.html


Login CSRF

A related type of attack that we haven't discussed at all so far is login CSRF. This attack is somewhat similar to the previous we have discussed, but targets login forms, making the impact and risk different.

Login CSRF can be mitigated by creating pre-sessions and embedding the token in the login form, or using any of the techniques previously discussed.


References