This recipe presents another very simple technique that will safeguard your forms against Cross Site Request Forgery (CSRF) attacks. Simply put, a CSRF attack is possible when, possibly using other techniques, an attacker is able to infect a web page on your website. In most cases, the infected page will then start issuing requests (that is, using JavaScript to purchase items, or make settings changes) using the credentials of a valid, logged-in user. It's extremely difficult for your application to detect such activity. One measure that can easily be taken is to generate a random token that is included in every form to be submitted. Since the infected page will not have access to the token, nor have the ability to generate one that matches, form validation will fail.
chap_12_form_csrf_test_unprotected.html:<!DOCTYPE html>
<body onload="load()">
<form action="/chap_12_form_unprotected.php"
method="post" id="csrf_test" name="csrf_test">
<input name="name" type="hidden" value="No Goodnick" />
<input name="email" type="hidden" value="malicious@owasp.org" />
<input name="comments" type="hidden"
value="Form is vulnerable to CSRF attacks!" />
<input name="process" type="hidden" value="1" />
</form>
<script>
function load() { document.forms['csrf_test'].submit(); }
</script>
</body>
</html>chap_12_form_unprotected.php that responds to the form posting. As with other calling programs in this book, we set up autoloading and use the Application\Database\Connection class covered in Chapter 5, Interacting with a Database:<?php
define('DB_CONFIG_FILE', '/../config/db.config.php');
require __DIR__ . '/../Application/Autoload/Loader.php';
Application\Autoload\Loader::init(__DIR__ . '/..');
use Application\Database\Connection;
$conn = new Connection(include __DIR__ . DB_CONFIG_FILE);if ($_POST['process']) {
$filter = [
'trim' => function ($item) { return trim($item); },
'email' => function ($item) {
return filter_var($item, FILTER_SANITIZE_EMAIL); },
'length' => function ($item, $length) {
return substr($item, 0, $length); },
'stripTags' => function ($item) {
return strip_tags($item); },
];
$assignments = [
'*' => ['trim' => NULL, 'stripTags' => NULL],
'email' => ['length' => 249, 'email' => NULL],
'name' => ['length' => 128],
'comments'=> ['length' => 249],
];
$data = $_POST;
foreach ($data as $field => $item) {
foreach ($assignments['*'] as $key => $option) {
$item = $filter[$key]($item, $option);
}
if (isset($assignments[$field])) {
foreach ($assignments[$field] as $key => $option) {
$item = $filter[$key]($item, $option);
}
$filteredData[$field] = $item;
}
}chap_12_form_view_results.php, which simply dumps the contents of the visitors table:try {
$filteredData['visit_date'] = date('Y-m-d H:i:s');
$sql = 'INSERT INTO visitors '
. ' (email,name,comments,visit_date) '
. 'VALUES (:email,:name,:comments,:visit_date)';
$insertStmt = $conn->pdo->prepare($sql);
$insertStmt->execute($filteredData);
} catch (PDOException $e) {
echo $e->getMessage();
}
}
header('Location: /chap_12_form_view_results.php');
exit;random_bytes() PHP 7 function to generate a truly random token, one which will be difficult, if not impossible, for an attacker to match:session_start(); $token = urlencode(base64_encode((random_bytes(32)))); $_SESSION['token'] = $token;
<input type="hidden" name="token" value="<?= $token ?>" />
chap_12_form_unprotected.php script mentioned previously, adding logic to first check to see whether the token matches the one stored in the session. Note that we unset the current token to make it invalid for future use. We call the new script chap_12_form_protected_with_token.php:if ($_POST['process']) {
$sessToken = $_SESSION['token'] ?? 1;
$postToken = $_POST['token'] ?? 2;
unset($_SESSION['token']);
if ($sessToken != $postToken) {
$_SESSION['message'] = 'ERROR: token mismatch';
} else {
$_SESSION['message'] = 'SUCCESS: form processed';
// continue with form processing
}
}To test how an infected web page might launch a CSRF attack, create the following files, as shown earlier in the recipe:
chap_12_form_csrf_test_unprotected.htmlchap_12_form_unprotected.phpYou can then define a file called chap_12_form_view_results.php, which dumps the visitors table:
<?php
session_start();
define('DB_CONFIG_FILE', '/../config/db.config.php');
require __DIR__ . '/../Application/Autoload/Loader.php';
Application\Autoload\Loader::init(__DIR__ . '/..');
use Application\Database\Connection;
$conn = new Connection(include __DIR__ . DB_CONFIG_FILE);
$message = $_SESSION['message'] ?? '';
unset($_SESSION['message']);
$stmt = $conn->pdo->query('SELECT * FROM visitors');
?>
<!DOCTYPE html>
<body>
<div class="container">
<h1>CSRF Protection</h1>
<h3>Visitors Table</h3>
<?php while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) : ?>
<pre><?php echo implode(':', $row); ?></pre>
<?php endwhile; ?>
<?php if ($message) : ?>
<b><?= $message; ?></b>
<?php endif; ?>
</div>
</body>
</html>From a browser, launch chap_12_form_csrf_test_unprotected.html. Here is how the output might appear:

As you can see, the attack was successful despite filtering and the use of prepared statements!
Next, copy the
chap_12_form_unprotected.php file to chap_12_form_protected.php. Make the change indicated in step 8 in the recipe. You will also need to alter the test HTML file, copying chap_12_form_csrf_test_unprotected.html to chap_12_form_csrf_test_protected.html. Change the value for the action parameter in the FORM tag as follows:
<form action="/chap_12_form_protected_with_token.php" method="post" id="csrf_test" name="csrf_test">
When you run the new HTML file from a browser, it calls chap_12_form_protected.php, which looks for a token that does not exist. Here is the expected output:

Finally, go ahead and define a file called chap_12_form_protected.php that generates a token and displays it as a hidden element:
<?php
session_start();
$token = urlencode(base64_encode((random_bytes(32))));
$_SESSION['token'] = $token;
?>
<!DOCTYPE html>
<body onload="load()">
<div class="container">
<h1>CSRF Protected Form</h1>
<form action="/chap_12_form_protected_with_token.php"
method="post" id="csrf_test" name="csrf_test">
<table>
<tr><th>Name</th><td><input name="name" type="text" /></td></tr>
<tr><th>Email</th><td><input name="email" type="text" /></td></tr>
<tr><th>Comments</th><td>
<input name="comments" type="textarea" rows=4 cols=80 />
</td></tr>
<tr><th> </th><td>
<input name="process" type="submit" value="Process" />
</td></tr>
</table>
<input type="hidden" name="token" value="<?= $token ?>" />
</form>
<a href="/chap_12_form_view_results.php">
CLICK HERE</a> to view results
</div>
</body>
</html>When we display and submit data from the form, the token is validated and the data insertion is allowed to continue, as shown here:

For more information on CSFR attacks, please refer to https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF).