My CodeIgniter CSRF + Form Helpers

It’s been 5 years since I last code in PHP. Recently, I started to re-visit and things have really changed. There are so many frameworks out there. CI (CodeIgniter) is one of the framework that caught my attention due to it’s lightweight payload.

Being lightweight will mean that a lot of stuff are missing, significantly in the security area. The CSRF protection that I was looking out for was no where to be found. In the end, I decided to come up with my own helpers that will aim to tackle the CSRF exploit that exist commonly in web applications.

  • CSRF Helper
<?php if (!defined('BASEPATH')) exit('No direct script access allowed');
/**
 * start the session just in case it has not already been started.
 * suppress any warning message if it has already been started.
 */
@session_start();  

if (! function_exists('csrf_is_token_valid'))
{
	/**
	 * @method bool csrf_is_token_valid() checks if a csrf token is valid
	 * @return bool true if token is valid. false if token is invalid.
	 */
	function csrf_is_token_valid()
	{
		$result = false;
		if (isset($_POST[csrf_varname()]))
		{
			$result = ((strcmp(csrf_value(), $_POST[csrf_varname()])) == 0);
		}
		return $result;
	}
}  

if (! function_exists('csrf_token'))
{
	/**
	 * @method void csrf_token($varlen, $str_to_shuffer) construct a random input field name and assign the token to it.
	 * @param int $varlen the length of the input field name that will be generated
	 * @param string $str_to_shuffer the string that will be used to generate the input field name
	 */
	function csrf_token($varlen = 3, $str_to_shuffer = "abcdefghijklmnopqrstuvwxyz0123456789_")
	{
		$start_pos = mt_rand(0, (strlen($str_to_shuffer) - $varlen));
		$_SESSION["CSRF_NONCE_VARNAME_{$_SERVER["REQUEST_URI"]}"] = substr(str_shuffle($str_to_shuffer), $start_pos, $varlen);
		$_SESSION["CSRF_NONCE_VALUE_{$_SERVER["REQUEST_URI"]}"]	= dohash(microtime() . mt_rand());
	}
}  

if (! function_exists('csrf_varname'))
{
	/**
	 * @method string csrf_varname($varlen, $str_to_shuffer) return the generated input field name
	 * @param int $varlen the length of the input field name that will be generated
	 * @param string $str_to_shuffer the string that will be used to generate the input field name
	 * @return string the generated input field name
	 */
	function csrf_varname($varlen = 3, $str_to_shuffer = "abcdefghijklmnopqrstuvwxyz0123456789_")
	{
		if (!isset($_SESSION["CSRF_NONCE_VARNAME_{$_SERVER["REQUEST_URI"]}"]))
		{
			csrf_token($varlen, $str_to_shuffer);
		}
		return $_SESSION["CSRF_NONCE_VARNAME_{$_SERVER["REQUEST_URI"]}"];
	}
}  

if (! function_exists('csrf_value'))
{
	/**
	 * @method string csrf_value($varlen, $str_to_shuffer) return the token
	 * @param int $varlen the length of the input field name that will be generated
	 * @param string $str_to_shuffer the string that will be used to generate the input field name
	 * @return string the token
	 */
	function csrf_value($varlen = 3, $str_to_shuffer = "abcdefghijklmnopqrstuvwxyz0123456789_")
	{
		if (!isset($_SESSION["CSRF_NONCE_VALUE_{$_SERVER["REQUEST_URI"]}"]))
		{
			csrf_token($varlen, $str_to_shuffer);
		}
		return $_SESSION["CSRF_NONCE_VALUE_{$_SERVER["REQUEST_URI"]}"];
	}
}  

if (! function_exists('csrf_clean'))
{
	/*
	 * @method void csrf_clean() clears the session variables that store the csrf token
	 */
	function csrf_clean()
	{
		session_unregister("CSRF_NONCE_VARNAME_{$_SERVER["REQUEST_URI"]}");
		session_unregister("CSRF_NONCE_VALUE_{$_SERVER["REQUEST_URI"]}");
	}
}  

if (isset($_POST) && count($_POST) > 0 && isset($_SESSION["CSRF_NONCE_VARNAME_{$_SERVER["REQUEST_URI"]}"]))
{
	if (!csrf_is_token_valid()) die("A form re-post or an unknown error has occurred.");
}
?>

