Difference between revisions of "PHP CSRF Guard"

From OWASP
Jump to: navigation, search
(link to download)
(PHP CSRFGuard code)
Line 1: Line 1:
==PHP CSRF Guard==
+
=PHP CSRF Guard=
  
If you need to protect against CSRF attacks in your code, this little helper can reduce the risk.
+
If you need to protect against CSRF attacks in your code, this little helper can reduce the risk:
  
===Architectural Issues===
+
session_start(); //if you are copying this code, this line makes it work.
  
The main issue with CSRF is the lack of "authentication" of a privileged function. Therefore, in combination with this helper, you should:
+
function store_in_session($key,$value)
 +
{
 +
if (isset($_SESSION))
 +
$_SESSION[$key]=$value;
 +
}
 +
function unset_session($key)
 +
{
 +
unset($_SESSION[$key]);
 +
}
 +
function get_from_session($key)
 +
{
 +
if (isset($_SESSION))
 +
return $_SESSION[$key];
 +
else return false; //no session data, no CSRF risk
 +
}
  
* Low value: Consider asking the user (on a new page / form) to re-authenticate using their password to confirm the action
+
function csrfguard_generate_token($unique_form_name)
* Medium value: Consider the use of second-factor authentication, such as sending the user an e-mail or SMS message with a random token which they must enter
+
{
* High value: disconnected calculator transaction signing the transaction
+
if (false && function_exists("hash_algos") and in_array("sha512",hash_algos()))
 +
$token=hash("sha512",mt_rand(0,mt_getrandmax()));
 +
else
 +
{
 +
$token='';
 +
for ($i=0;$i<128;++$i)
 +
{
 +
$r=mt_rand(0,35);
 +
if ($r<26)
 +
$c=chr(ord('a')+$r);
 +
else
 +
$c=chr(ord('0')+$r-26);
 +
$token.=$c;
 +
}
 +
}
 +
store_in_session($unique_form_name,$token);
 +
return $token;
 +
}
 +
function csrfguard_validate_token($unique_form_name,$token_value)
 +
{
 +
$token=get_from_session($unique_form_name);
 +
if ($token===false)
 +
return true;
 +
elseif ($token==$token_value)
 +
$result=true;
 +
else
 +
$result=false;
 +
unset_session($unique_form_name);
 +
return $result;
 +
}
  
