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 CSRF is, how can it harm a website and how to make your website CSRF protected.

Protect Forms with CSRF Token in PHP

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 its victim without victim even noticing it. The victim is tricked to perform an action that he did intend to. 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. There are many ways to protect website forms against such forged requests. Most commonly used is generate a random unique token to be sent along with request to verify.I am going to show you how can you implement CSRF protection on forms.

config.php

Configurations for csrf like enabling csrf, token name, and expiry goes here.
<?php
global $config;
$config["csrf_protection"] = true;
$config["csrf_token_name"] = "csrf_token";
$config["csrf_token_expire"] = 300;

/******************************************************
 * Function to load error page and exit
*******************************************************/
if(!function_exists("show_error")){
    function show_error($heading,$message){
        include("errors/general.php");
        die(0);
    }
}

csrf.php

CSRF class which will be performing actions such as generating unique token, verify token sent in request, showing input field for form.
<?php
class csrf{

    private static $csrf_token_name;
    private static $csrf_token_expire;

    public static function construct(){
        global $config;
        self::$csrf_token_name = $config["csrf_token_name"];
        self::$csrf_token_expire = $config["csrf_token_expire"];
        self::generate_token();
    }
    /************************************************************
     * This function will generate a unique a token
     * Sets generate token in session
     * Sets token expiry in session
    ************************************************************/
    public static function generate_token(){
        if(empty($_POST) && empty($_GET)){

            $csrf_token = bin2hex(random_bytes(16));
            $_SESSION[self::$csrf_token_name] = $csrf_token;
            $_SESSION["csrf_token_expire"] = time() + intval(self::$csrf_token_expire);

            return $csrf_token;
        }
    }

    /************************************************************
     * This function will check if a token is already generated
     * then generated then return that token
     * otherwise generate the token return generated token
    ************************************************************/
    public static function get_token(){

        if(isset($_SESSION[self::$csrf_token_name])){
            $csrf_token = $_SESSION[self::$csrf_token_name];
        }else{
            $csrf_token = self::generate_token();
        }

        return $csrf_token;
    }
    /************************************************************
     * This function will get the request method
     * checks if token exists in request
     * checks if token sent in request matches the generated token
    ************************************************************/
    public static function verify_token(){

        $request = $_SERVER["REQUEST_METHOD"];
        $methods = ["GET" => $_GET, "POST" => $_POST];
        $token_name = self::$csrf_token_name;

        if(!empty($methods[$request])){
            if(!isset($methods[$request][$token_name])){
                show_error("CSRF ERROR","No CSRF token was sent");
            }

            if($_SESSION["csrf_token_expire"] < time()){
                if($methods[$request][$token_name] !== $_SESSION[$token_name]){
                    show_error("CSRF ERROR","CSRF token did not match. Please reload page");
                }
            }
        }
    }

    /************************************************************
     * This function will return token name
    ************************************************************/
    public static function get_token_name(){
        return self::$csrf_token_name;
    }

    /************************************************************
     * This function will return token input field for forms
    ************************************************************/
    public static function get_token_field(){
        return '<input type="hidden" name="csrf_token" value="'.self::get_token().'" />';
    }
}

// Generate the token on first load
csrf::construct();
?>

index.php

Contains example form with csrf token included.
<?php
session_start();
include("config.php");

// Implement CSRF protection if enabled in config
if($config["csrf_protection"]){
    include("csrf.php");
    csrf::verify_token($_POST);
}
if(!empty($_POST)){
// Process form data here
}
?>
<!DOCTYPE html>
<html>
    <head>
        <title>Protect Forms with CSRF Token in PHP </title>
        <meta content='text/html; charset=UTF-8' http-equiv='Content-Type'/>
        <link rel="stylesheet" href="css/style.css" />
    </head>
    <body>
        <div class="main-container">
            <div class="section">
                <form name="csrf_form" method="POST">
                    <?php echo csrf::get_token_field();?>
                    <button type="submit" class="btn btn-green">Submit</button>
                </form>
            </div>
        </div>
    </body>
</html>

errors/general.php

An error to show an error when token verification is failed.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
<style type="text/css">
body {
 background-color: #fff;
 margin: 40px;
 font: 13px/20px normal Helvetica, Arial;
 color: #4F5155;
}
#container {
    margin: 10px;
    border: 1px solid #D0D0D0;
    box-shadow: 0 0 2px #D0D0D0;
}
h1 {
    color: #e42c2c;
    background: #D0D0D0;
    border-bottom: 1px solid #D0D0D0;
    font-size: 19px;
    font-weight: normal;
    margin: 0 0 14px 0;
    padding: 14px 15px 10px 15px;
}
p {
    margin: 12px 15px 12px 15px;
}
</style>
</head>
<body>
    <div id="container">
        <h1><?php echo $heading; ?></h1>
        <p><?php echo $message; ?></p>
    </div>
</body>
</html>

style.css

All the style for index.php are wrapped in this stylesheet.
*{
    box-sizing: border-box;
}
html,body{
    margin: 0px;
}
body{
    background: #f0f0f0;
    font: normal normal 14px Open Sans,Verdana, Arial;
}
.main-container {
    max-width: 1024px;
    margin: 0px auto;
}
.section{
    padding: 15px;
    background: #fff;
}

.btn-green {
    background: #00a65a;
    border: 1px solid #009549;
    color: #fff;
    text-decoration: none;
    display: inline-block;
    padding: 5px 10px;
    cursor: pointer;
}