Save the above code into system/application/helpers/csrf_helper.php

  • Extended FORM Helper
<?php if (!defined('BASEPATH')) exit('No direct script access allowed');
if (! function_exists('form_csrf'))
{
	/*
	 * @method string form_csrf($varlen, $str_to_shuffer) returns a constructed hidden input field of the csrf token
	 * @param int $varlen the length of the input field name that will be generated
	 * @param string $str_to_shuffer the string that will be used to generate the input field name
	 * @return string the hidden input field
	 */
	function form_csrf($varlen = 3, $str_to_shuffer = "abcdefghijklmnopqrstuvwxyz0123456789_")
	{
		csrf_token($varlen, $str_to_shuffer);
		return form_hidden(csrf_varname(), csrf_value());
	}
}
?>

Save the above code into system/application/helpers/MY_form_helper.php. This is the extended helper of CI’s FORM Helper.

  • Auto-Loading Helpers
/*
| -------------------------------------------------------------------
|  Auto-load Helper Files
| -------------------------------------------------------------------
| Prototype:
|
|	$autoload['helper'] = array('url', 'file');
*/

$autoload['helper'] = array('csrf', 'form', 'security');

Edit system/application/config/autoload.php. Look for the above code segment, add ‘csrf_helper’ and ‘form’ into the $autoload['helper'] array.

  • Testcsrf Controller
<?php class Testcsrf extends Controller {

	function Testcsrf()
	{
		parent::Controller();
	}

	function index()
	{
		$this->load->view('view_test_csrf');
	}
}
?>

Save the above code into system/application/controllers/testcsrf.php.

  • Testcsrf View
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html dir="ltr" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Test CSRF</title>
</head>

<body>
<form method="post">
<?=form_csrf(); ?>
<ul>
	<li>Your name:<ul>
		<li>
		<input name="f" type="text" value="" /></li>
	</ul>
	</li>
	<li>Email:<br />
	(this is also your login id)<ul>
		<li><input name="e" type="text" value="" /></li>
	</ul>
	</li>
	<li>Password:<ul>
		<li><input name="p" type="password" /></li>
	</ul>
	</li>
	<li>Confirm Password:<ul>
		<li><input name="c" type="password" /></li>
		<li><input name="s" type="submit" value="Submit" />
			<input name="s" type="submit" value="Cancel" /></li>
	</ul>
	</li>
</ul></form>
</body>
</html>

Save the above code into system/application/views/view_test_csrf.php. You probably have notice this line

<?=form_csrf(); ?>

in the above code. Yes. To protect against CSRF exploit, you just need to include that into your form. It’s just that simple.

A view source of the generated HTML from the Testcsrf controller will reveal a hidden input field:

<input type="hidden" name="90r" value="05d15a9f077ffb5e469193f8f33431665ac79c7b" />

The field name will always change when the page is reloaded.

The default length of the field name is 3

The default character set that is used to generate the field name is abcdefghijklmnopqrstuvwxyz0123456789_

You can however choose to change the default values by passing in the $varlen and $str_to_shuffle param:

<?=form_csrf(10, "ABCDEFGHIJKLMNOPQRSTUVWXYZ"); ?>

What the above does is that it will generate a hidden field with a field name of 10 characters from the supplied character set.

or simply just supply the $varlen param:

<?=form_csrf(10); ?>

which will use the default character set and generate a hidden field with a field name of 10 characters long.

These helpers are by no means to be perfect as there are several scenarios that it does not take care. One example will be the slim chance of generating a random field name that collide with a legit field.

However, you could always specify a big number for $varlen which will result in a longer field name. This should greatly reduce the chance of collision of field names with the CSRF helper.

Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • Mixx
  • Google
  • StumbleUpon

Filed under: DevelopmentPHPSecurity


5 Responses to “My CodeIgniter CSRF + Form Helpers”

john Says:

nice snippets for CI. thanks a bunch!


james Says:

CI is cool. the latest version 1.7.1 is even cooler.


sammy Says:

nice piece of work there. which version of codeigniter does it support? does it support 1.7.1?


小眀 Says:

很不錯的方程式。情繼續加油!!!


tom Says:

your codes looked cool. how do i get to run it in 1.7.1? i couldn’t get it to work. does it support 1.7.1?


Leave a Reply