<?php
/**
* Cross Site Request Forgery protection class.
*
* This class implements CSRF. The initiate method needs to be called on top of
* all pages where CSRF needs to be implemented. The script will save the token
* in a cookie. Cookies is to prevent an attacker to just spoof the session-
* identifier. Cookie-data is stored client side.
*
* On requerst pages, you can call the static method validate that returns true
* in case of valid CSRF tokens else false. You need to handle the false-return
* by your self in your system.
*
* Upon each request the token is invalidated.
*
* @author Andreas Krüger
* @copyright (c) 2012, Andreas Krüger
* @version 1.0
*/
class csrf
{
/**
* Sets the CSRF token.
*/
public static function initiate( $renew = false )
{
if (!isset($_COOKIE['__csrf_token']) || $renew === true)
{
$token = self::generate_token();
$_COOKIE['__csrf_token'] = $token;
@setcookie('__csrf_token', $token);
}
}
/**
* Returns a truely random token of 512 characters.
* Uses openssl_random_pseudo_bytes() if available, else SHA512 if
* available. If either is not available, then a
* random string is created using mt_rand and chr(ord()).
*
* @return string
*/
private static function generate_token()
{
if (function_exists('openssl_random_pseudo_bytes'))
{
$token = bin2hex(openssl_random_pseudo_bytes(256));
}
elseif (function_exists('hash_algos') and in_array('sha512', hash_algos()))
{
$token = hash("sha512", mt_rand(0, mt_getrandmax()));
}
else
{
$token = '';
for ($i = 0; $i < 512; ++$i)
{
$r = mt_rand(0, 35);
if ($r < 26)
$token .= chr(ord('a') + $r);
else
$token .= chr(ord('0') + $r - 26);
}
}
return $token;
}
/**
* Validates a CSRF request. Looks firstly for the CSRF in _POST, and if
* not found, then in _GET.
*
* Will renew CSRF token upon validate request.
*
* @return boolean
*/
public function validate()
{
// If the csrf token is not found in cookies, we dont know what to
// compare. Renew the token.
if (!isset($_COOKIE['__csrf_token']))
{
self::initiate(true); // Renew token
return false;
}
else
$csrf_token = $_COOKIE['__csrf_token'];
$token = '';
// Request token has to be set and be a string.
if (isset($_POST['__csrf_token']) && is_string($_POST['__csrf_token']))
$token = $_POST['__csrf_token'];
// Request token was not found in POST request, lets see if it's in a GET request.
if (empty($token) && isset($_GET['csrf_token']) && is_string($_GET['__csrf_token']))
$token = $_GET['__csrf_token'];
// Renew the token upon each request, making sure to invalidate the request everytime.
self::initiate(true);
// Compare tokens.
if ($csrf_token == $token)
return true;
// Tokens wasn't equal
return false;
}
/**
* Returns the current token.
*
* @return string
*/
public static function getToken()
{
// If no token is set, renew it to get a fresh one.s
if (!isset($_COOKIE['__csrf_token']))
self::initiate(true); // Renew token
// Return token.
return $_COOKIE['__csrf_token'];
}
}
// Initiate CSRF
csrf::initiate();
// Ajax return call:
if (isset($_POST['type']))
{
echo (csrf::validate() ? 'true' : 'false');
exit;
}
// Form request
$form_result = false;
if (isset($_POST) && count($_POST) > 0)
$form_result = csrf::validate();
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<script src="http://cdn.jquerytools.org/1.2.6/jquery.tools.min.js"></script>
<script type="text/javascript">
// Get cookie content
function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie != '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = jQuery.trim(cookies[i]);
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) == (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
// Do a ajax call, read csrf token from cookie.
function doAjax()
{
if (typeof jQuery != 'undefined')
{
$.post("csrf.php", { type: "ajax", __csrf_token: getCookie('__csrf_token') },
function(data)
{
$('#result').html('Ajax result: ' + data);
});
}
}
function doAjaxWithout()
{
if (typeof jQuery != 'undefined')
{
$.post("csrf.php", { type: "ajax" },
function(data)
{
$('#result').html('Ajax result: ' + data);
});
}
}
if (typeof jQuery != 'undefined')
{
// Append CSRF token upon form submit, getting token from cookie.
$(document).ready(function()
{
$('form').submit(function(e)
{
if (!$(this).hasClass('notoken'))
{
// Dont allow form to submit before we added the token
e.preventDefault();
// Add token to the form we're trying to submit
$(this).append($("<input>").attr("type", "hidden").attr("name", "__csrf_token").val(getCookie('__csrf_token')));
// Remove bind and submit form!
$(this).unbind('submit').submit();
}
});
});
}
</script>
</head>
<body>
<h3>Form example</h3>
<form action="" method="post">
<input type="submit" name="send" value="Click me to submit form" />
</form>
<form action="" method="post" class="notoken">
<input type="submit" name="send" value="Click me to submit form without token" />
</form>
<br>
<?php echo "Form result: " . ($form_result ? 'true' : 'false') . "<br>"; ?>
<h3>Ajax post call example</h3>
<span onclick="doAjax()" style="border: 1px solid #FF0000; padding: 3px;background:#cccccc; cursor: pointer;">Click me with token</span>
<span onclick="doAjaxWithout()" style="border: 1px solid #FF0000; padding: 3px;background:#cccccc; cursor: pointer;">Click me without token</span>
<br>
<br>
<span id="result">Ajax result: false</span>
</body>
</html>