===Using this code===
+
function csrfguard_replace_forms($form_data_html)
 +
{
 +
$count=preg_match_all("/<form(.*?)>(.*?)<\\/form>/is",$form_data_html,$matches,PREG_SET_ORDER);
 +
if (is_array($matches))
 +
foreach ($matches as $m)
 +
{
 +
if (strpos($m[1],"nocsrf")!==false) continue;
 +
$name="CSRFGuard_".mt_rand(0,mt_getrandmax());
 +
$token=csrfguard_generate_token($name);
 +
$form_data_html=str_replace($m[0],
 +
"<form{$m[1]}>
 +
<input type='hidden' name='CSRFName' value='{$name}' />
 +
<input type='hidden' name='CSRFToken' value='{$token}' />{$m[2]}</form>",$form_data_html);
 +
}
 +
return $form_data_html;
 +
}
  
* Download the code (see [https://www.owasp.org/index.php/Category:OWASP_CSRFGuard_Project OWASP CSRFGuard Project page] for github details)
+
function csrfguard_inject()
* Include it within your project
+
* Use the object to create a hidden field Inside your forms, like this:
+
 
+
?&gt;
+
&lt;form ... &gt;
+
  &lt;input ...&gt;
+
  &lt;?php echo $cg; ?&gt;
+
&lt;/form&gt;
+
&lt;/code&gt;
+
 
+
When you take the form submission, assert the form's validity:
+
 
+
try
+
 
  {
 
  {
    $cg->isValid();
+
$data=ob_get_clean();
    // your code here
+
$data=csrfguard_replace_forms($data);
 +
echo $data;
 
  }
 
  }
  catch (TokenException $e)
+
  function csrfguard_start()
 
  {
 
  {
  // handle exception
+
if (count($_POST))
 +
{
 +
if (!isset($_POST['CSRFName']))
 +
trigger_error("No CSRFName found, probable invalid request.",E_USER_ERROR);
 +
$name=$_POST['CSRFName'];
 +
$token=$_POST['CSRFToken'];
 +
if (!csrfguard_validate_token($name, $token))
 +
trigger_error("Invalid CSRF token.",E_USER_ERROR);
 +
}
 +
ob_start();
 +
register_shutdown_function(csrfguard_inject);
 
  }
 
  }
  catch (// your exceptions here)
+
  csrfguard_start();
 +
 
 +
 
 +
=Description and Usage=
 +
The first three functions, are an abstraction over how session variables are stored. Replace them if you don't use native PHP sessions.
 +
 
 +
The '''generate''' function, creates a random secure one-time CSRF token. If SHA512 is available, it is used, otherwise a 512 bit random string in the same format is generated. This function also stores the generated token under a unique name in session variable.
  
===Caveats on use===
+
The '''validate''' function, checks under the unique name for the token. There are three states:
 +
* Sessions not active: validate succeeds (no CSRF risk)
 +
* Token found but not the same, or token not found: validation fails
 +
* Token found and the same: validation succeeds
 +
Either case, this function removes the token from sessions, ensuring one-timeness.
 +
 +
The '''replace''' function, receives a portion of html data, finds all <form> occurrences and adds two hidden fields to them: CSRFName and CSRFToken. If any of these forms has an attribute or value '''''nocsrf'''''', the addition won't be performed (note that using default inject and detect breaks with this).
  
This code is object-orientated. It relies upon PHP 5 only features, and throws a custom exception type. You should familiarize yourself with this coding style if you are still practicing function based PHP coding. It's not hard - see above.  
+
The other two functions, '''inject''' and '''start''' are a demonstration of how to use the other functions. Using output buffering on your entire output is not recommended (some libraries might dump output buffering). This default behavior, enforces CSRF tokens on all forms using POST method. It is assumed that no sensitive operations with GET method are performed in the application.
  
The code is PHP 5 only as this is the safest and most secure version of PHP available today. Your apps should be using PHP 5.2 or later to ensure that your application is running on a secure foundation.
+
To test this code, append the following HTML to it:
  
===LIcense===
+
<form method='post'>
 +
<input type='text' name='test' value='<?php echo "testing"?>' />
 +
<input type='submit' />
 +
</form>
 +
 +
<form class='nocsrf'>
 +
</form>
  
To allow widespread adoption, this code is licensed under the Creative Commons Share Alike 2.5 License. This allows you to use this in commercial and non-commercial applications alike. This license is GPL friendly.
+
=Author and License=
 +
This piece of code is by Abbas Naderi Afooshteh [mailto:abbas.naderi@owasp.org] from OWASP under Creative Commons 3.0 License.

Revision as of 20:52, 9 July 2012

PHP CSRF Guard

If you need to protect against CSRF attacks in your code, this little helper can reduce the risk:

session_start(); //if you are copying this code, this line makes it work.
function store_in_session($key,$value)
{
	if (isset($_SESSION))
		$_SESSION[$key]=$value;
}
function unset_session($key)
{
	unset($_SESSION[$key]);
}
function get_from_session($key)
{
	if (isset($_SESSION))
		return $_SESSION[$key];
	else return false; //no session data, no CSRF risk
}
function csrfguard_generate_token($unique_form_name)
{
	if (false && function_exists("hash_algos") and in_array("sha512",hash_algos()))
		$token=hash("sha512",mt_rand(0,mt_getrandmax()));
	else
	{
		$token=;
		for ($i=0;$i<128;++$i)
		{
			$r=mt_rand(0,35);
			if ($r<26)
				$c=chr(ord('a')+$r);
			else 
				$c=chr(ord('0')+$r-26);
			$token.=$c;
		}
	}
	store_in_session($unique_form_name,$token);
	return $token;
}
function csrfguard_validate_token($unique_form_name,$token_value)
{
	$token=get_from_session($unique_form_name);
	if ($token===false)
		return true;
	elseif ($token==$token_value)
		$result=true;
	else
		$result=false;
	unset_session($unique_form_name);
	return $result;
}
function csrfguard_replace_forms($form_data_html)
{
	$count=preg_match_all("/<form(.*?)>(.*?)<\\/form>/is",$form_data_html,$matches,PREG_SET_ORDER);
	if (is_array($matches))
	foreach ($matches as $m)
	{
		if (strpos($m[1],"nocsrf")!==false) continue;
		$name="CSRFGuard_".mt_rand(0,mt_getrandmax());
		$token=csrfguard_generate_token($name);
		$form_data_html=str_replace($m[0],
				"<form{$m[1]}>
<input type='hidden' name='CSRFName' value='{$name}' />
<input type='hidden' name='CSRFToken' value='{$token}' />{$m[2]}</form>",$form_data_html);
	}
	return $form_data_html;
}
function csrfguard_inject()
{
	$data=ob_get_clean();
	$data=csrfguard_replace_forms($data);
	echo $data;
}
function csrfguard_start()
{
	if (count($_POST))
	{
		if (!isset($_POST['CSRFName']))
			trigger_error("No CSRFName found, probable invalid request.",E_USER_ERROR);		
		$name=$_POST['CSRFName'];
		$token=$_POST['CSRFToken'];
		if (!csrfguard_validate_token($name, $token))
			trigger_error("Invalid CSRF token.",E_USER_ERROR);
	}
	ob_start();
	register_shutdown_function(csrfguard_inject);	
}
csrfguard_start();


Description and Usage

The first three functions, are an abstraction over how session variables are stored. Replace them if you don't use native PHP sessions.

The generate function, creates a random secure one-time CSRF token. If SHA512 is available, it is used, otherwise a 512 bit random string in the same format is generated. This function also stores the generated token under a unique name in session variable.

The validate function, checks under the unique name for the token. There are three states:

  • Sessions not active: validate succeeds (no CSRF risk)
  • Token found but not the same, or token not found: validation fails
  • Token found and the same: validation succeeds

Either case, this function removes the token from sessions, ensuring one-timeness.

The replace function, receives a portion of html data, finds all <form> occurrences and adds two hidden fields to them: CSRFName and CSRFToken. If any of these forms has an attribute or value nocsrf', the addition won't be performed (note that using default inject and detect breaks with this).

The other two functions, inject and start are a demonstration of how to use the other functions. Using output buffering on your entire output is not recommended (some libraries might dump output buffering). This default behavior, enforces CSRF tokens on all forms using POST method. It is assumed that no sensitive operations with GET method are performed in the application.

To test this code, append the following HTML to it:

<form method='post'>
<input type='text' name='test' value='<?php echo "testing"?>' />
<input type='submit' />
</form>

<form class='nocsrf'>
</form>

Author and License

This piece of code is by Abbas Naderi Afooshteh [1] from OWASP under Creative Commons 3.0 License.