Selectively Overriding CSRF Protection in CodeIgniter

Submitted by Michael Reichner on Tue, 06/07/2011 - 17:22

In response to my AJAX with CSRF Protection in Codeigniter 2.0 http://aymsystems.com/ajax-csrf-protection-codeigniter-20 post, a commenter named Equalizer wrote:

"This does not work for me. If you leave page open long enough for cookie to expire, the value is no longer retrievable, and the server does NOT set a new cookie like it would if the page were refreshed!"

The author is correct, but the issue isn't with my AJAX fix. Rather, that problem is inherent to the way CSRF protection is implemented in CodeIgniter.

The CSRF token expires based on the value set in your config.php file. The default is 7200 seconds - 2 hours.

$config['csrf_expire'] = 7200;

If a form was submitted after sitting in a user's browser for a period of time exceeding the csrf_expire value, the form submission will fail with the error:

An Error Was Encountered
 
The action you have requested is not allowed.

Enabling CSRF protection for forms is an all or nothing deal. But sometimes that protection is not needed, and the potential error can frustrate your users.

For example, if your page contains a search form, there's really no risk to the system because a search does not add or change any data. And it's not at all unusual for a user to load your page, shift to another task, and come back to your page hours later.

In a case like this, you can eliminate the potential for error by overriding the csrf_verify() function in the Security class. You do this by extending the Security class, with your own version of the csrf_verify() function.

Create a file called MY_Security.php in application/libraries with the following code:


class MY_Security extends CI_Security {
 
    function __construct()
    {
        parent::__construct();
    }
 
 
    function csrf_verify()
    {
        if (isset($_SERVER['REDIRECT_QUERY_STRING'])) {
            $path_segments = explode('/', $_SERVER['REDIRECT_QUERY_STRING']);
            $bypass = FALSE;
 
            if ($path_segments[0] == 'search') {
                $bypass = TRUE;
            }
 
            if ( ! $bypass) {
                parent::csrf_verify();
            }
        }
    }
}

All that we're doing here is checking the controller name to see if it matches the controller for which we wish to bypass CSRF verification - in this case, the "search" controller. If it does match, we do nothing. If it does not match, we simply call the original version of csrf_verify().

My rule of thumb for using this bypass functionality is as follows:

  • if the form can only be used by an authenticated user, it must go through the CSRF verification process
  • otherwise, bypassing CSRF verification may be considered

So I bypass CSRF verification for search, login and email sign-up forms.

When in doubt, you should always err on the side of caution and use the normal CSRF verification flow.