PHP CSRF

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:

php csrf demo

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.
Did you find this tutorial useful?