Summary: in this tutorial, you will learn about cross-site request forgery (CSRF) attacks and how to prevent them in PHP.
What is CSRF
CSRF stands for cross-site request forgery. It’s a kind of attack in which a hacker forces you to execute an action against a website where you’re currently logged in.
For example, you visit the malicious-site.com
that has a hidden form. And that form submits on page load to yourbank.com/transfer-fund
form.
Because you’re currently logged in to the yourbank.com
, the request silently transfers a fund out of your bank account.
If yourbank.com/transfer-fund
implements the CSRF correctly, it generates a one-time token and inserts the token into the fund transfer form like this:
<input type="hidden"
name="token"
value="b3f44c1eb885409c222fdb78c125f5e7050ce4f3d15e8b15ffe51678dd3a33d3a18dd3">
Code language: PHP (php)
When the malicious-site.com
submits the form, the yourbank.com/transfer-fund
form compares the token with the one on the yourbank.com
‘s server.
If the token doesn’t exist in the submitted data or it doesn’t match with the token on the server, the fund transfer form will reject the submission and return an error.
When the malicious-site.com
tries to submit the form, the token is likely not available or won’t match.
How to implement CSRF token in PHP
First, create a one-time token and add it to the $_SESSION
variable:
$_SESSION['token'] = md5(uniqid(mt_rand(), true));
Code language: PHP (php)
Second, add a hidden field whose value is the token and insert it into the form:
<input type="hidden" name="token" value="<?php echo $_SESSION['token'] ?? '' ?>">
Code language: PHP (php)
Third, when the form is submitted, check if the token exists in the INPUT_POST
and compare it with the $_SESSION['token']
:
<?php
$token = filter_input(INPUT_POST, 'token', FILTER_SANITIZE_STRING);
if (!$token || $token !== $_SESSION['token']) {
// return 405 http status code
header($_SERVER['SERVER_PROTOCOL'] . ' 405 Method Not Allowed');
exit;
} else {
// process the form
}
Code language: PHP (php)
If the token doesn’t exist or doesn’t match, return the 405 HTTP status code and exit.
PHP CSRF example
We’ll create a simple fund transfer form to demonstrate how to prevent a CSRF attack:
First, create the following file and directory:
.
├── css
│ └── style.css
├── inc
│ ├── footer.php
│ ├── get.php
│ ├── header.php
│ └── post.php
└── index.php
Code language: PHP (php)
header.php
The header.php
file contains the code that shows the first section of the page:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="css/style.css">
<title>PHP CSRF - Fund Transfer Demo</title>
</head>
<body>
<main>
Code language: PHP (php)
footer.php
The footer.php
file contains the closing tags corresponding to the opening tags in the header.php
file:
</main>
</body>
</html>
Code language: PHP (php)
index.php file
Place the following code to the index.php
file:
<?php
session_start();
require __DIR__ . '/inc/header.php';
$errors = []; // for storing the error messages
$inputs = []; // for storing sanitized input values
$request_method = strtoupper($_SERVER['REQUEST_METHOD']);
if ($request_method === 'GET') {
// generate a token
$_SESSION['token'] = bin2hex(random_bytes(35));
// show the form
require __DIR__ . '/inc/get.php';
} elseif ($request_method === 'POST') {
// handle the form submission
require __DIR__ . '/inc/post.php';
// re-display the form if the form contains errors
if ($errors) {
require __DIR__ . '/inc/get.php';
}
}
require __DIR__ . '/inc/footer.php';
Code language: PHP (php)
How the index.php
works.
First, start a new session by calling the session_start()
function; use the $errors
array to store the error messages and the $inputs
array is to store sanitized input values.
Next, show the form in the get.php
file if the HTTP request is GET.
The following generates the one-time token:
$_SESSION['token'] = bin2hex(random_bytes(35));
Code language: PHP (php)
The random_bytes(35)
generates a random string with 35 characters. And the bin2hex()
function returns the hexadecimal representation of the random string. The token will look like this:
c4724e490407a1770efcc4ea19776c06e0bd4614a9dd37900f5eb001581dffee9b377a
Code language: PHP (php)
Then, load the code from the post.php
file to handle the form submission if the HTTP request is POST.
After that, show the form again with error messages if the form data is invalid. Note that if the form has errors, the $errors
will contain the error messages.
Finally, display a message from the message.php
file to inform that the fund has been transferred successfully.
get.php
The following creates the fund transfer form with two input fields transfer amount and recipient account:
<form action="<?= htmlspecialchars($_SERVER['PHP_SELF']) ?>" method="post">
<header>
<h1>Fund Transfer</h1>
</header>
<div>
<label for="amount">Amount (between $1-$5000):</label>
<input type="number" name="amount" value="<?= $inputs['amount'] ?? '' ?>" id="amount" placeholder="Enter the transfered amount">
<small><?= $errors['amount'] ?? '' ?></small>
</div>
<div>
<label for="recipient_account">Recipient Account:</label>
<input type="number" name="recipient_account" value="<?= $inputs['recipient_account'] ?? '' ?>" id="recipient_account" placeholder="Enter the recipient account">
<small><?= $errors['recipient_account'] ?? '' ?></small>
</div>
<input type="hidden" name="token" value="<?= $_SESSION['token'] ?? '' ?>">
<button type="submit">Transfer Now</button>
</form>
Code language: PHP (php)
post.php
The following code validates the token and form data:
<?php
$token = filter_input(INPUT_POST, 'token', FILTER_SANITIZE_STRING);
if (!$token || $token !== $_SESSION['token']) {
// show an error message
echo '<p class="error">Error: invalid form submission</p>';
// return 405 http status code
header($_SERVER['SERVER_PROTOCOL'] . ' 405 Method Not Allowed');
exit;
}
// Validate amount
$amount = filter_input(INPUT_POST, 'amount', FILTER_SANITIZE_NUMBER_INT);
$inputs['amount'] = $amount;
if ($amount) {
$amount = filter_var(
$amount,
FILTER_VALIDATE_INT,
['options' => ['min_range' => 1, 'max_range' => 5000]]
);
if (!$amount) {
$errors['amount'] = 'Please enter a valid amount (from $1 to $5000)';
}
} else {
$errors['amount'] = 'Please enter the transfered amount.';
}
// validate account (simple)
$recipient_account = filter_input(INPUT_POST, 'recipient_account', FILTER_SANITIZE_NUMBER_INT);
$inputs['recipient_account'] = $recipient_account;
if ($recipient_account) {
$recipient_account = filter_var($recipient_account, FILTER_VALIDATE_INT);
if (!$recipient_account) {
$errors['recipient_account'] = 'Please enter a valid recipient account';
}
// validate the recipient account against the database
// ...
} else {
$errors['recipient_account'] = 'Please enter the recipient account.';
}
Code language: PHP (php)
How the post.php
works.
First, sanitize the token from the INPUT_POST:
$token = filter_input(INPUT_POST, 'token', FILTER_SANITIZE_STRING);
Code language: PHP (php)
The filter_input()
function returns null if the token is included in the submitted data. It returns false
if the FILTER_SANITIZE_STRING
filter fails to filter the token.
Second, compare the sanitized token with the one stored in the $_SESSION
variable:
if (!$token || $token !== $_SESSION['token']) {
// process error
}
Code language: PHP (php)
If they’re not matched, we return the HTTP status code 405 (method not allowed) to the client using the header()
function and immediately stops the script.
header($_SERVER['SERVER_PROTOCOL'] . ' 405 Method Not Allowed');
Code language: PHP (php)
The remaining code sanitizes and validates the amount and recipient account. If there is no error, we show a confirmation message:
<?php if (!$errors) : ?>
<section>
<div class="circle">
<div class="check"></div>
</div>
<h1 class="message">You've transfered</h1>
<h2 class="amount">$<?= $amount ?></h2>
<a href="<?= htmlspecialchars($_SERVER['PHP_SELF']) ?>" rel="prev">Done</a>
</section>
<?php endif ?>
Code language: HTML, XML (xml)
Summary
- CSRF attacks force users to execute an action against the site where they’re currently logged in.
- Use the
bin2hex(random_bytes(35))
to generate the one-time token. - Check the submitted token with the one stored in the
$_SESSION
to prevent the CSRF attacks.