Howdy.
I have spent considerable time in making a simple AJAX interface which has CSRF throughout. I spent a fair amount of time trying to use the form helper but it was hijacking my button with a ‘post’, so I gave up decided to implement my own.
What I am looking for please, is any security weaknesses in my approach, as well as any observations on how it should be done with CakePHP methodology.
One thing that springs to mind is identifying the initial call of my controller - at the moment it is identified as ‘not ajax’!
You should be able to duplicate this project by creating a new app and putting in these 2 files: -
<!-- templates/Tests/simple.php -->
<h2>Simple AJAX</h2>
<label for="edit">Show me:</label>
<input type="text" id="edit" value="!pirt dnuor">
<button id="button" data-rel="<?= $this->Url->build(['_ext' => 'json']) ?>">Reverse text</button>
<div id="result-container" style="font-size: 200%;"></div>
<script src="//code.jquery.com/jquery-3.5.0.min.js"></script>
<script>
$(function() {
$('#button').click(function() {
$('#result-container').html('Pending...');
var targeturl = $(this).data('rel');
var data = 'csrfToken=<?= $csrfToken ?>&edit=' + $('#edit').val();
console.log(data);
$.ajax({
type: 'get',
url: targeturl,
data: data,
beforeSend: function(xhr) {
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
},
success: function(response) {
if (response.result) {
var result = response.result;
if (result.csrfToken !== '<?= $csrfToken ?>') //Admittedly if a hacker got this far
$('#result-container').html('Invalid CSRF Token, try refreshing the page'); // he has his data in result.edit anyway
else
$('#result-container').html(result.edit);
}
},
error: function(e) {
alert("An error occurred: " + e.responseText.message);
console.log(e);
}
});
});
});
</script>
and
<?php // src/Controller/TestController.php
declare(strict_types=1);
namespace App\Controller;
class TestsController extends AppController {
public function simple() {
$ss = $this->getRequest()->getSession(); //an alias only
$cc = 'tests_simple_csrf'; //session var unique to this controller + view
if (!$this->request->is(['ajax'])) { //prepare for first time (note the not!)
$csrfToken = bin2hex(random_bytes(24)); //create our token
$_serialize = ['csrfToken'];
$this->set(compact('csrfToken', '_serialize'));
$ss->write($cc, $csrfToken); // could there be race condition / session issues?
} else { //our ajax reply, check token & process data
$new_csrfToken = $this->request->getQuery('csrfToken');
$csrfToken = $ss->read($cc);
$return = __('Invalid CSRF Token, try refreshing the page');
if ($csrfToken === $new_csrfToken) //raise exception on else?
$return = strrev($this->request->getQuery('edit')); //the data we generate
$result = ['edit' => $return, 'csrfToken' => $csrfToken]; //pass back original token!
$_serialize = ['result'];
//If you wanted to be more secure you could pass a new token on
// this ajax return and use that for next request.
//But, if the current token has be acquired then the new one will be too.
$this->set(compact('result', '_serialize'));
}
}
}
and you must add this line to config/bootstrap.php
: -
Router::extensions(['json']);
The URL for this is /Tests/simple like http://localhost:8765/Tests/simple
Personally I change this line in config/routes.php
from
$builder->connect('/', ['controller' => 'Pages', 'action' => 'display', 'home']);
to
$builder->connect('/', ['controller' => 'Tests', 'action' => 'simple']);
and just use the domain home address; eg. http://localhost:8765 .
I hope this snippet of code can help others too with CakePHP 4 & AJAX as it took me a long time to get it to work. The demonstrations at https://sandbox.dereuromark.de/sandbox/ajax-examples/simple did help, but its near impossible to extract a stand-alone app from it. They rely on several other plugins and the code needs to be exorcised from the sandbox plugin. (It took many, many hours to realise that https://github.com/dereuromark/cakephp-sandbox/blob/master/plugins/Sandbox/src/Controller/AjaxExamplesController.php#L48 FORMAT_DB_DATETIME is not a PHP value, and came from a toolbox - which was killing the AJAX but not showing errors! Also, the ‘simple’ test requires this script https://sandbox.dereuromark.de/js/cjs/js-combined.v1585517020.js yea…!)
So please, let me know if my code is the wrong approach to our methodology here, or the security is flawed. For instance I am not using X-CSRF-Token headers, should I be? If so, how?!?
Cheers
Jonathan