CodeIgniter Forums

Full Version: POST via Ajax returns 403 with CSRF enabled
You're currently viewing a stripped down version of our content. View the full version with proper formatting.
Hello Community,

I've deployed the latest Codeigniter 4.0.2 version and I have an issue when submitting a form with Post method via Ajax when CSRF is enabled, I've tried my best to figure out what is wrong but still no luck.

Notice that:
- When CSRF is disabled, the Ajax call is successful
- If I don't use Ajax and I enable CSRF, the controller handles correctly the request with 200 code

Allow me to share with you the details:

- CSRF enabled globally in the file App\Config\Filters.php:
Code:
// Always applied before every request
public $globals = [
    'before' => [
        //'honeypot'
        'csrf',
    ],
    'after'  => [
        'toolbar',
        //'honeypot'
    ],
];

- CSRF configuration:
Code:
public $CSRFTokenName  = 'csrf_token';
public $CSRFHeaderName = 'X-CSRF-TOKEN';
public $CSRFCookieName = 'csrf_cookie';
public $CSRFExpire    = 1200;
public $CSRFRegenerate = true;  // It fails with false and true
public $CSRFRedirect  = false;

- A simple contact form with the CSRF hidden input created manually:
Code:
<form name='contactForm' class="contactForm" id='contactForm'>
<input type="hidden" name="<?= csrf_token() ?>" value="<?= csrf_hash() ?>" id="msg_csrf"/>


- When submitting the form, the following Javascript code is being executed:
Code:
const apiUrl = '/api/contact';
const contactCSRF = document.getElementById('msg_csrf');
const contactCSRFName = [contactCSRF.getAttribute('name')];
const contactName = document.getElementById('msg_name');
const contactSubmit = document.getElementById('msg_submit');

contactSubmit.addEventListener("click", function(e){
    e.preventDefault();
    let contactFormData = {
        [ contactCSRFName ] : contactCSRF.value,
        msg_name : contactName.value
    };
    sendContactForm(contactFormData);
});

const sendContactForm = (contactFormData) => {
    $.ajax({
        type: 'POST',
        url: apiUrl,
        data: contactFormData,
        contentType: "application/json",
        headers: {'X-Requested-With': 'XMLHttpRequest'},
        dataType: 'json',
        success: function(result){
            console.log(result);
        },
        error: function(result){
            console.log(result.responseJSON);
        }
    })
}


- When accessing URL /api/contact, the following Controller handles the request:
Code:
<?php namespace App\Controllers;
class Contact extends BaseController
{
    public function index()
    {
        helper(['form', 'validation', 'url']);
        if ($this->request->isAJAX())
        {
            $formDataRaw = $this->request->getRawInput();
            return $this->response->setJSON($formDataRaw);
        } else {
            return '{ \'error\': \'Invalid Request\'}';
        }
    }
}

- Analyzing the Payload, it is correct:
Code:
csrf_token=563def7f2e22a4df1f6e53ce8f0b75d7&msg_name=jdoe

It matches with the cookie:
Code:
Cookie: csrf_cookie=563def7f2e22a4df1f6e53ce8f0b75d7


Mind that I've tried another way to pass the CSRF token, directly to the headers:
Code:
$.ajaxSetup({
    headers: { csrf_token : contactCSRF.value }
})


It is added correctly:
Code:
csrf_token: 563def7f2e22a4df1f6e53ce8f0b75d7


- In all my attempts, the same result, 403 error code:
Code:
code: 403
message: "The action you requested is not allowed."


What am I missing here? CSRF token name and CSRF hash are passed correctly to the controller but it keeps showing 403 error only when the request is performed via Ajax.

Could you please shed some light on this issue?


Thank you in advance for taking your time reading this.
When using csrf, a unique token is automatically regenerated for each page update (for HTML), but not for AJAX (as the page isn’t updated)... having a brief glance at your code, it does look like you’re passing the token correctly and also for every reply to the server, which may point to your reply being rejected AFTER the first use of the token (do you perhaps find you get one AJAX submission in, then it goes pear-shaped from the second one (?)).

