CSRF Application Security

CSRF stands for Cross Site Request Forgery. CSRF attacks are attacks that can be used against users without them understanding what is happening. For example, you can send a user a link to a website that has them update something that they didn't know they were updating. This can also be used against a user to force them to transfer funds from their bank accounts.

How do we guard against CSRF attacks? Actually, it's pretty simple. We use the same random generator from the previous section and we store it in a PHP session. We call this a token. Then, whenever a user is executing a task on the site, that user must send that token back with each request. An attacker wouldn't know what that token is and therefore they wouldn't be able to send a link to an unsuspecting user to execute something. If the token doesn't exist or is wrong, then the action is not done and the process is abruptly halted with no damage done.

Let's update our employees application to use CSRF tokens.

First, we're going to introduce PHP Sessions. PHP Sessions are a way to store user data on the server between requests. PHP Sessions work by using a cookie set in the web browser. Each time a request is made, the cookie is sent back to the server by the browser, PHP uses the cookie to load the session, and then we retrieve our CSRF token from the session. Once we have our CSRF token loaded, we can compare it to any request that we're trying to execute.

It basically works like this:

This code should be put into session.php in your webroot directory.

<?php

// Set the cookie life time
$cookieLifetime = 604800; // seven days

// set our cookie life time settings
ini_set('session.gc_maxlifetime', $cookieLifetime);
session_set_cookie_params($cookieLifetime);

// start the session
session_start();

// check to see if we have a CSRF token set yet.
if ( isset($_SESSION["csrf"]) == false || $_SESSION["csrf"] == "" ) {
	// store our CSRF token into a session variable.
	$_SESSION["csrf"] = bin2hex( random_bytes(8) );
}

How does this code work? First, this code needs to execute on every page load. So, we put this into it's own separate file and we require it at the top of each controller.

Second, we set our cookie life time. Cookie life times are specified in seconds. You can extrapolate that 604,800 seconds is equal to 7 days. Basically, the cookie in the browser will last for seven days if it hasn't been used. If the user continues to use the site, the cookie life time will be reset to allow another seven days. After seven days of no usage, the cookie is deleted and a new cookie is assigned along with a new CSRF token when the user returns.

Once we have defined our cookie life time, we set a few cookie life time settings. After that, we use session_start(). This asks PHP to load any session variables into the super global $_SESSION.

$_SESSION can be used as a key / value store and operates exactly like $_GET or $_POST. You can use $_SESSION variables anywhere in your code and they can be used for other things besides a CSRF token. They come in really handy for creating log in systems. We'll cover that in a future section.

Finally, once we have our session loaded, we can check to see if the current user has already been assigned a generated CSRF token. If not, we generate a token, and we move on.

This code must be run before most other code can execute, so it's good to include this early in our application.

How does this look in our controller code?

This code should be put into index.php in your webroot directory. This code should be put at the very top of this file, right below the <php tag.

<?php

// require session to be loaded
require_once "session.php";

Now, if you open up your browser Dev Tools (press F12 to open) and you go to Application, and then Cookies, you'll see something that looks like the following:

dev tools application displaying php session cookie

The next step is to update our view to always send our CSRF token with any requests.

This code should be put into indexviews.php in your webroot directory.

<?php

namespace IndexViews;

