How to CSRF protect all your forms

Tags:

CSRF, or Cross-Site Request Forgery, is a vulnerability very common in websites. In short, it means that if you have your site at foo.com, and an attacker at badguy.com can display a form similar to one of your site’s, and make users on his site submit the forms on your site, possibly without their knowledge.

This can be dangerous, especially if your admin interface is compromised: There may be a button on the other site which goes to your admin interface and deletes the latest blogpost for example – and you wouldn’t want that!

Let’s look at a simple way to prevent CSRF attacks, and then how to apply it in Zend Framework apps to automatically secure all forms.

Preventing CSRF

Preventing CSRF requires three things:

  1. Make sure your forms use POST
  2. Make sure your site is not vulnerable to XSS
  3. Make your forms use a CSRF key

Step 1 isn’t really a requirement – you could CSRF protect forms which use GET too, but it’s generally a bad idea to have forms use GET for any possibly dangerous things. In fact, the only good use for GET forms I can think of is a search query, so that you can easily use it in the future by just changing the query in the URL.

Step 2 is quite critical. It’s difficult to protect forms against CSRF if there’s a script in the page which sends the CSRF key to the attacker.

Step 3 is the actual anti-CSRF measure. It’s quite simple, actually – create a key, store it in the session, and require it as a value in form submissions. If the form doesn’t contain the key generated on the previous request, it’s probably not a proper form submission.

A simple protection script

It’s relatively simple to protect a form against CSRF. We first need to generate a key, store it, and put it in the form. In the next request, the script needs to check whether the value was actually in the request or not..

<?php
if($_SERVER['REQUEST_METHOD'] == 'POST')
{
    //Here we parse the form
    if(!isset($_SESSION['csrf']) || $_SESSION['csrf'] !== $_POST['csrf'])
        throw new RuntimeException('CSRF attack');
 
    //Do the rest of the processing here
}
 
//Generate a key, print a form:
$key = sha1(microtime());
$_SESSION['csrf'] = $key;
?>
 
<form action="this.php" method="post">
<input type="hidden" name="csrf" value="<?php echo $key; ?>" />
<!-- Some other form fields you want here, and of course a submit button -->
</form>

This example is simplified a lot. In reality you would probably want to at least have the HTML code in a template file. It does demonstrate the protection method, though.

Zend Framework CSRF protection plugin

I’ve written a ZF controller plugin which automatically secures all your forms – neat, huh?

Download CU_Controller_Plugin_CsrfProtect

Using it is really easy. Just register it with your front controller and that’s it:

//this is in your bootstrap or such
$fc = Zend_Controller_Front::getInstance();
$fc->registerPlugin(new CU_Controller_Plugin_CsrfProtect());

There are also two parameters that you can use to configure the class:

$protect = new CU_Controller_Plugin_CsrfProtect(array(
    'expiryTime' => 60, //will make the CSRF key expire in 60 seconds. Defaults to 5 minutes
    'keyName' => 'safetycheck' //will make the CSRF form element be called "safetycheck". Defaults to "csrf"
));
$fc->registerPlugin($protect);

The expiry time parameter is more of a nice to have feature. You may not require it, so it defaults to a relatively lenient 5 minutes time.

How does all this work? It’s really thanks to ZF’s request/response architechture and regular expressions. When you open a page, the plugin will check if this was a POST request, and will compare the submitted form to a key automatically stored in the session in the previous request – just like the simple script. Just before your code sends the HTML response to the user, it also looks at the code and automatically adds a hidden element to all forms, thus securing them against CSRF with it.

I’ve tried to make it detect the content-type of your response. If you send out something else than html/xhtml, it will not add the elements.

Note that if you have forms inside CDATA blocks or inside JavaScript code, it will also add the hidden field to those. This is because detecting if the code is inside CDATA or JS is probably more time consuming than it’s worth. Of course, I’m open to suggestions in this regard.

I’ve tested the class, but do tell if you find any issues with it.

There’s also the matter of what to do when a CSRF attack is detected. For this, I recommend reading Preventing CSRF Properly at Tom Graham’s blog.

For some further improvements on the plugin check out CSRF protection revisited