Protect Forms with CSRF Token in PHP
Just making an eye-catching website is not enough. Keeping a website secure is one of the biggest challenges for web developers. In this post, we will learn what CSRF is, how it can harm a website, and how to make your website CSRF protected.

CSRF is a security vulnerability that attackers use to trick their victims and make them perform an action they did not intend to. This post demonstrates how to implement CSRF security using a random CSRF token in a request in PHP. Files we are going to create for this post:
- config.php: Configuration to enable CSRF protected requests.
- csrf.php: A CSRF class containing the methods to implement CSRF token generation and verification.
- index.php: An HTML page with a form to test the CSRF token implementation.
- style.css: CSS styles for our web page and form.
What is CSRF?
CSRF (Cross-Site Request Forgery), also known as a one-click attack or session riding, is a forged request approach that an attacker uses to attack their target without the victim even noticing it. The victim is tricked into performing an action that he did not intend to.
Example of a CSRF Attack Request
For a quick example, let's assume a user can transfer payments to other users within the same system, and the form is using the POST method to transfer payment. The request looks like this:
<body onload="document.forms[0].submit()">
<form action="http://paymentsystem.com/funds-trasfer" method="POST">
<input type="hidden" name="account_number" value="attackers_account"/>
<input type="hidden" name="amount" value="1000"/>
<input type="submit" value="How to earn $100 a day"/>
</form>
</body>
Now knowing the pattern of request, an attacker can very well prepare a page where he has set up this form for the victim. All an attacker has to do is send the link to this page to a victim who is already logged into the system.
How to Implement CSRF Security in PHP
There are many ways to protect website forms against such forged requests. The most commonly used method is to generate a random, unique CSRF security token and send it along with the request for verification. We are going to learn in this post how to implement CSRF protection in PHP for your form requests.
Step 1: Configure CSRF Security & Token Generation
This code enables CSRF configurations that will be used when the user submits a form. Steps to enable CSRF protection:
- Define a global
$configarray variable. - Set
csrf_protectionto true. This will enable CSRF protection. - Set
csrf_token_nameas the name for the CSRF token. - Set
csrf_token_expireas an expiry time for the CSRF token.
config.php
<?php
global $config;
$config['csrf_protection'] = true;
$config['csrf_token_name'] = 'csrf_token';
$config['csrf_token_expire'] = 300;
/**
* Loads an error page for failed verification of CSRF token
*/
if (!function_exists('show_error')) {
function show_error(string $heading, string $message)
{
include 'errors/csrf.php';
die(0);
}
}
Step 2: Create a PHP Class to Generate and Verify CSRF Token
Create a PHP CSRF class to generate and verify a CSRF token along with an expiration time. This class is the main script doing the job of creating CSRF security to ensure the form was submitted by the same user visiting the page. Steps to implement CSRF protection in PHP:
- Call the construct method of the csrf class to generate a CSRF token.
- Generate a CSRF token only if it is not already generated and has not expired.
- Show the token input hidden field in an HTML form.
- Compare the CSRF token set in session with the token sent in the form request.
- If the token verification fails, show an error message.
The class definition contains the following properties as below:
$csrf_token_name:A property to store the CSRF token name for the input field and verification.$csrf_token_expire:A property to store the expiration time of the token.
While the methods used to create and verify the CSRF token, displaying the token field are as below.
__construct(): We set the CSRF token name and expiration time from the configuration file. Then we generate a CSRF token if it has not been generated or if it has expired.token_is_set(): Checks if the token is already set in the session.token_has_expired(): Checks if the token in session has expired.get_token_name(): Returns the token name string.get_token(): Returns the generated token from the session or creates a new one.generate_token(): Generates a new token if the content length is null.verify_token(): Verifies the token in the request with the token stored in the session.get_token_field(): Return token input field as HTML string.
csrf.php
<?php
class csrf
{
private static string $csrf_token_name;
private static string $csrf_token_expire;
public static function construct(): void
{
global $config;
self::$csrf_token_name = $config['csrf_token_name'];
self::$csrf_token_expire = $config['csrf_token_expire'];
// Generate CSRF token only if it is not set in session or has not expired yet
if (!self::token_is_set() || self::token_has_expired()) {
self::generate_token();
}
}
/**
* Checks if CSRF token is set in session
* @return bool
*/
public static function token_is_set(): bool
{
return !empty($_SESSION[self::$csrf_token_name]);
}
/**
* Checks if current CSRF token has expired
* @return bool
*/
public static function token_has_expired(): bool
{
return $_SESSION['csrf_token_expire'] < time();
}
/**
* Returns token field name
* @return string
*/
public static function get_token_name(): string
{
return self::$csrf_token_name;
}
/**
* Returns the generated token or creates a new token and returns
* @return string
*/
public static function get_token(): string
{
return $_SESSION[self::$csrf_token_name] ?? self::generate_token();
}
/**
* Generates a new token and sets token along with expire time in session
*/
public static function generate_token(): void
{
$content_length = filter_input(INPUT_SERVER, 'CONTENT_LENGTH', FILTER_SANITIZE_NUMBER_INT);
// Generate token only if content length is not given, i.e. Form was not submitted
if (is_null($content_length)) {
$csrf_token = bin2hex(random_bytes(16));
$_SESSION[self::$csrf_token_name] = $csrf_token;
$_SESSION['csrf_token_expire'] = time() + intval(self::$csrf_token_expire);
}
}
/**
* Checks if token exists in request, and it matches the generated token in session
* @return void
*/
public static function verify_token(): void
{
$methods = ['GET' => filter_input_array(INPUT_GET), 'POST' => filter_input_array(INPUT_POST)];
$request_method = filter_input(INPUT_SERVER, 'REQUEST_METHOD', FILTER_SANITIZE_SPECIAL_CHARS);
$content_length = filter_input(INPUT_SERVER, 'CONTENT_LENGTH', FILTER_SANITIZE_NUMBER_INT);
$token_name = self::$csrf_token_name;
if (!empty($methods[$request_method]) && !is_null($content_length)) {
if (!isset($methods[$request_method][$token_name])) {
show_error('CSRF ERROR', 'No CSRF token was sent in request.');
}
if ($_SESSION['csrf_token_expire'] < time()) {
show_error('CSRF ERROR', 'CSRF token has expired, Please reload the page.');
}
if (!hash_equals($methods[$request_method][$token_name], $_SESSION[$token_name])) {
show_error('CSRF ERROR', 'CSRF token did not match, Please reload the page.');
}
}
}
/**
* Returns CSRF token input field
* @return string
*/
public static function get_token_field(): string
{
return '<input type="hidden" name="' . self::get_token_name() . '" value="' . self::get_token() . '" />';
}
}
// Generate the token on first load
csrf::construct();
Step 3: Render HTML Form with CSRF Token
This file in where we create our HTML form with a token field using the PHP CSRF class. In this file, we check if CSRF protection is configured; we include the csrf.php class. We check if $_POST data is not empty i.e. form was submitted, then verify the CSRF token using the csrf::verify_token() method of the csrf class.
index.php
<?php
global $config;
if (!session_id()) {
session_start();
}
include 'config.php';
// Implement CSRF protection if enabled in config
if ($config['csrf_protection']) {
include 'csrf.php';
}
if (!empty(filter_input_array(INPUT_POST))) {
csrf::verify_token();
// Process form data here
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Protect Forms with CSRF Token in PHP - Demo</title>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/>
<meta content="width=device-width, initial-scale=1, maximum-scale=1" name="viewport"/>
<link rel="stylesheet" href="css/style.css"/>
</head>
<body>
<section class="section py-4">
<div class="container">
<p>Open this same page in new tab and click submit on this page to test csrf.</p>
<form name="csrf_form" method="POST">
<?= csrf::get_token_field(); ?>
<button type="submit" class="btn btn-green">Submit</button>
</form>
</div>
</section>
</body>
</html>
Step 4: Display the CSRF Verification Failed Message in PHP
Create an error file that displays an error message when CSRF token verification in PHP fails.
errors/csrf.php
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>CSRF Error</title>
<style type="text/css">
* {
box-sizing: border-box;
}
html, body {
margin: 0;
padding: 0;
}
body {
background-color: #f6f6f6;
font-family: "Segoe UI", "Roboto", "Helvetica", sans-serif;
font-size: 15px;
font-weight: normal;
font-style: normal;
line-height: 1.5;
}
.container {
width: 100%;
max-width: 1140px;
margin-right: auto;
margin-left: auto;
padding-right: 15px;
padding-left: 15px;
}
.error-wrapper {
border: 1px solid #d0d0d0;
margin-top: 1rem;
margin-bottom: 1rem;
}
.error-heading {
color: #e42c2c;
background: #d0d0d0;
border-bottom: 1px solid #d0d0d0;
padding: 0.5rem 1rem;
}
.error-title {
margin: 0;
font-weight: 600;
}
.error-body {
padding: 1rem;
}
</style>
</head>
<body>
<div class="container">
<div class="error-wrapper">
<div class="error-heading">
<h1 class="error-title"><?=$heading;?></h1>
</div>
<div class="error-body"><?=$message;?></div>
</div>
</div>
</body>
</html>
Step 5: Add CSS Styles for CSRF Form
Add all CSS styles for our entire page, including the example CSRF form.
style.css
* {
box-sizing: border-box;
}
html,body {
margin: 0;
padding: 0;
}
body {
background-color: #f6f6f6;
font-family: "Segoe UI", "Roboto", "Helvetica", sans-serif;
font-size: 15px;
font-weight: normal;
font-style: normal;
line-height: 1.5;
}
.container {
width: 100%;
max-width: 1140px;
margin-right: auto;
margin-left: auto;
padding-right: 15px;
padding-left: 15px;
}
.py-4 {
padding-top: 1rem;
padding-bottom: 1rem;
}
.btn {
display: inline-block;
padding: 5px 10px;
cursor: pointer;
font: inherit;
}
.btn-green {
background-color: #00a65a;
border: 1px solid #009549;
color: #ffffff;
}