How can you help protect your web forms from CSRF?
Your web form can be vulnerable to a number of different attack vectors, such as XSS, SQL Injection, Information Leakage and CSRF. In the last case, CSRF, if you form is vulnerable to the other vectors mentioned, there is a good chance it may be vulnerable to CSRF.
First things first:
- Make sure you have no XSS. Validate all input, be it from the user, the user's browser (User Agent or Headers), or from the database.
- Use prepared statements of parameterized SQL to mitigate the risks of SQL injection.
- Use proven libraries and adequate exception handling to prevent an error from leaking potentially sensitive information to an attacker.
- Make sure your form page has been validated as Valid HTML or XHTML.
- Use SSL/TSL for form communications.
- Use a cryptographically sound token, tied to the user's Session ID to help prevent an attacker from submitting a form on behalf of another user. If the Session ID of the user submitting the form does not match, the form submission will fail.
In this case we will base the token on the Session ID, the user's remote IP address, a random GUID, and the current time. These values will be hashed using SHA1 and a random salt. The resultant token (hash) will be written to the form as a hidden form field as well as to the user's browser as a cookie. Assuming the form is served over SSL/TSL it would be nearly impossible for an attacker to:
- Obtain or guess the user's Session ID (assuming your server side code produces sufficiently random Session IDs,)
- Guess the random GUID, and
- Guess the random Salt value.
In addition, there is a five minute time limit so any attack against the token, in an attempt to get the Session ID is going to take too long to be successful. Therefore, it would not be possible for an attacker to gather the necessary information to submit a form on behalf of another person which is the whole purpose of CSRF.
In this example page the token is revealed with a submit button. If the token is tampered with the "Check Token" result will be false, but you won't see that because your session will be destroyed and you will be redirected to a forbidden page. If the cookie is tampered with the check token function will similarly fail. Finally, if you wait longer than 5 minutes to submit the form the check token function will fail.
If you have a multi-page form, or will allow the user to go back to a previous form (using the back button, for example) the token won't be valid and the user would be redirected wrongly to the forbidden page. In those cases you must maintain the same token from page to page until the entire transaction is complete. That may require you lengthen the time allowed or remove the time check from the checkToken function.
In this example a new CSRF token is created on each form submission. For a more detailed explanation of CSFR please refer to the OWASP projects CSRF page. If you are protecting a high value site (banking, financial services, etc.) you should probably use the OWASP's CSRF Guard implementation and you may also choose to require the user to re-authenticate before completing a transaction.
<?PHP
session_start();
function checkToken() {
/*****************************************************************************************
* Check the posted token for correctness
* ### CHANGE PATH TO THE 403 FORBIDDEN PAGE ###
******************************************************************************************/
$oldToken="";
$testToken="";
$tokenStr="";
$page=$_SERVER["SCRIPT_NAME"];
/********************************************************
* NO NEED FOR FILTERING INPUT AS IT WILL NEVER BE OUTPUT
*********************************************************/
$oldToken=$_POST["token"];
$tokenStr = "IP:" . $_SESSION["ip"] . ",SESSIONID:" . session_id() . ",GUID:" . $_SESSION["guid"];
$testToken=sha1(($tokenStr&$_SESSION["salt"]).$_SESSION["salt"]);
$checkToken=False;
If ($oldToken===$testToken) {
$diff = time() - $_SESSION["time"];
If ($diff<=300) { // Five minutes max
If ($_SESSION["usecookie"]) {
If ($_COOKIE["token"]===$oldToken) {
setcookie("token", '', time()-42000);
return true;
}else{
$_SESSION = array();
if (isset($_COOKIE[session_name()])) {
setcookie(session_name(), '', time()-42000, '/');
}
session_destroy();
header("Location: http://www.yourdomain.com/forbidden.php?p=" . $page . "&t=ec");
}
}else{
return True;
}
}else{
$_SESSION = array();
if (isset($_COOKIE[session_name()])) {
setcookie(session_name(), '', time()-42000, '/');
}
session_destroy();
header("Location: http://www.yourdomain.com/forbidden.php?p=" . $page . "&t=et");
}
}else{
$_SESSION = array();
if (isset($_COOKIE[session_name()])) {
setcookie(session_name(), '', time()-42000, '/');
}
session_destroy();
header("Location: http://www.yourdomain.com/forbidden.php?p=" . $page . "&t=e");
}
}
function writeToken() {
/*****************************************************************************************
* Create and set a new token for CSRF protection
* on initial entry or after form errors and we are going to redisplay the form.
* CHANGE THE SALT!
******************************************************************************************/
$salt="";
$tokenStr="";
$salt = sha1("your_random_salt");
setcookie("token", "", time()-42000);
$_SESSION["salt"]=$salt;
$_SESSION["guid"] = com_create_guid();
$_SESSION["ip"] = $_SERVER["REMOTE_ADDR"];
$_SESSION["time"] = time();
$tokenStr = "IP:" . $_SESSION["ip"] . ",SESSIONID:" . session_id() . ",GUID:" . $_SESSION["guid"];
$_SESSION["token"]=sha1(($tokenStr&$_SESSION["salt"]).$_SESSION["salt"]);
if (setcookie("token", $_SESSION["token"], time()+500)) {
$_SESSION["usecookie"]=True;
}
echo '<input id="token" name="token" type="hidden" accesskey="u" tabindex="999" value="' .$_SESSION['token']. '">';
}
?>
<?php
session_start();
include 'form_token.php';
if ($_SERVER["REQUEST_METHOD"]=="POST") {
if ( checkToken() ) {
// PROCESS THE FORM DATA - NO NEED FOR ELSE: checkToken() will send them to forbidden.php
}
}
?>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta http-equiv="expires" content="now-1">
<meta http-equiv="pragma" content="no-cache">
<meta name="language" content="en-US">
<meta name="author" content="Roderick Divilbiss">
<meta name="copyright" content="© 2005-2010 Roderick Divilbiss">
<title>Form Token Test</title>
</head>
<body>
<h1>Anti-CSRF Token</h1>
<form name="frm" method="post" action="test_form_token.php">
<p><?PHP writeToken(); ?>
<input type="submit" value="Submit"></p>
</form>
</body>
</html><?PHP
header("HTTP/1.1 403 Forbidden");
?>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta http-equiv="expires" content="now-1">
<meta http-equiv="pragma" content="no-cache">
<meta name="language" content="en-US">
<meta name="author" content="Roderick Divilbiss">
<meta name="copyright" content="© 2005-2010 Roderick Divilbiss">
<title>Forbidden</title>
</head>
<body>
<h1>Forbidden</h1>
<p>You do not have permission to access this resource or you have a form submission error.</p>
<p>If you believe you have received this page in error, please contact the <a title="E-Mail the webmaster" href="mailto:webmaster@yourdomain.com">webmaster</a>.</p>
</body>
</html>