What is a CSRF token and how does it work?

Many modern web frameworks like Laravel or the Play Framework have built-in support to protect your web application against cross-site request forgery (CSRF). That’s a good thing, but it is not always clear to every developer when and how to use it. In this article I try to give some (hopefully) easily understandable answers.

Before we dive in, let’s remind ourselves of some given rules and restrictions:

First of all, in this article I assume that you have not configured your web server to explicitly enable cross-site AJAX requests by setting CORS headers such as X-Requested-With.

  1. If this condition is met, which is the default, a web browser would not perform an AJAX request to a domain other than the domain the page was loaded from. Example: A HTML page loaded from a.com cannot request and parse data via AJAX from b.com.
  2. A HTML form can only submit GET and POST requests, not PUT, DELETE or any other.
  3. Cookies are bound to a certain domain (or subdomains) and can only be set and accessed if the cookie domain matches the domain where the page was loaded from. Example: A cookie set for a.com cannot be read by a page or its scripts loaded from b.com.
  4. You cannot submit custom headers with a HTML form. The only exception is a header describing the form encoding (but this doesn’t really qualify as a custom header).
  5. You stick to good practice by distinguishing between request methods, which means two things:
    (1) On the server, only process POST requests and POST data if the form was intended to make a POST request. Don’t read GET parameters and assume they were posted, e.g. in PHP by reading $_REQUEST instead of $_POST. If you expect a POST request and receive a GET request you could, for example, return an error response 405 Method Not Allowed.
    (2) Design your requests in a way that GET requests are only used to actually get data from the server, but never make any significant changes to the state of your web application, e.g. the database. For example, don’t use a GET request to let the user to change their email address. If it’s just some logging, that’s alright of course.

What is a CSRF token good for?

A valid CSRF token does not tell you on the server side that the client has sent valid or trustable data, it rather tells you that it most likely was the user’s intention to send you the data. Always remember: Never trust client data blindly. You still need to validate the data.

Is it not always the users’ intention to send data if they do?

No. Let me give you an example…

Imagine you had a website like a simplified Twitter, hosted on a.com. Signed in users can enter some text (a tweet) into a form that’s being sent to the server as a POST request and published when they hit the submit button. On the server the user is identified by a cookie containing their unique session ID, so your server knows who posted the Tweet.

The form could be as simple as that:

<form action="http://a.com/tweet" method="POST">
  <input type="text" name="tweet">
  <input type="submit">

Now imagine, a bad guy copies and pastes this form to his malicious website, let’s say b.com. The form would still work. As long as a user is signed in to your Twitter (i.e. they’ve got a valid session cookie for a.com), the POST request would be sent to http://a.com/tweet and processed as usual when the user clicks the submit button.

So far this is not a big issue as long as the user is made aware about what the form exactly does, but what if our bad guy tweaks the form like this:

<form action="https://example.com/tweet" method="POST">
  <input type="hidden" name="tweet" value="Buy great products at http://b.com/#iambad">
  <input type="submit" value="Click to win!">

Now, if one of your users ends up on the bad guy’s website and hits the “Click to win!” button, the form is submitted to your website, the user is correctly identified by the session ID in the cookie and the hidden Tweet gets published.

If our bad guy was even worse, he would make the innocent user submit this form as soon they open his web page using JavaScript, maybe even completely hidden away in an invisible iframe. This basically is cross-site request forgery.

A form can easily be submitted from everywhere to everywhere. Generally that’s a common feature, but there are many more cases where it’s important to only allow a form being submitted from the domain where it belongs to.

Things are even worse if your web application doesn’t distinguish between POST and GET requests (e.g. in PHP by using $_REQUEST instead of $_POST). Don’t do that! Data altering requests could be submitted as easy as <img src="http://a.com/tweet?tweet=This+is+really+bad">, embedded in a malicious website or even an email.

How do I make sure a form can only be submitted from my own website?

This is where the CSRF token comes in. A CSRF token is a random, hard-to-guess string. On a page with a form you want to protect, the server would generate a random string, the CSRF token, add it to the form as a hidden field and also remember it somehow, either by storing it in the session or by setting a cookie containing the value. Now the form would look like this:

<form action="https://example.com/tweet" method="POST">
  <input type="hidden" name="csrf-token" value="nc98P987bcpncYhoadjoiydc9ajDlcn">
  <input type="text" name="tweet">
  <input type="submit">

When the user submits the form, the server simply has to compare the value of the posted field csrf-token (the name doesn’t matter) with the CSRF token remembered by the server. If both strings are equal, the server may continue to process the form. Otherwise the server should immediately stop processing the form and respond with an error.

Why does this work?

There are several reasons why the bad guy from our example above is unable to obtain the CSRF token:

Copying the static source code from our page to a different website would be useless, because the value of the hidden field changes with each user. Without the bad guy’s website knowing the current user’s CSRF token your server would always reject the POST request.

Because the bad guy’s malicious page is loaded by your user’s browser from a different domain (b.com instead of a.com), the bad guy has no chance to code a JavaScript, that loads the content and therefore our user’s current CSRF token from your website. That is because web browsers don’t allow cross-domain AJAX requests by default.

The bad guy is also unable to access the cookie set by your server, because the domains wouldn’t match.

When should I protect against cross-site request forgery?

If you can ensure that you don’t mix up GETPOST and other request methods as described above, a good start would be to protect all POST requests by default.

You don’t have to protect PUT and DELETE requests, because as explained above, a standard HTML form cannot be submitted by a browser using those methods.

JavaScript on the other hand can indeed make other types of requests, e.g. using jQuery’s $.ajax() function, but remember, for AJAX requests to work the domains must match (as long as you don’t explicitly configure your web server otherwise).

This means, often you do not even have to add a CSRF token to AJAX requests, even if they are POST requests, but you will have to make sure that you only bypass the CSRF check in your web application if the POST request is actually an AJAX request. You can do that by looking for the presence of a header like X-Requested-With, which AJAX requests usually include. You could also set another custom header and check for its presence on the server side. That’s safe, because a browser would not add custom headers to a regular HTML form submission (see above), so no chance for Mr Bad Guy to simulate this behaviour with a form.

If you’re in doubt about AJAX requests, because for some reason you cannot check for a header like X-Requested-With, simply pass the generated CSRF token to your JavaScript and add the token to the AJAX request. There are several ways of doing this; either add it to the payload just like a regular HTML form would, or add a custom header to the AJAX request. As long as your server knows where to look for it in an incoming request and is able to compare it to the original value it remembers from the session or cookie, you’re sorted.