How To Implement CSRF Token in PHP – Really Simple Example

Welcome to a quick tutorial on how to implement CSRF token protection in PHP. Working 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 this token thing is nothing more than a random string in the session.

The simplified steps to implementing a simple CSRF token protection are:

  1. Start the session and generate a random token.
    • session_start();
    • $_SESSION["token"] = bin2hex(random_bytes(32));
  2. Embed the CSRF 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 (!isset($_POST["token"]) || !isset($_SESSION["token"])) { exit(); }
    • if ($_POST["token"] == $_SESSION["token"]) { DO PROCESSING }

That’s all for the basics. But just what is CSRF? 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.

 

 

QUICK SLIDES

 

TABLE OF CONTENTS

Download & Notes What is CSRF? CSRF Token
Useful Bits & Links Tutorial Video The End

 

DOWNLOAD & NOTES

Firstly, here is the download link to the example 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

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.

 

 

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.

 

A SIMPLE EXAMPLE OF CSRF ATTACK

THE LEGIT SITE

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>

 

THE BAD SITE

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
<!-- 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 “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 “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.

 

STEP 1) INSERT RANDOM CSRF TOKEN INTO HTML FORM

1-generate-token.php
<?php 
// (A) GENERATE CSRF TOKEN (RANDOM STRING INTO SESSION)
session_start();
 
// (A1) FOR PHP 5
// 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);
 
// (A2) FOR PHP 7+
// CREDITS : https://stackoverflow.com/questions/6287903/how-to-properly-add-csrf-token-using-php
$_SESSION["token"] = bin2hex(random_bytes(32));
 
// (A3) TOKEN EXPIRY
$_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 your “regular HTML form”. But take extra note that a random string (token) has been generated and inserted into a hidden form field.

 

 

STEP 2) VERIFY THE TOKEN ON FORM SUBMISSION

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

// (B) CHECK IF TOKEN IS SET
if (!isset($_POST["token"]) || !isset($_SESSION["token"]) || !isset($_SESSION["token-expire"])) {
  exit("Token is not set!");
}
 
// (C) COUNTER CHECK SUBMITTED TOKEN AGAINST SESSION
if ($_SESSION["token"]==$_POST["token"]) {
  // (C1) EXPIRED
  if (time() >= $_SESSION["token-expire"]) {
    exit("Token expired. Please reload form.");
  }
  // (C2) OK - DO YOUR PROCESSING
  else {
    unset($_SESSION["token"]);
    unset($_SESSION["token-expire"]);
    echo "OK";
  }
}

// (D) 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 method="post" action="2-submit.php" onsubmit="return doAJAX()">
  <input type="hidden" id="token" value="<?=$_SESSION["token"]?>"/>
  <input type="email" id="email" value="jon@doe.com"/>
  <input type="submit" value="Go!"/>
</form>
 
<script>
function doAJAX(){
  // (C) GET FORM DATA + TOKEN
  var data = new FormData();
  data.append("token", document.getElementById("token").value);
  data.append("email", document.getElementById("email").value);
  // data.append(KEY, VALUE);
 
  // (D) AJAX
  var xhr = new XMLHttpRequest();
  xhr.open("POST", "2-submit.php");
  xhr.onload = function(){
    console.log(this.response);
  };
  xhr.send(data);
  return false;
}
</script> 

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

 

 

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

 

 TUTORIAL VIDEO

 

INFOGRAPHIC CHEAT SHEET

Simple CSRF Protection In PHP (click to enlarge)

 

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!

18 thoughts on “How To Implement CSRF Token in PHP – Really Simple Example”

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

  2. 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$')) { ... }

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

  4. 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 *