3 Steps to Implement Simple CSRF Token in PHP

INTRODUCTION

SECURE YOUR SITE!

Welcome to a step-by-step tutorial on how to implement simple CSRF token in PHP. In this tutorial, we will walk through a simple example of what cross-site request forgery (CSRF) is, and how we can prevent it using a token In just 3 simple steps:

  1. Generate a random token in the PHP session – $_SESSION[‘token’] = substr(base_convert(sha1(uniqid(mt_rand())), 16, 36), 0, 32;
  2. Place this token into the HTML form – <input type=”hidden” name=”token” value=”<?=$_SESSION[‘token’]?>”/>
  3. When the form is submitted, cross-check the submitted token against the session – if ($_POST[‘token’] == $_SESSION[‘token’]) { … }

That’s it. But just how does this thing work exactly? How will this token prevent a CSRF attack? Read on to find out!

ⓘ I have included a zip file with all the example source code at the start of this tutorial, so you don’t have to copy-paste everything… Or if you just want to dive straight in.

 

 

 

PREAMBLE

DOWNLOAD & NOTES

First, here is the download link to all the example source code as promised.

 

EXAMPLE CODE DOWNLOAD

Click here to download the source code, I have released it under the MIT license, so feel free to build on top of it or use it in your own project.

 

QUICK NOTES

There is nothing to install, so just download and unzip into a folder. If you spot a bug, please feel free to comment below. I try to answer questions too, but it is one person versus the entire world… If you need answers urgently, please check out my list of websites to get help with programming.

 

SECTION A

WHAT IS CSRF?

For this first section, we are going to debunk the mystery of cross-site request forgery (CSRF) – What the heck it is, and why you should be worried. For you guys who already know what it is, feel free to skip this section.

 

THE VIDEO EXPLANATION

If you have 10 minutes to spare, here is a detailed explanation by Computerphile… For you guys who don’t want to spend too much time, I have a short compressed example below.

 

A VERY SIMPLE EXAMPLE OF CSRF

You have signed up at “legit site” and is currently signed in. This “legit site” has a form where you can delete your account. For example, all you need to do is to enter “CONFIRM” to delete the account:

http://legit-site.com/my-account.php
<form action="http://legit-site.com/delete.php" method="POST">
  <p>Type in "CONFIRM" below to delete your account</p>
  <input type="text" name="confirmation"/>
  <input type="submit" value="DELETE ACCOUNT"/>
</form>

Now let’s say that on another “fake site” that evil code ninjas have created, they wrote a fake viral article to bait people into writing comments. But the comments form is fake, and it actually submits to delete your account on “legit site” instead:

http://fake-site.com/fake-article.php
<form action="http://legit-site.com/delete.php" method="POST">
  <!-- THE NINJA MALICIOUS CODE -->
  <input type="hidden" name="confirmation" value="CONFIRM"/>

  <!-- FAKE FIELDS TO ACT LIKE LEGIT COMMENTS FORM -->
  Name - <input type="text"/>  
  Message - <input type="text"/>
  <input type="submit" value="Reply"/>
</form>

Will this method actually work? Yes, if the security at “legit site” is dumb enough. Since you are already signed into the “legit site”, it will probably be taken as a legit request to delete your account.

This is only a small example of how CSRF attacks work… So what if “legit site” is an eCommerce website? That will mean hackers may be able to purchase stuff using your account, without your knowledge. What can be worse? What if this attack is done on a money transfer website? I think you get the picture now, and this is why CSRF prevention is such a big deal.

 

 

THE SAVING GRACE

How do we prevent such attacks then? Thankfully, modern technology and firewalls have become smarter to detect such attacks – Firewalls and servers can be configured to ignore requests that originate from other websites. But we cannot count on that single layer of security to be 100% fool-proof, and thus, we will walk through another prevention measure in PHP.

 

THE SIMPLE ILLUSTRATION

A Simple CSRF Attack Example (click to enlarge)

 

SECTION B

THE CSRF TOKEN

In this section, we shall go through the measures to prevent CSRF attacks in PHP – by the use of a token (or what most people call CSRF token). It may seem like rocket science complicated hacker stuff, but it is actually something laughably simple.

 

STEP 1) CREATE A RANDOM SESSION TOKEN

