CSRF Token in PHP (Very Simple Example)

Welcome to a quick tutorial on how to implement CSRF token protection in PHP. Want to further secure your website, or just stumbled on this CSRF “token thing” on the Internet? Well, it stands for Cross-Site Request Forgery, and it is nothing more than a random string in the session.

// (A) START SESSION & GENERATE RANDOM TOKEN
session_start();
$_SESSION["token"] = bin2hex(random_bytes(32));

// (B) EMBED TOKEN INTO HTML FORM
<input type="hidden" name="token" value="<?=$_SESSION["token"]?>">

// (C) ON FORM SUBMIT, CHECK SUBMITTED TOKEN AGAINST SESSION
if (isset($_POST["token"]) && isset($_SESSION["token"]) && $_POST["token"]==$_SESSION["token"]) { PROCEED }

That’s all for the basics. But just what is CSRF? How will this token prevent a CSRF attack? Read on to find out!

 

 

TABLE OF CONTENTS

 

DOWNLOAD & NOTES

Here is the download link to the example code, so you don’t have to copy-paste everything.

 

EXAMPLE CODE DOWNLOAD

Source code on GitHub Gist

Just click on “download zip” or do a git clone. I have released it under the MIT license, so feel free to build on top of it or use it in your own project.

 

SORRY FOR THE ADS...

But someone has to pay the bills, and sponsors are paying for it. I insist on not turning Code Boxx into a "paid scripts" business, and I don't "block people with Adblock". Every little bit of support helps.

Buy Me A Coffee Code Boxx eBooks

 

 

WHAT IS CSRF?

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

 

SIMPLE EXAMPLE OF A CSRF ATTACK

THE LEGIT SITE

You are currently signed in at “legit site” and it has a form where you can delete your account. All you need to do is enter “CONFIRM” and submit it:

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>

 

THE BAD SITE

On another “fake site” that evil code ninjas have created, they wrote a viral article to bait people into writing comments. But the comments form actually submits “CONFIRM” to delete your account on the “legit site”:

http://fake-site.com/fake-article.php
<!-- FAKE COMMENT FORM THAT SUBMITS TO DELETE ACCOUNT ON "LEGIT SITE" -->
<form action="http://legit-site.com/delete.php" method="POST">
  <input type="hidden" name="confirmation" value="CONFIRM">
  Name - <input type="text">
  Message - <input type="text">
  <input type="submit" value="Reply">
</form>

Will this method actually work? Yes, if the security at the “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.

 

 

A LOT OF TROUBLE

This is only a small example of how CSRF attacks work. So what if the “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.

 

PHP CSRF TOKEN

Thankfully, modern technology and firewalls have become smarter to detect CSRF 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 the purpose of the CSRF token.

 

 TUTORIAL VIDEO

 

STEP 1) INSERT RANDOM CSRF TOKEN INTO FORM

1-generate-token.php
<?php 
// (A) GENERATE CSRF TOKEN (RANDOM STRING INTO SESSION)
// CREDITS : https://stackoverflow.com/questions/6287903/how-to-properly-add-csrf-token-using-php
session_start();
$_SESSION["token"] = bin2hex(random_bytes(32));
$_SESSION["token-expire"] = time() + 3600; // 1 hour = 3600 secs
 
// (B) EMBED TOKEN INTO FORM ?>
<form method="post" action="2-submit.php">
  <input type="hidden" name="token" value="<?=$_SESSION["token"]?>">
  <input type="email" name="email" value="jon@doe.com">
  <input type="submit" value="Go!">
</form>

Yes, this is just a “regular HTML form”. But take note that we generate a random string (token) in $_SESSION["token"], and insert it into a hidden form field.

 

 

STEP 2) VERIFY TOKEN ON FORM SUBMISSION

2-verify-token.php
<?php
// (A) START SESSION
session_start();

// (B) COUNTER CHECK SUBMITTED TOKEN AGAINST SESSION
if (
  isset($_POST["token"]) &&
  isset($_SESSION["token"]) &&
  isset($_SESSION["token-expire"]) &&
  $_SESSION["token"]==$_POST["token"]
) {
  // (B1) EXPIRED
  if (time() >= $_SESSION["token-expire"]) {
    exit("Token expired. Please reload form.");
  }
 
  // (B2) OK - DO YOUR PROCESSING
  unset($_SESSION["token"]);
  unset($_SESSION["token-expire"]);
  echo "OK";
}