From a comment in your code, you seem aware of one way around this being to set $CSRFRegenerate=FALSE in App.php, which will keep a single csrf token valid for the whole browser session (likely not quite as secure as having a new one generated each time... though it is an easy fix for the AJAX submission problem, and also when using the browser's navigate back button)… so as an initial suggestion, I’d suggest changing and leaving $CSRFRegenerate set to FALSE, which  would make it easier to get working initially (which then you could change later, if required).

As an aside, having been burnt a few times with trailing spaces in names and values, and although it probably is of no consequence here, as I'm paranoid, I’d normally encode:
Code:
name="<?= csrf_token() ?>" value="<?= csrf_hash() ?>" id="msg_csrf"/>
as:
Code:
name="<?=csrf_token();?>" value="<?=csrf_hash();?>" id="msg_csrf"/> <!-- semicolon added to be pedantically correct -->
(04-06-2020, 08:27 AM)Gary Wrote: [ -> ]When using csrf, a unique token is automatically regenerated for each page update (for HTML), but not for AJAX (as the page isn’t updated)... having a brief glance at your code, it does look like you’re passing the token correctly and also for every reply to the server, which may point to your reply being rejected AFTER the first use of the token (do you perhaps find you get one AJAX submission in, then it goes pear-shaped from the second one (?)).

From a comment in your code, you seem aware of one way around this being to set $CSRFRegenerate=FALSE in App.php, which will keep a single csrf token valid for the whole browser session (likely not quite as secure as having a new one generated each time... though it is an easy fix for the AJAX submission problem, and also when using the browser's navigate back button)… so as an initial suggestion, I’d suggest changing and leaving $CSRFRegenerate set to FALSE, which  would make it easier to get working initially (which then you could change later, if required).

As an aside, having been burnt a few times with trailing spaces in names and values, and although it probably is of no consequence here, as I'm paranoid, I’d normally encode:
Code:
name="<?= csrf_token() ?>" value="<?= csrf_hash() ?>" id="msg_csrf"/>
as:
Code:
name="<?=csrf_token();?>" value="<?=csrf_hash();?>" id="msg_csrf"/> <!-- semicolon added to be pedantically correct -->


Thank you Gary,

Note that:
- There is only one submission
- Yep, I'm aware of CSRFRegenerate, but the issue persists while setting it to false as well
- The semicolon didn't make a difference

Any other ideas? Might it be a bug?  Tongue I think you should be able to reproduce this issue with all the details that I've shared initially
Once you've checked the setting CSRFRegenerate is FALSE, it may be worth trying setting dataType: 'html' in your AJAX routine (?).

You could also try removing contentType: "application/json" to see whether that makes a difference.
(04-06-2020, 12:08 PM)Gary Wrote: [ -> ]Once you've checked the setting CSRFRegenerate is FALSE, it may be worth trying setting dataType: 'html' in your AJAX routine (?).

You could also try removing contentType: "application/json" to see whether that makes a difference.

Hi Gary,

The contentType was causing this malfunction... IMO it doesn't make any sense but at least now it's working, here the working Ajax function:
Code:
const sendContactForm = (contactFormData) => {
    $.ajax({
        type: 'POST',
        url: apiUrl,
        data: contactFormData,
        //contentType: "application/json", THIS WAS CAUSING THE MALFUNCTION
        headers: {'X-Requested-With': 'XMLHttpRequest'},
        dataType: 'json',
        success: function(result){
            console.log(result);
        },
        error: function(result){
            console.log(result.responseJSON);
        }
    })
}

Now that it works, could you please clarify if this is expected? Why the contentType was causing this? It should be included, don't you think?

Thanks
As far as I'm aware, contentType is a page header field.

I suspect when it arrives in your AJAX submission, it confuses things on the server side because it's not something that's expected.

I don't believe it's supposed to be included. data: is what is submitted to the server, and although there's two or three formats this data: can be, it is ultimately always converted to a query string before it is sent (so the server is only ever expecting this and not a programmer-spec'd contentType).
(04-06-2020, 03:16 PM)Gary Wrote: [ -> ]As far as I'm aware, contentType is a page header field.

I suspect when it arrives in your AJAX submission, it confuses things on the server side because it's not something that's expected.

I don't believe it's supposed to be included. data: is what is submitted to the server, and although there's two or three formats this data: can be, it is ultimately always converted to a query string before it is sent (so the server is only ever expecting this and not a programmer-spec'd contentType).


Yes, CI expects urlencoded, I would swear I did try x-www-form-urlencoded in one of my previous attempts... anyway, here it is the valid Ajax submission, it might help someone in the future:
Code:
const sendContactForm = (contactFormData) => {
    $.ajax({
        type: 'POST',
        url: apiUrl,
        data: contactFormData,
        contentType: 'application/x-www-form-urlencoded; charset=UTF-8',
        headers: {'X-Requested-With': 'XMLHttpRequest'},
        dataType: 'json',
        success: function(result){
            console.log(result);
        },
        error: function(result){
            console.log(result.responseJSON);
        }
    })
}


Thanks you!  Heart