[csrf] What is a CSRF token? What is its importance and how does it work?

The Cloud Under blog has a good explanation of CSRF tokens. (archived)

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">
 </form> 

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!">
 </form> 

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">
    </form> 

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 GET, POST 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.