Strong Cross-Site Request Forgery Prevention (CSRF) for PHP and jQuery

Strong Cross-Site Request Forgery Prevention (CSRF) for PHP and jQuery

A few days ago, we have recently noticed some strange access to one of our customer portals, some kind of direct external access to a specific account, nothing dangerous for customers' data, but just annoying, we never like direct access especially when it should be already prevented.

It was not a hack per se but only a clever customer that created a bot using Python to be able to download his daily consumption data in a .csv file (something you can do if you use the portal). The creator of the bot found the PHP /login endpoint in charge of login validation and he succeeded to submit his real creds through AJAX request. Next, he talked to another PHP endpoint to grab a dump a .csv of his data. He understood how we exchange data between PHP scripts, the random token protection implemented and how to get around all other protections.

Nothing illegal here as the bot was never able to access other customers' data and needed to go through all the login processes to access other legitim functions. But what was annoying to us is that it was possible to pilot AJAX requests through PHP /login endpoint which should already have been more than protected against CSRF or XSS attacks. At this time we used most of the well-known conventional methods you have surely already found thousands of times in a bunch of forums and probably implemented in your very own scripts:

  • XSS protection by using a random csrf-token generation send through the AJAX form and requests

  • Direct access prevention by validating the HTTP_REFERER

  • Validation if it is an AJAX request (isXmlHttpRequest)

  • Validation of the interface between the web server and PHP (php_sapi_name)

  • Cross-Origin Ressource Sharing (CORS)

I do know the creator of the bot and we did exchange, he helped us find a way to prevent this access and in exchange, he is allowed to use the REST API to download his daily data.

How to protect your CRSF token against unwanted direct access

Our mistake was using a Synchronised Token Pattern to submit the CRSF token back to the server as a hidden field on a form submission. This created a vulnerable form and the bot was able to load the login page, parse for the field (even if we choose random names for the field) and get the values needed before the login phase.

As this specific portal is a tailor-made solution that mostly is made of custom code, we didn't use vendor protection at this point and choose to implement our specific protections. So we did need to understand how to be really protected against these types of attacks.

The best way we found is to still use a CSRF token, a token generated by our server and provided to the client in some way. We will need to put the CSRF token in a non-cookie header whenever making a POST request to our backend. The bot won't be able to create this specific header and could no longer be successful just by posting data to the /loginendpoint.

Image from Nick Scialli

So we needed a way to add the anti-csrf-token header to our POST requests. JQuery proposes an API called $.ajaxSetup()which can be used to do exactly that.

Scripts examples

Here is an example of a PHP login page implementing the JQuery API $ajaxSetup()used to add an anti-crsf-token header to the AJAX request. It configures jQuery to automatically add the token to all request headers.

To generate a real randomized token in this example, we simply hash a password using Blowfish. Don't forget to add the token to the META tag in the HMTL HEADER and to store a verification value in the PHP session.

The function csrfFsafeMethod()will filter out the safe HTTP methods and only add the header to unsafe HTTP methods.

<?php
session_name('my_session');
session_start();

//CSRF-TOKEN Generation
$options = ['cost' => 8];
$_SESSION['X-XSRF-TOKEN'] = password_hash("<YOUR-PASSWORD>",PASSWORD_BCRYPT, $options);
?>
<head>
  <meta charset="UTF-8">
  <meta content="authenticity_token" name="csrf-param" />
  <meta content="<?php echo $_SESSION['X-XSRF-TOKEN'];?>" name="csrf-token" />
</head>
<body>
  <form id="form_login" method="POST" action="#">
    <input id="email" type="email">
    <input id="password" type="password">
    <button type="submit">Login</button>
  </form>
</body>
</html>


<!-- JQUERY-->
<script src="link-to-your-jquery/jquery.min.js"></script>
<script>
  var csrf_token = $('meta[name="csrf-token"]').attr('content');

function csrfSafeMethod(method) {
  // these HTTP methods do not require CSRF protection
  return (/^(GET|HEAD|OPTIONS)$/.test(method));
}

$.ajaxSetup({
  beforeSend: function(xhr, settings) {
  if (!csrfSafeMethod(settings.type) && !this.crossDomain)             {
xhr.setRequestHeader("anti-csrf-token", csrf_token);
}}});

$.ajax({
  type: "POST",
  url: 'endpoint.login.php',
  data: {
    'email': $('#email').val(),
    'password': $('#password').val(),
  },
  success: function (data) {
    if(data=='true'){
      //The user is loged in
      $(location).attr('href', 'index.php');
    }else{
      //Manage your errors
    }
  }
});
return false;
});
</script>

And the endpoint script is even easier. We can simply get the header and chek it against the session value if it matches everything is great and you can continue your process else you can kill everything.

<?php

//endpoint.login.php script

session_name('my_session');
session_start();

if(apache_request_headers()['anti-csrf-token']!=$_SESSION['X-XSRF-TOKEN']){
  echo "<script>console.log('AH AH AH YOU DIDN\'T SAY THE MAGIC WORD.');</script>";
  header("HTTP/1.1 403 Forbidden");
  session_destroy();
  die("<script>this.location='login.php'</script>");
}

Here are some interesting links about this subject you can find here :

Final word

I hope this example will help you. Do you have any experience implementing your very own CSRF protection? Do hesitate to share your comments.

Did you find this article valuable?

Support D Ʌ V I D ★ S Ξ N Ʌ T Ξ by becoming a sponsor. Any amount is appreciated!