Start a session on your server, then generate a random unique token – Keep this random token in the session.

1-generate-token.php
<?php 
session_start();
if (empty($_SESSION['token'])) {
  // RANDOM STRING CREDITS : https://www.thecodedeveloper.com/generate-random-alphanumeric-string-with-php/
  $length = 32;
  $_SESSION['token'] = substr(base_convert(sha1(uniqid(mt_rand())), 16, 36), 0, $length); 
 
  // FOR PHP 7, CREDITS : https://stackoverflow.com/questions/6287903/how-to-properly-add-csrf-token-using-php
  // $_SESSION['token'] = bin2hex(random_bytes(32));
}

 

 

STEP 2) PLACE THE TOKEN IN THE FORM

Next, place the random token into a hidden field somewhere in your form.

2-token-form.php
<?php
session_start();
?>
<form method="post" action="3-verify-token.php">
  <p>The form that does absolutely nothing</p>
  <input type="text" name="hello" value="world"/>
  <input type="text" name="token" value="<?=$_SESSION['token']?>"/>
  <input type="submit" value="GO"/>
</form>

 

STEP 3) VERIFY THE TOKEN WHEN SUBMITTED

Finally, as the user submits the form, simply match the posted token against the session token on the server-side.

3-verify-token.php
<?php
session_start();
if ($_SESSION['token']==$_POST['token']) {
  // VALID TOKEN PROVIDED - PROCEED WITH PROCESS
} else {
  // ERROR - INVALID TOKEN
}

That’s it, the CSRF protection is now complete.

 

EXTRA) TOKEN LIFESPAN

Wait, what!? That is a “top-notch” security feature!? Yes, this simple “random string in session” will prevent most CSRF attacks because only the user will have that token and it is tied to the server session; The “fake site” does not have this security token, cannot be verified, and cannot launch an attack now.

But here’s the caveat – It will not deter the more “advanced hackers” if they manage to get their hands on that token in one way or another. So as a precaution, I will recommend that this token is renewed every time the user signs in; It should have a short lifespan and expires quickly.

Some high-security systems even set the life a token to as short as a couple of minutes, and the user has to refresh the page to get a new token when it expires.

4-token-expire-example.php
<?php
// GENERATE THE TOKEN, ADD AN EXPIRY TIMESTAMP
session_start();
$length = 32;
$_SESSION['token'] = substr(base_convert(sha1(uniqid(mt_rand())), 16, 36), 0, $length); 
// 1 hour = 60 seconds * 60 minutes = 3600
$_SESSION['token-expire'] = time() + 3600;

// THEN CHECK FOR THE EXPIRY ON REQUEST
if ($_SESSION['token']==$_POST['token']) {
  if (time() >= $_SESSION['token-expire']) {
    // EXPIRED - ASK USER TO RELOAD PAGE
  } else {
   // DO PROCESSING AS USUAL
  }
}

 

 

EXTRA) IMPLEMENTATION PAIN

I know, implementing this token is easy in a simple system. But when you have a lot of different service endpoints (e.g. create an account, delete the account, update the account, etc…), it becomes a hassle to do it for every single script. So this is kind of good programming practice, I will recommend putting all the service endpoints into a single script:

5-endpoint-example.php
<?php
// TOKEN CHECK
session_start();
if ($_SESSION['token']!=$_POST['token']) { die("INVALID TOKEN"); }

// PROCESS
switch ($_POST['req']) {
  case "add":
    // ADD NEW ACCOUNT
    break;

  case "edit":
    // UPDATE ACCOUNT
    break;

  case "del":
    // DELETE ACCOUNT
    break;
}

 

SECTION C

AJAX TOKEN?