class IndexViews {
	// $employees is a list of firstName and lastName
	static public function index($employees) {
		IndexViews::header();

		// create a form so that we can send checked employees to the server
		?> 
		<form action="index.php" method="post">
		<input type="hidden" name="do" value="deleteEmployees">
		<input type="hidden" name="csrf" value="<?= $_SESSION["csrf"] ?>">
		
		<input type="submit" value="Delete Selected Employees">
		
		<?php
		
		// echo the results to the browser
		foreach ($employees as $res) {
		?>
		<div>
			<input type="checkbox" name="obfuscatedIds[]" value="<?= $res["obfuscatedId"] ?>"> 
			<a href="index.php?do=update&obfuscatedId=<?= $res["obfuscatedId"] ?>&csrf=<?= $_SESSION["csrf"] ?>">
				<?= $res["lastName"] ?>, <?= $res["firstName"] ?>, <?= $res["number"] ?>, <?= $res["phoneType"] ?>
			</a>
		</div>
		<?php
		}
		
		// end the form
		?>
		</form> 
		<?php

		IndexViews::employeeForm();
		IndexViews::footer();
	}
	
	
	static public function updateEmployee($employeeRecord) {
		IndexViews::header();
		?><h3>Update Employee</h3><?php
		IndexViews::employeeForm(true, $employeeRecord);
		IndexViews::footer();
	}
	
	
	static public function header() {
		
?>
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
	<meta charset="utf-8">
	<title>Employees</title>
	<style>
		body{ background-color: #333; color: #ddd; }
		a{ color: #ddd; }
	</style>
</head>
<body>
<?php
	}
	
	static public function footer() {
?>
</body>
</html>
<?php
	}
	
	// flip $isUpdate to true if we are using this form to update a record
	// we set $employeeRecord defaults so we don't get a bunch of notices in our logs when doing saveNewEmployee
	static public function employeeForm($isUpdate = false, 
		$employeeRecord = ["obfuscatedId"=>"", "firstName"=>"", "lastName"=>"", "salary"=>"", "street"=>"", "city"=>"", "state"=>"", "zip"=>"", "number"=>"", "phoneType"=>"", ]) {

		?>
		<form action="index.php" method="post">
			
			<input type="hidden" name="csrf" value="<?= $_SESSION["csrf"] ?>">
			
			<?php 
			// use ternary operators to set up do and nameId to be passed back to the server
			echo $isUpdate == true ? '<input type="hidden" name="do" value="saveEmployeeUpdate">' : '<input type="hidden" name="do" value="saveNewEmployee">';
			echo $isUpdate == true ? '<input type="hidden" name="obfuscatedId" value="'.$employeeRecord["obfuscatedId"].'">' : '';
			?>
			
			<div class="">
				Name
			</div>
			<div class="">
				<input type="text" name="firstName" placeholder="first name" value="<?= $employeeRecord["firstName"] ?>">
				<input type="text" name="lastName" placeholder="last name" value="<?= $employeeRecord["lastName"] ?>">
				<input type="text" name="salary" placeholder="salary $" value="<?= $employeeRecord["salary"] ?>">
			</div>
			<div class="">
				Address
			</div>
			<div class="">
				<input type="text" name="street" placeholder="street" value="<?= $employeeRecord["street"] ?>">
				<input type="text" name="city" placeholder="city" value="<?= $employeeRecord["city"] ?>">
				<input type="text" name="state" placeholder="state" value="<?= $employeeRecord["state"] ?>">
				<input type="text" name="zip" placeholder="zip" value="<?= $employeeRecord["zip"] ?>">
			</div>
			<div class="">
				Phone
			</div>
			<div class="">
				<input type="text" name="number" placeholder="number" value="<?= $employeeRecord["number"] ?>">
				<select name="type" value="<?= $employeeRecord["phoneType"] ?>">
					<option>Cell</option>
					<option>Home</option>
					<option>Work</option>
				</select>
			</div>
			<input type="submit" value="Save">
		</form>
		
		<?php
	}
}

You should notice that we've added the CSRF token in three different places. The first place is within our function index($employees) method, we've added a second hidden field with a name that equals csrf and a value that equals <?= $_SESSION["csrf"] ?>. This fills in the CSRF token for the delete functionality. When we click the delete button, it sends the CSRF token to the server, along with the list of employees to delete.

The second place that we've included it is within the link for each employee. When you click on the employee to update their record, it sends the CSRF token to the server. This is defined like, <a href="index.php?do=update&obfuscatedId=<?= $res["obfuscatedId"] ?>&csrf=<?= $_SESSION["csrf"] ?>">. You'll notice the CSRF token being added to the end of the href with &csrf=<?= $_SESSION["csrf"] ?>.

The third place we've added it is to our function employeeForm($isUpdate = false, $employeeRecord) method. This is also another hidden field that is defined like, <input type="hidden" name="csrf" value="<?= $_SESSION["csrf"] ?>">. When the user submits the form, the CSRF token will be passed to the server to check.

Let's look at our controller now. In our controller, we're basically doing two CSRF checks.

This code should be put into index.php in your webroot directory.

<?php

// require session to be loaded
require_once "session.php";

require_once "filter.php";
use \filter\filter;

require_once "indexmodels.php";
use \IndexModels\IndexModels;

require_once "indexviews.php";
use \IndexViews\IndexViews;

// handle GETS
if ( isset($_GET["do"]) == true ) {
	
	// check csrf token to make sure it matches
	// exit if any of these conditions are not met
	if (isset($_GET["csrf"]) == false || $_GET["csrf"] != $_SESSION["csrf"]) {
		exit;
	}
	
	// use $_GET here, because clicking a link and sending HTTP vars is a GET request
	if ($_GET["do"] == "update") {
		$obfuscatedId = "";
		if (isset($_GET["obfuscatedId"]) == true && $_GET["obfuscatedId"] != "") {
			$obfuscatedId = $_GET["obfuscatedId"];
		}
		
		if ($obfuscatedId != "") {
			IndexViews::updateEmployee( IndexModels::getEmployee($obfuscatedId) );
		}
		exit;
	}
}

// handle POSTS
if ( isset($_POST["do"]) == true ) {

	// check csrf token to make sure it matches
	// exit if any of these conditions are not met
	if (isset($_POST["csrf"]) == false || $_POST["csrf"] != $_SESSION["csrf"]) {
		exit;
	}
	
	// utilize this validation with saveNewEmployee or saveEmployeeUpdate
	if ($_POST["do"] == "saveNewEmployee" || $_POST["do"] == "saveEmployeeUpdate") {
		
		$lastName = "";
		if (isset($_POST["lastName"]) == true && $_POST["lastName"] != "") {
			$lastName = filter::names($_POST["lastName"]);
		}
		
		$firstName = "";
		if (isset($_POST["firstName"]) == true && $_POST["firstName"] != "") {
			$firstName = filter::names($_POST["firstName"]);
		}
		
		$salary = "";
		if (isset($_POST["salary"]) == true && $_POST["salary"] != "") {
			$salary = filter::salary($_POST["salary"]);
		}
		
		$street = "";
		if (isset($_POST["street"]) == true && $_POST["street"] != "") {
			$street = filter::street($_POST["street"]);
		}
		
		$city = "";
		if (isset($_POST["city"]) == true && $_POST["city"] != "") {
			$city = filter::city($_POST["city"]);
		}
		
		$state = "";
		if (isset($_POST["state"]) == true && $_POST["state"] != "") {
			$state = filter::state($_POST["state"]);
		}
		
		$zip = "";
		if (isset($_POST["zip"]) == true && $_POST["zip"] != "") {
			$zip = filter::zip($_POST["zip"]);
		}
		
		$number = "";
		if (isset($_POST["number"]) == true && $_POST["number"] != "") {
			$number = filter::number($_POST["number"]);
		}
		
		$type = "";
		if (isset($_POST["type"]) == true && $_POST["type"] != "") {
			$type = filter::type($_POST["type"]);
		}
		
		if ($lastName != "" && $firstName != "" && $salary != "" && $street != "" && $city != "" && $state != "" && $zip != "" && $number != "" && $type != "") {
			
			// decide what IndexModels method we should use, depending on the request
			if ($_POST["do"] == "saveNewEmployee") {
				IndexModels::saveNewEmployee($lastName, $firstName, $salary, $street, $city, $state, $zip, $number, $type);
			} else if ($_POST["do"] == "saveEmployeeUpdate") {
				
				// grab the nameId
				$obfuscatedId = "";
				if (isset($_POST["obfuscatedId"]) == true && $_POST["obfuscatedId"] != "") {
					$obfuscatedId = $_POST["obfuscatedId"];
				}
				
				if ($obfuscatedId != "") {
					IndexModels::saveEmployeeUpdate($obfuscatedId, $lastName, $firstName, $salary, $street, $city, $state, $zip, $number, $type);
				}
				
			}
			
		}
		
		// after we've saved, forward the user back to our default screen
		header("Location: /");
		exit;
	}
	
	if ($_POST["do"] == "deleteEmployees") {
		
		// all your nameIds will come through PHP via an array
		$obfuscatedIds = [];
		if (isset($_POST["obfuscatedIds"]) == true) {
			$obfuscatedIds = $_POST["obfuscatedIds"];
		}
		
		if (count($obfuscatedIds) > 0) {
			IndexModels::deleteEmployees($obfuscatedIds);
		}
		
		// after we've deleted, forward the user back to our default screen
		header("Location: /");
		exit;
	}
	
} else {
	// show the default screen, which is a list of employees
	$results = IndexModels::index();
	IndexViews::index($results);
}

In the code above, we've added some code that checks the CSRF token when either a GET or a POST request comes in. If the CSRF token is missing or is incorrect, the code simply exits. The user is presented with a blank page.

Here's the $_POST example.

// check csrf token to make sure it matches
// exit if any of these conditions are not met
if (isset($_POST["csrf"]) == false || $_POST["csrf"] != $_SESSION["csrf"]) {
	exit;
}

Thankfully, there is nothing more we need to do to our IndexModels class. We are done!