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.
<?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
<?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-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.
<?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.
<!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.