How to CSRF protect all your forms

October 16, 2008 – 2:26 pm 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

Share this:
  • Digg
  • del.icio.us
  • Facebook
  • Google
  • description
  • E-mail this story to a friend!
  • LinkedIn
  • Pownce
  • Reddit
  • StumbleUpon
  • Technorati

RSS feed Subscribe to my RSS feed

  1. 14 Responses to “How to CSRF protect all your forms”

  2. Also check your forms with: http://ha.ckers.org/xss.html

    By Harro on Oct 16, 2008

  3. Hey hey, that’s cool, but have you looked at Zend_Form_Element_Hash ? That element seems to do what you do.

    By jpauli on Oct 16, 2008

  4. Indeed, it does. However, the Hash element requires you to use Zend_Form, and you need to manually add it to each form. My plugin requires no other action than registering it with the front controller, and it works with all forms.

    By Jani Hartikainen on Oct 16, 2008

  5. Hi Jani,

    Nice plugin, thanks for sharing it with the community.

    What you could do, instead of creating an instance of CU_Controller_Plugin_CsrfProtect and registering the plugin on each request, is crate a CU_Controller_Action_Helper_GetForm helper class and generate/validate/store the key every time an instance of Zend_Form is created or a string retrieved from the cache. This way you delegate this responsibility to the Action Controller that is responsible for displaying and processing the form.

    Of course, it would be nice to have CSRF protection built-in the ResourceLoader plugin (http://tinyurl.com/ResourceLoader), and validate every time a resource class of resource type “form” is loaded.

    By Federico on Oct 18, 2008

  6. Hmmm, if you reload the page a couple of times, the csrf-key in session and the one in the page-source go out of sync. Leaving one with a “Tokens do not match” error.

    By Fili on Dec 8, 2008

  7. I’m unable to reproduce that. It shouldn’t happen, since when you do a request, it generates a new key, and it also would display the same key afterwards.

    By Jani Hartikainen on Dec 8, 2008

  8. good article,

    but why simple not use as describe in zf documentation?

    $form->addElement(’hash’, ‘no_csrf_foo’, array(’salt’ => ‘unique’));

    Internally, the element stores a unique identifier using Zend_Session_Namespace, and checks for it at submission (checking that the TTL has not expired). The ‘Identical’ validator is then used to ensure the submitted hash matches the stored hash.

    By unixvps on Jan 14, 2009

  9. That would require manually adding the element to all forms, and it would not work with non-zend_form forms. The plugin works automatically and with all forms.

    By Jani Hartikainen on Jan 14, 2009

  10. Hello I wanted to know whether this plugin cab be used for JSP.I have JSP pages so how do I use it .Thanks

    By Aniket on Jan 16, 2009

  11. Hi Jani,

    Thanks for the wonderful post.. Have you tried this with AJAX requests? I find it difficult and it’s weird that previous token and the current token are staying same. I can’t figure out why. And so the validation becomes invalid for all the requests. But for normal HTTP requests, it works like a cake.. Does anyone has this issue?

    By Sathesh on Jan 10, 2010

  12. Hi Jani, I got a work around for the problem. The real problem is this, for AJAX requests, it works for first time and not for the subsequent requests. The AJAX request was having the same token for all it’s requests but the plugin was generating unique token for all the requests. So is the mismatch / problem. That was the problem.

    Workaround:
    What I did was, for each AJAX request, I stopped generating a new token and used the already generated one to compare. So by this way I can cover for multiple AJAX requests. It could be vulnerable for the time that it lives and after that I will eject (logout) the user from the system and then they (attacker) will have to login again and get a token and then attack (if they wanted to really). That was it.

    Hope it helps someone.. Many thanks to Jani.. Saved me a hell lot of time. Thanks again dude..

    By Sathesh on Jan 10, 2010

  1. 3 Trackback(s)

  2. Nov 27, 2008: CSRF protection revisited | CodeUtopia
  3. Jan 22, 2009: Routing and complex URLs in Zend Framework | CodeUtopia
  4. Oct 25, 2009: Self Managed Guidelines, Tips & Frequently Asked Questions - Hosting Articles - RatingHost.Com

Post a Comment