This site is the archived OWASP Foundation Wiki and is no longer accepting Account Requests.
To view the new OWASP Foundation website, please visit https://owasp.org

Difference between revisions of "PHP CSRF Guard"

From OWASP
Jump to: navigation, search
(PHP CSRFGuard code)
(Modern PHP: Don't use mt_rand(), use random_bytes() (also, hash_equals()))
 
(18 intermediate revisions by 8 users not shown)
Line 1: Line 1:
=PHP CSRF Guard=
+
{{taggedDocument}}
 +
=Code Snippet=
 +
 
 +
'''CAUTION:''' This code snippet below is segmented intentionally. You are not supposed to copy paste this code, unless you understand its risks and how it operates. It is not verified by OWASP professional, just composed by them. Up until now two small flaws have been reported and fixed in this code. Also it uses regular expressions which are not even close to a good solution for parsing HTML ([http://stackoverflow.com/questions/1732348/regex-match-open-tags-except-xhtml-self-contained-tags see why]), it is just there to give you an insight on what should be done. See the discussion page for more details.
  
 
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:
Line 8: Line 11:
 
  {
 
  {
 
  if (isset($_SESSION))
 
  if (isset($_SESSION))
 +
{
 
  $_SESSION[$key]=$value;
 
  $_SESSION[$key]=$value;
 +
}
 
  }
 
  }
 
  function unset_session($key)
 
  function unset_session($key)
 
  {
 
  {
 +
$_SESSION[$key]=' ';
 
  unset($_SESSION[$key]);
 
  unset($_SESSION[$key]);
 
  }
 
  }
 
  function get_from_session($key)
 
  function get_from_session($key)
 
  {
 
  {
  if (isset($_SESSION))
+
  if (isset($_SESSION[$key]))
 +
{
 
  return $_SESSION[$key];
 
  return $_SESSION[$key];
  else return false; //no session data, no CSRF risk
+
}
 +
  else return false; }
 
  }
 
  }
  
 
  function csrfguard_generate_token($unique_form_name)
 
  function csrfguard_generate_token($unique_form_name)
 
  {
 
  {
  if (false && function_exists("hash_algos") and in_array("sha512",hash_algos()))
+
  $token = random_bytes(64); // PHP 7, or via paragonie/random_compat
$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);
 
  store_in_session($unique_form_name,$token);
 
  return $token;
 
  return $token;
Line 43: Line 37:
 
  function csrfguard_validate_token($unique_form_name,$token_value)
 
  function csrfguard_validate_token($unique_form_name,$token_value)
 
  {
 
  {
  $token=get_from_session($unique_form_name);
+
  $token = get_from_session($unique_form_name);
  if ($token===false)
+
  if (!is_string($token_value)) {
return true;
+
return false;
  elseif ($token==$token_value)
+
  }
$result=true;
+
$result = hash_equals($token, $token_value);
else
 
$result=false;
 
 
  unset_session($unique_form_name);
 
  unset_session($unique_form_name);
 
  return $result;
 
  return $result;
Line 58: Line 50:
 
  $count=preg_match_all("/<form(.*?)>(.*?)<\\/form>/is",$form_data_html,$matches,PREG_SET_ORDER);
 
  $count=preg_match_all("/<form(.*?)>(.*?)<\\/form>/is",$form_data_html,$matches,PREG_SET_ORDER);
 
  if (is_array($matches))
 
  if (is_array($matches))
foreach ($matches as $m)
 
 
  {
 
  {
  if (strpos($m[1],"nocsrf")!==false) continue;
+
  foreach ($matches as $m)
$name="CSRFGuard_".mt_rand(0,mt_getrandmax());
+
{
$token=csrfguard_generate_token($name);
+
if (strpos($m[1],"nocsrf")!==false) { continue; }
$form_data_html=str_replace($m[0],
+
$name="CSRFGuard_".mt_rand(0,mt_getrandmax());
 +
$token=csrfguard_generate_token($name);
 +
$form_data_html=str_replace($m[0],
 
  "<form{$m[1]}>
 
  "<form{$m[1]}>
 
  <input type='hidden' name='CSRFName' value='{$name}' />
 
  <input type='hidden' name='CSRFName' value='{$name}' />
 
  <input type='hidden' name='CSRFToken' value='{$token}' />{$m[2]}</form>",$form_data_html);
 
  <input type='hidden' name='CSRFToken' value='{$token}' />{$m[2]}</form>",$form_data_html);
 +
}
 
  }
 
  }
 
  return $form_data_html;
 
  return $form_data_html;
Line 81: Line 75:
 
  if (count($_POST))
 
  if (count($_POST))
 
  {
 
  {
  if (!isset($_POST['CSRFName']))
+
  if ( !isset($_POST['CSRFName']) or !isset($_POST['CSRFToken']) )
 +
{
 
  trigger_error("No CSRFName found, probable invalid request.",E_USER_ERROR);
 
  trigger_error("No CSRFName found, probable invalid request.",E_USER_ERROR);
  $name=$_POST['CSRFName'];
+
}
 +
  $name =$_POST['CSRFName'];
 
  $token=$_POST['CSRFToken'];
 
  $token=$_POST['CSRFToken'];
 
  if (!csrfguard_validate_token($name, $token))
 
  if (!csrfguard_validate_token($name, $token))
  trigger_error("Invalid CSRF token.",E_USER_ERROR);
+
{
 +
  throw new Exception("Invalid CSRF token.");
 +
}
 
  }
 
  }
 
  ob_start();
 
  ob_start();
  register_shutdown_function(csrfguard_inject);
+
/* adding double quotes for "csrfguard_inject" to prevent:
 +
          Notice: Use of undefined constant csrfguard_inject - assumed 'csrfguard_inject' */
 +
  register_shutdown_function("csrfguard_inject");
 
  }
 
  }
 
  csrfguard_start();
 
  csrfguard_start();
 
  
 
=Description and Usage=
 
=Description and Usage=
Line 107: Line 106:
 
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 '''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.
+
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, as required by [http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1 RFC 2616].
  
 
To test this code, append the following HTML to it:
 
To test this code, append the following HTML to it:
Line 120: Line 119:
  
 
=Author and License=
 
=Author and License=
This piece of code is by Abbas Naderi Afooshteh [mailto:[email protected]] from OWASP under Creative Commons 3.0 License.
+
This piece of code is by [mailto:[email protected] Abbas Naderi Afooshteh] from OWASP under Creative Commons 3.0 License.
 +
 
 +
Contributions from Krzysztof Kotowicz <krzysztof.kotowicz at securing.pl>, Jakub Kałużny <jakub.artur.kaluzny at g>, Nick Le Mouton <nick at noodles.net dot nz>

Latest revision as of 12:53, 19 May 2016

This page has been tagged and needs review. Please help OWASP and document the reason for this, see FixME.

Code Snippet

CAUTION: This code snippet below is segmented intentionally. You are not supposed to copy paste this code, unless you understand its risks and how it operates. It is not verified by OWASP professional, just composed by them. Up until now two small flaws have been reported and fixed in this code. Also it uses regular expressions which are not even close to a good solution for parsing HTML (see why), it is just there to give you an insight on what should be done. See the discussion page for more details.

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)
{
	$_SESSION[$key]=' ';
	unset($_SESSION[$key]);
}
function get_from_session($key)
{
	if (isset($_SESSION[$key]))
	{
		return $_SESSION[$key];
	}
	else {  return false; }
}
function csrfguard_generate_token($unique_form_name)
{
	$token = random_bytes(64); // PHP 7, or via paragonie/random_compat
	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 (!is_string($token_value)) {

return false;

	}
	$result = hash_equals($token, $token_value);
	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']) or !isset($_POST['CSRFToken']) )
		{
			trigger_error("No CSRFName found, probable invalid request.",E_USER_ERROR);		
		} 
		$name =$_POST['CSRFName'];
		$token=$_POST['CSRFToken'];
		if (!csrfguard_validate_token($name, $token))
		{ 
			throw new Exception("Invalid CSRF token.");
		}
	}
	ob_start();
	/* adding double quotes for "csrfguard_inject" to prevent: 
          Notice: Use of undefined constant csrfguard_inject - assumed 'csrfguard_inject' */
	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, as required by RFC 2616.

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 from OWASP under Creative Commons 3.0 License.

Contributions from Krzysztof Kotowicz <krzysztof.kotowicz at securing.pl>, Jakub Kałużny <jakub.artur.kaluzny at g>, Nick Le Mouton <nick at noodles.net dot nz>