CodeIgniter CSRF (XSRF) library

They say a lot of developers work on things to scratch their own itch. This is me scratching.
I'm a fan of the CodeIgniter project but I find it odd that such a well developed framework lacks CSRF protection in the core. There are a few CI CSRF libs but most of them are outdated so I wrote my own. A few ideas came from Michael Wales' toolkit but this library is written from scratch. One of the major differences is that this toolkit uses form IDs instead of a timer to handle forms in different tabs which I find more robust since it doesn't rely on time.
Download csrf-v4.tgz
Update 2009.07.13
The first version would fill up the cookie if session DB was not used and users opened too many forms without submitting them. This update clears old tokens and limits the number of tokens to 3 or 10 depending whether session data are stored in cookies or a database.
Installation
This library consists of 3 required files and 2 example files.
- Csrf.php - CI library class
- MY_Form_validation.php - CI library extension
- csrf_helper.php - CI helper
- csrf_test.php - Example CI controller
- csrf_form.php - Example CI view
- Copy Csrf.php and MY_Form_validation.php to application/libraries/
- Copy csrf_helper.php to application/helpers/
Usage
Load both the csrf library and helper in classes which use forms. The sesson and form_validation libraries and form helper are required by the csrf library so these should be loaded as well. MY_Form_validation is automatically loaded since it's an extension of form_validation. Tokens are validated when form_validation->run() is called.
Example controller csrf_test.php
/**
* Csrf test controller
*
* Example controller with CSRF protection
*/
class Csrf_test extends Controller {
function Csrf_test()
{
parent::Controller();
}
function index()
{
$data = array();
$this->load->library('session');
$this->load->library('form_validation');
$this->load->library('csrf');
$this->load->helper('form');
$this->load->helper('csrf_helper');
// Set validation rules
$this->form_validation->set_rules('foo', 'Foo', 'trim|required|min_length[3]|max_length[32]|alpha_dash');
// Run validation
if ($this->form_validation->run()) {
$data['message'] = 'Valid';
}
else {
$data['message'] = validation_errors();
}
// Show login form
$this->load->view('csrf_form', $data);
}
}
Then, in every form, print form_token()
Example view csrf_form.php
<HTML>
<HEAD></HEAD>
<BODY>
<p><?=$message;?></p>
<div>
<?=form_open('/csrf_test/');?>
<?=form_token();?>
<br />
<?=form_label('Foo', 'foo');?>
<br />
<?=form_input(array('name'=>'foo', 'value'=>set_value('foo')));?>
<br />
<?=form_submit(array('name'=>'submit', 'value'=>'Send'));?>
<?=form_close();?>
</div>
</BODY>
</HTML>
That's it.
Csrf.php library source
/**
* Csrf library class
*
* Protects against cross site request forgery attempts
*/
class Csrf
{
var $formID = NULL;
var $token = NULL;
var $CI = NULL;
function Csrf()
{
log_message('debug', 'Csrf class initialized');
// Get the CI super object
$this->CI =& get_instance();
}
/**
* Creates and saves a form ID & token
*
* @access public
* @return array
*/
function create_token() {
log_message('debug', 'Csrf::create_token() called');
// Get existing tokens from the session
$tokens = $this->CI->session->userdata('tokens');
if(!is_array($tokens)) $tokens = array();
// Remove old tokens
$now = time();
foreach(array_keys($tokens) as $key) {
if($tokens[$key]['ts'] > $now + 86400) {
unset($tokens[$key]);
}
}
// Limit the tokens saved. Less if stored in a cookie
$numTokens = 3;
if($this->CI->config->item('sess_use_database')) {
$numTokens = 10;
}
if(count($tokens) >= $numTokens) {
// Trim and re-index the array but keep the array order.
$tokens = array_values(array_slice($tokens, 0, $numTokens, TRUE));
}
// Generate data for the new token
$token = md5(uniqid(rand(), TRUE));
$formID = uniqid(rand());
// Add the new token to the token array and save to the session
$tokens[] = array('ts'=>$now, 'token'=>$token, 'formID'=>$formID);
$this->CI->session->set_userdata('tokens', $tokens);
// Save the token data for this instance
$this->formID = $formID;
$this->token = $token;
return array('formID'=>$formID, 'token'=>$token);
}
/**
* Returns the current form ID and token
*
* @access public
* @return array
*/
function get_token() {
log_message('debug', 'Csrf::get_token() called');
if(! $this->formID || ! $this->token) {
log_message('debug', 'Csrf::get_token() invalid token');
return FALSE;
}
return array('formID' => $this->formID, 'token' => $this->token);
}
/**
* Saves a form ID and token
*
* @access public
* @param string formID
* @param string token
* @return void
*/
function save_token($formID, $token) {
log_message('debug', 'Csrf::save_token() called');
$this->formID = $formID;
$this->token = $token;
}
/**
* Clears form ID and token after successful validation
*
* @access public
* @return void
*/
function clear_token() {
log_message('debug', 'Csrf::clear_token() called');
// Get existing tokens
$tokens = $this->CI->session->userdata('tokens');
// No existing tokens. Clear current tokens and return.
if(!is_array($tokens)) {
$this->formID = $this->token = NULL;
return NULL;
}
// Loop through existing tokens and remove this one only
foreach(array_keys($tokens) as $key) {
if($tokens[$key]['formID'] == $this->formID) {
unset($tokens[$key]);
}
}
// Reindex the remaining tokens and save them to the session
$tokens = array_values($tokens);
$this->CI->session->set_userdata('tokens', $tokens);
// Clear current tokens
$this->formID = $this->token = NULL;
}
/**
* Validates token sent in POST
*
* @access public
* @param string formID
* @param string token
* @return void
*/
function validate_token($formID, $token) {
log_message('debug', 'Csrf::validate_token() called');
// Get existing tokens
$tokens = $this->CI->session->userdata('tokens');
// No known tokens
if(!is_array($tokens)) return FALSE;
// Loop through tokens for a match
foreach(array_keys($tokens) as $key) {
if($tokens[$key]['formID'] == $formID && $tokens[$key]['token'] == $token) {
return TRUE;
}
}
return FALSE;
}
}
csrf_helper.php helper source
/**
* CSRF helper
*
* Inserts formID and token hidden form fields
*
* @access public
* @return string HTML hidden input elements
*/
function form_token() {
$CI =& get_instance();
// Form helper and csrf lib should be loaded from the controller
// $CI->load->helper('form');
// $CI->load->library('csrf');
// Get the token from the csrf class
$tokenArray = $CI->csrf->get_token();
if(!$tokenArray) {
// Token is bad. Create a new one
$tokenArray = $CI->csrf->create_token();
}
// Return token hidden form field strings
$input_formID = form_input(array('name'=>'formid', 'id'=>'formid', 'value'=>$tokenArray['formID'], 'type'=>'hidden'));
$input_token = form_input(array('name'=>'token', 'id'=>'token', 'value'=>$tokenArray['token'], 'type'=>'hidden'));
// Visible form fields for testing. Should not be used in production
// $input_formID = form_input(array('name'=>'formid', 'id'=>'formid', 'value'=>$tokenArray['formID'], 'type'=>'input'));
// $input_token = form_input(array('name'=>'token', 'id'=>'token', 'value'=>$tokenArray['token'], 'type'=>'input'));
return "\n $input_formID \n $input_token\n";
}
MY_Form_validation.php library extension source
/**
* MY Form Validation Class
*
* Extends the CI Form Validation Class
*/
class MY_Form_validation extends CI_Form_validation {
function MY_Form_validation()
{
parent::CI_Form_validation();
log_message('debug', 'Class MY_Form_validation initialized');
}
/**
* Override the CI run() function
*
* Mostly copied/pasted from orig but adds CSRF token checks.
*
* @access public
* @return bool
*/
function run($group = '')
{
log_message('debug', 'My_Form_validation::run() called');
// Do we even have any data to process? Mm?
if (count($_POST) == 0)
{
return FALSE;
}
// Validate the token
$this->_validate_token();
// Does the _field_data array containing the validation rules exist?
// If not, we look to see if they were assigned via a config file
if (count($this->_field_data) == 0)
{
// No validation rules? We're done...
if (count($this->_config_rules) == 0)
{
return FALSE;
}
// Is there a validation rule for the particular URI being accessed?
$uri = ($group == '') ? trim($this->CI->uri->ruri_string(), '/') : $group;
if ($uri != '' AND isset($this->_config_rules[$uri]))
{
$this->set_rules($this->_config_rules[$uri]);
}
else
{
$this->set_rules($this->_config_rules);
}
// We're we able to set the rules correctly?
if (count($this->_field_data) == 0)
{
log_message('debug', "Unable to find validation rules");
return FALSE;
}
}
// Load the language file containing error messages
$this->CI->lang->load('form_validation');
// Cycle through the rules for each field, match the
// corresponding $_POST item and test for errors
foreach ($this->_field_data as $field => $row)
{
// Fetch the data from the corresponding $_POST array and cache it in the _field_data array.
// Depending on whether the field name is an array or a string will determine where we get it from.
if ($row['is_array'] == TRUE)
{
$this->_field_data[$field]['postdata'] = $this->_reduce_array($_POST, $row['keys']);
}
else
{
if (isset($_POST[$field]) AND $_POST[$field] != "")
{
$this->_field_data[$field]['postdata'] = $_POST[$field];
}
}
$this->_execute($row, explode('|', $row['rules']), $this->_field_data[$field]['postdata']);
}
// Did we end up with any errors?
$total_errors = count($this->_error_array);
if ($total_errors > 0)
{
$this->_safe_form_data = TRUE;
}
// Now we need to re-set the POST data with the new, processed data
$this->_reset_post_array();
// No errors, validation passes!
if ($total_errors == 0)
{
$this->CI->csrf->clear_token();
return TRUE;
}
// Validation fails
return FALSE;
}
/**
* Validates the token sent from POST
*
* @access private
* @return void
*/
function _validate_token() {
log_message('debug', 'My_Form_validation::_validate_token() called');
// Form ID and token from the POST input
$in_formID = $this->CI->input->post('formid');
$in_token = $this->CI->input->post('token');
// Validate token from POST
if(!$this->CI->csrf->validate_token($in_formID, $in_token)) {
log_message('debug', 'My_Form_validation::_validate_token() bad token');
// Create a new token and set the error
$this->CI->csrf->create_token();
$this->_error_array[] = $this->CI->lang->line('my_form_validation_bad_token');
}
// Token is fine. Save it for reuse in case other validation tests fail
else {
$this->CI->csrf->save_token($in_formID, $in_token);
}
}
}
- CI /
- Code /
- CodeIgniter /
- csrf /
- development /
- Linux FLOSS /
- php /
- security /