// (C) INVALID TOKEN
else { exit("INVALID TOKEN"); }

That’s all for the “rocket science” CSRF token. How the heck does a random string in the session prevent forged requests?

  • Very simply, only the server ($_SESSION["token"]) and the user (<input name="token">) has the token.
  • The request will only proceed if the token is validated – if ($_SESSION["token"]==$_POST["token"]) { ... }
  • In other words – To forge a request, the “bad site” has to somehow get hold of the token.

This should be straightforward enough, but take extra note of the part “bad site gets hold of the token”. Yes, the protection is broken when the token is leaked. This is why we set an expiry ($_SESSION["token-expire"]) to reduce the risk of a leaked token; Very secure sites set the expiry to a few minutes, giving hackers very little time to even try to get the token.

 

 

EXTRA) HOW ABOUT AJAX FORMS?

3-ajax-form.php
<?php
// (A) GENERATE CSRF TOKEN (RANDOM STRING INTO SESSION)
session_start();
$_SESSION["token"] = bin2hex(random_bytes(32));
$_SESSION["token-expire"] = time() + 3600; // 1 hour = 3600 secs
 
// (B) EMBED TOKEN INTO FORM AS USUAL ?>
<form id="myForm" onsubmit="return doAJAX()">
  <input type="hidden" name="token" value="<?=$_SESSION["token"]?>">
  <input type="email" name="email" value="jon@doe.com">
  <input type="submit" value="Go!">
</form>
 
<script>
function doAJAX () {
  // (C) GET FORM DATA
  var data = new FormData(document.getElementById("myForm"));
 
  // (D) AJAX FETCH
  fetch("2-submit.php", { method: "POST", body: data })
  .then(res => res.text())
  .then(res => {
    // DO WHATEVER YOU NEED
    console.log(res);
    // if (res=="OK") { ... }
  })
  .catch(err => console.error(err));
  return false;
}
</script> 

Nothing special in particular. Just generate the token and send it as-it-is.

 

 

EXTRAS

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

 

THE END

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!

23 thoughts on “CSRF Token in PHP (Very Simple Example)”

  1. Hi,
    We have a large php application built over the years with ajax calls and trying to implement CSRF now. We have many functions with ajax calls. Do we need to touch each and every function having ajax calls for this purpose? OR, is there an easy way out to implement CSRF at one single place or few places ?
    Please suggest.

  2. I think this implementation still incorrect, because an attacker can ommit the token in the request, and bypass your validation

  3. You shouldn’t really trivialise to compare tokens in this way because it is prone to timing attacks (not mentioning mandatory sanitation of $_POST here for brevity)
    BAD:
    if($_SESSION['token']==$_POST['token']) { ... }

    GOOD:
    if(hash_equals(crypt('$_SESSION['token'],'$2a$07$usesomesillystringforsalt$'), crypt($_POST['token'],'$2a$07$usesomesillystringforsalt$')) { ... }

    1. I’ve noticed a strange (?) thing implementing hash_equals + crypt. An example to explain it (PHP 8.1.8):

      $sessionToken = crypt(“562710df09d5b3b1e33769cd50a7e15d0cad770e66771ebbe9”, ‘12345’);
      $postToken = crypt(“562710df09d5b3b1e33769cd50a7e15d0cad770e66771ebbe9”, ‘12345’);

      $res = hash_equals($sessionToken, $postToken);

      var_export($res); // true

      Now, even if you change the last character of $postToken (say “8” instead of “9”), hash_equals returns true (but it shouldn’t, right?).
      This doesn’t happen if what you change is the first character of $postToken (say “a” instead of “5”): it returns false – or if you don’t use crypt.

      Is it normal?

    2. Not too sure about crypto stuff, but probably not a good idea to use crypt – It does generate the same hash even if you change the last character.

    3. What you are promoting is “security by obscurity” (eyecandy-nothing-mistake). All operations are the same. All operation can be optimized. This kind of complication could make sense only when the two side of the operations are performed on different machines e.g. client and server.
      Then the stored and the compared values would differ in reality.

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

Leave a Comment

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