CodeIgniter Forums

Full Version: CSRF regenerate with AJAX
You're currently viewing a stripped down version of our content. View the full version with proper formatting.
So, I've been fiddling with CSRF.
I found a way of keeping the regenerate setting on, with AJAX, like this:
PHP Code:
if ($this->request->isAJAX())
{
    $formDataRaw $this->request->getRawInput();
    $formDataRaw['csrf_token'] = csrf_hash();
    return $this->response->setJSON($formDataRaw);
} else {
    return '{ \'error\': \'Invalid Request\'}';

and then in ajax doing this
Code:
bla bla bla....ajax form code submit.done(function (data) {
                console.log(data);
                csrf.val(data.csrf_token)
            })
Where csrf is a variable for this: let csrf = $("input[name='csrf_token']",form);
Basically I retrieve a new generated value for the csrf cookie hash and replace the old value with the new one retreieved - and that works.
BUT I have a whole bunch of forms on my page (image editing)! And THEIR csrf values all stay the same so they don't work now.
Can someone suggest a way I can pass this value on and change the csrf input values on ALL my forms??

Also, having read the docs I'm still wondering is the auto CSRF protection in CI 4 turned on ONLY in app/config/Filters.php - because the docs say that when form_open() is used it's added automatically - but its not.

Also I didn't see much difference made when turning app.CSRFProtection  = true in the .env
I've also been fiddlig with the CSRF filter recently, and provided 'csrf' is uncommented in $globals, it will be inserted automatically in every form CI generates.  For example, on my test server, <?php echo form_open();?> generates this:
Code:
<form action="https://pig.pen/" method="post" accept-charset="utf-8">
<input type="hidden" name="csrf_token_name" value="4e01754b75479ae926ed73ea2a6aa86b">

If one generates the form manually, it won't be inserted automatically (or if the form's action="..." references another site).

Although it comes down to personal preferences, I generally don't bother with settings in the .env for anything (primarily because all of these settings seem to be available elsewhere too, and I loose track of what's set where).

Provided your page is regenerated EVERY time there's a submission, the csrf values on all the forms will automatically be regenerated.  I found the headache comes when the page isn't regenerated- and you need a new csrf value for each of the (many) possible ajax submissions links on the (same, unupdated) page.  Setting $CSRFRegenerate = FALSE; makes this headache go away... though many believe that this compromises security enough to warrent going the extra 1 mile.

I've put a few other security mechanisms in place to mitigate the risk, and may come back to it later.  I suspect one easy way (if there are many possible ajax submisions from a (static) page) would be to have all the PHP functions that accept ajax submissions return a new csrf token in their reply, which the ajax success routine would then store as a global, to use in the next submission it (or perhaps a differnt ajax submission routine) makes.
I think I found a solution. Of course no page reloading. CSRF set to regenerate. A lot of forms on a single page. Please test, or point out if this may not work in some scenarios, if you have the time. Heres a simple, handy, dandy, vanilla javascript function that updates ALL present forms on the page with current csrf token:
Code:
function update_csrf_fields(value) {
    let all_forms = document.forms;
    for(e of all_forms) {
        e.querySelector('input[name=csrf_token]').value = value;
    }
}
Please note if your csrf_token is named something else - change the input name. Heres full example cycle:

Form submit using JQuery:
Code:
$(".form").submit(function(e) {
    e.preventDefault();
    let form = this;
    let choice = $(e.target).find("input[type=submit]:focus");
    if (choice) {
        if (choice[0].className === 'delete') {
            $.ajax({
                url: base_url + 'media/delete',
                type: 'POST',
                data: $(form).serializeArray(),
                dataType: 'json',
                headers: {'X-Requested-With': 'XMLHttpRequest'}
            }).done(function (data) {
                //show a message that I deleted the item, or do some fancy form.fadeOut() stuff
                update_csrf_fields(data.csrf_token); ////////TADA! This is where its AT!
            }).fail(function () {
                alert('Ajax Submit Failed ...');
            });
        }

        if (choice[0].className === 'save') {
            //a different submit button was pressed with save class
            //it will now do save actions with ajax
        }
    }
});
The controller ('media/delete'):

PHP Code:
public function delete()
{
        if ($this->request->isAJAX())
        {
            $formDataRaw $this->request->getRawInput();
            $formDataRaw['csrf_token'] = csrf_hash();
            return $this->response->setJSON($formDataRaw);
        } else {
            return '{ \'error\': \'Invalid Request\'}';
        }

Thx Leo! That's what I was looking for. Works great!
That looks, and sounds like it's ok...

My only additional observaton is that if all the submissions are done using ajax (which may be the case, seeing: $(form).serializeArray() in your code), then sticking copies of the same token in every form is wasteful in terms of processing time and effort - having the token as an ajax global, or if you really want it stuck into the DOM, having a single copy of it inserted (somewhere close to the top of the document) that is then extracted (and appended to the next post) by the ajax submission call would seem more efficient.
(04-15-2020, 12:09 PM)Gary Wrote: [ -> ]That looks, and sounds like it's ok...

My only additional observaton is that if all the submissions are done using ajax (which may be the case, seeing: $(form).serializeArray() in your code), then sticking copies of the same token in every form is wasteful in terms of processing time and effort - having the token as an ajax global, or if you really want it stuck into the DOM, having a single copy of it inserted (somewhere close to the top of the document) that is then extracted (and appended to the next post) by the ajax submission call would seem more efficient.
Naaah, it seems brutal, but its not wasteful with processing time - Javascript is client-side! Smile I think it can work with a hundred little forms no problem - but I never design sites with too many forms - this is for administration panel, editing and deleting photos. I tried to make it as plug-and-play as possible so I may use it in various sections of the admin panel.
Yeah!... screw those bastid users!
(04-15-2020, 12:30 PM)Leo Wrote: [ -> ]
(04-15-2020, 12:09 PM)Naaah, it seems brutal, but its not wasteful with processing time - Javascript is client-side!  I think it can work with a hundred little forms no problem - but I never design sites with too many forms - this is for administration panel, editing and deleting photos. I tried to make it as plug-and-play as possible so I may use it in various sections of the admin panel.What is the problem? After you createvar csrf = $(\.csrf_token').val(); Wrote: [ -> ]<input type="hidden" class="csrf_token" name="csrf_token" value="<?= csrf_hash(); ?>">
            $.ajax({url: '<?= base_url() ?>/NameController/namefunction',
                    type: 'POST',
                    data: { "<?= csrf_token(); ?>": csrf},
                    dataType: "json"
                }).done(function (data) {
                $('.csrf_token').val(data.csrf);
NameController
 namefunction(){
return json_encode(array('result' => 1, 'csrf'=> csrf_hash()));
}