You should know how the CSRF token works now, but the Internet is a lot more advanced today. In particular, we have something called AJAX. So how do we work with the CSRF token in AJAX?

 

JUST POST IT, AS IT IS

When you first load a webpage, you can weave the token into a hidden element, a hidden text field, or even directly into the Javascript.

6-ajax-page.php
<?php 
session_start();
$length = 32;
$_SESSION['token'] = substr(base_convert(sha1(uniqid(mt_rand())), 16, 36), 0, $length); ?>
 
<!-- PLACE TOKEN INTO HIDDEN FIELD ON PAGE -->
<input type="hidden" id="token" value="<?=$_SESSION['token']?>"/>
<div id="div-token" style="display:none"><?=$_SESSION['token']?></div>
 
<!-- OR DIRECTLY INTO JAVASCRIPT -->
<script>var token = <?=$_SESSION['token']?>;</script>

With that, you can post it along with your AJAX calls.

ajax-process.js
// APPEND FORM DATA + TOKEN
var data = new FormData();
data.append('token', document.getElementById("token").value);
data.append('KEY', 'VALUE');
 
// AJAX
var xhr = new XMLHttpRequest();
xhr.open('POST', "script.php", true);
xhr.onload = function(){
  console.log(this.response);
};
xhr.send(data);

 

 

EXTRA

USEFUL BITS & LINKS

That’s all for this guide, and here is a small section on some extras and links that may be useful to you.

 

LINKS & REFERENCES

 

EXTRA

VIDEO TUTORIAL

For you guys who want more, here’s the video tutorial, and shameless self-promotion – Subscribe to the Code Boxx YouTube channel for more!

 

YOUTUBE TUTORIAL

 

INFOGRAPHIC CHEAT SHEET

Simple CSRF Protection In PHP (click to enlarge)

 

CLOSING

WHAT NEXT?

Thank you for reading, and we have come to the end of this short tutorial. I hope it will help you make more secure sites in the future, and if you have anything to add to this tutorial, please feel free to comment below. Good luck, and happy coding!

14 thoughts on “3 Steps to Implement Simple CSRF Token in PHP”

  1. There’s a typo (“toekn”) in ajax-process.js:
    data.append(‘toekn’, document.getElementById(“token”).value);

  2. For example: the user opens two different pages, and user can be on both pages send Comment ….
    Token reset after each post?
    Do we need some tokens for each page?
    I think tokens should be like arrays and need check the requesting page… . Is that right?

    1. That is called over-thinking. 😆 Generate random token, set an expire time. Use it for everything until it expires, then regenerate. That’s all – If security is a big concern, set a short expiry time as above.

    1. Of course, and the steps are the same – Start the session, create a random token, put the hidden token into the form, then check the token as the form is being submitted.

    1. It does not matter, just fall back to an HTML-only form. As long as the token get passed back to the server and you can verify it. You will only get a problem when the user disables the cookie, which, there is nothing you can do about it.

          1. Thank you for the quick reply.
            From your code, there seems to be no setcookie command to write to the cookie(stored on the client side ) ? Or is it that there is no need to write the token value to the cookie ? As its just kept in the session file ( server side ) ?

            Thanks.

          2. Go through the cookie vs session tutorial again and slowly trace through the entire process if you are still confused.

            1) session_start() will generate a cookie, set a unique session ID, AND create a temporary session data file on the server.
            2) $_SESSION[‘token’] = “RANDOM” is kept on the server.
            3) The cookie with session ID is passed to the client – But whether the user’s device choose to keep or discard the cookie is beyond our control.
            4) On return visits, the client will send the cookie back to the server.
            5) session_start() will then use the cookie to tie back to the session ID, retrieve the temporary data file on the server.
            6) Without the cookie, there is no way to tie back to the session ID. Without the session ID, there is no way to retrieve the CSRF token. Without the CSRF token, there is no way we can verify. The system falls apart.

            Nothing to do with Javascript. We don’t want to keep the CSRF token in the cookie.

Leave a Comment

Your email address will not be published. Required fields are marked *