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

CSRF is a security vulnerability which attackers use to trick their victim and make them perform an action they did not intend to. In this post we are going to implement CSRF security using a random CSRF token in 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 form to test CSRF token implementation.
- style.css: CSS styles for our web page and form.
What is CSRF?
CSRF (Cross-Site Request Forgery) also known as one-click attack or session riding, is a forged request approach an attacker uses to attack his target without the victim even noticing it. The victim is tricked into performing an action that he did not intend to.
Example of CSRF Attack Request
For a quick example lets assume a user can transfer payments to other users within the same system and the form is using 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 pattern of request an attacker can very well prepare a page where he has set up this form for victim, All attacker has to do is send the link to this page to a victim who is already logged in the system.
How to Implement CSRF Security in PHP
There are many ways to protect website forms against such forged
requests. Most commonly used is generate a random unique CSRF token to be
sent along with request for verification. We are going to learn in this post how to implement CSRF protection in PHP for your form requests.
Enable CSRF Security Token Generation
This code enables CSRF configurations which will be used when user submits a form. Steps to enable CSRF protection:
- Define a global $config array variable.
- Set csrf_protection to true.
- Set a name for CSRF token.
- Set an expiry time for 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);
}
}
Create and Verify CSRF Token in PHP
Create a PHP CSRF class to generate and verify CSRF token along with expire time. This class is main script doing the job of creating CSRF security to ensure form was submitted by same user visiting the page. Steps to implement CSRF protection in PHP:
- Call the construct method of csrf class to generate CSRF if token.
- Generate CSRF token only if it is not already generated and has not expired.
- Show the CSRF input hidden field in HTML form.
- Compare CSRF token set in session with the token sent in form request.
- If token fails verification, show an error message.
The class definition contains following properties is as below:
- $csrf_token_name: A property to store CSRF token name for input field and verification.
- $csrf_token_expire: A property to store expire time of token.
While the methods used to create and verify CSRF token, displaying token field are as below.
- __construct: We set the CSRF token name and expire from configuration file. Then we generate CSRF token if it has not been generated or if it has expired.
- token_is_set: Checks if token is already set in session.
- token_has_expired: Checks if token in session has expired.
- get_token_name: Return token name string.
- get_token: Returns the generated token from session or creates a new one.
- generate_token: Generates a new token if content length is null.
- verify_token: Verifies the token in request with token stored in 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_FULL_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();
Render HTML Form with CSRF Token
This file in which we create our HTML form with token field using the PHP CSRF class.
index.php
<?php
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"/>
<link rel="stylesheet" href="css/style.css" />
</head>
<body>
<div class="container">
<div class="my-4">
<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>
</div>
</body>
</html>
Show CSRF Verification Failed Message in PHP
Create an error file which 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>
Add CSS Styles
Add all CSS styles for our entire page including the example 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;
}
.my-4{
margin-top: 1rem;
margin-bottom: 1rem;
}
.btn {
display: inline-block;
padding: 5px 10px;
cursor: pointer;
font: inherit;
}
.btn-green {
background-color: #00a65a;
border: 1px solid #009549;
color: #ffffff;
}