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:
  1. 34 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

  13. Hi all, and thanks for sharing!

    I have tryied this plug in, and as far I see, sometimes the token that was stored in session, is lost beetween one request and the other.

    One of these times is when in the view script (the .phtml file), there is a broken image resource.
    es:

    No Idea why.

    By Bitsurs on Apr 24, 2010

  14. Something that I seem to be finding is that if I submit a form which is on the first page of my site (i.e. when the session is first set-up) then for some reason the post and session don’t match. After that initial failure I can go back to the first page and it works fine.

    I took out the sha call and for some reason the two microtime’s do not match. For some reason my session value appears to be changing.

    By Rjs37 on Oct 12, 2010

  15. Rjs37, it could be that if you use an iframe or browse your page in multiple tabs (or something similar) the value might change.

    By Jani Hartikainen on Oct 12, 2010

  16. Nah, no iframes or other tabs. Think I’m getting to the bottom of it though. The file which is setting the session seems to be getting called multiple times when the session is first created. So just gotta work out whats causing that.

    By Rjs37 on Oct 13, 2010

  17. Okay, sorted it. I was getting a 404 error in the background because it couldn’t find a favicon. This 404 file was then including my global file and therefore changing the session value.

    So I just needed to add my favicon in lol. I also put in a quick check to make sure that PHP_SELF isn’t looking at my 404 file. Just in-case it ever happens with another file that can’t be found.

    By Rjs37 on Oct 13, 2010

  18. Hey Thanks for it,

    I am having a strange issue here

    I am using zend 1.11.4 version. Loaded plugin through application.ini, it executed properly. I got input element into the form properly. When I submit a form, plugin called twice and getting exception saying the code mismacthed.

    There is _redirect(), _forward() etc

    Please advise me

    By venu on May 2, 2011

  19. sorry for the typo.

    There is no _redirect() or _forward() etc

    By venu on May 2, 2011

  20. Some additional information about ZF and CSRF you can find in http://plutov.by/post/zf_csrf

    By Alexander on Sep 22, 2011

  21. You should delete/update the session variable right after you get into the if statement to prevent multiple submits to a form

    so

    if FROM_POSTED and KEY = SESSION_KEY
    set SESSION_KEY to SOMETHING_NEW
    do beautiful proccessing
    else
    set SESSION_KEY to SOMETHING
    endif

    By Philip on Dec 21, 2012

  22. You should delete/update the session variable right after you get into the if statement to prevent multiple submits to a form

    so

    if FROM_POSTED and KEY = SESSION_KEY
        set SESSION_KEY to SOMETHING_NEW
        do beautiful proccessing
    else
        set SESSION_KEY to SOMETHING
    endif

    By Philip on Dec 21, 2012

  23. This is an interesting post, but I have a question that doesn’t get answered in your article…

    How do you unittest a zend controller that has a post method attached to it when the form uses this plugin? My unittests fail because there doesn’t appear to be a zend form element from which the value can be retrieved?

    Thanks

    Glenn

    By Glenn on Jul 10, 2014

  1. 12 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
  5. May 24, 2010: Guidelines, Tips & Frequently Asked Questions - Online Internet Marketing and Search Engine Optimization Forum
  6. May 24, 2010: Guidelines, Tips & Frequently Asked Questions - Online Internet Marketing and Search Engine Optimization Forum
  7. Sep 25, 2011: Preventing Session Hijacking, CSRF
  8. May 23, 2012: Why codeigniter2 doesn't store the csrf_hash in a more secure way, such as session? | PHP Developer Resource
  9. Aug 10, 2012: How can you secure your web site or application from CSRF – Cross Site Request Forgery attacks | Eko UK
  10. Mar 2, 2013: Routing and complex URLs in Zend Framework
  11. May 4, 2013: Protection in my php form | BlogoSfera
  12. May 23, 2013: CSRF Referer & Token protection bypass with XSS
  13. Aug 29, 2013: Plugin para Zend Framework para evitar CSRF | School of Net Cursos

Post a Comment

You can use some HTML (a, em, strong, etc.). If you want to post code, use <pre lang="PHP">code here</pre> (you can replace PHP with the language you are posting)