0
votes

I have admin page with multiple settings, each setting have different form in different tab.

I am using ajax and to save data, and i didn't had any problems so far with csrf token when i had only one form on a page, or when i disable csrf token.

On each ajax request new token is generated in controller and sent back to ajax which is updating hidden field with name="csrf_token" but with different id's.

After first form is submitted all is good, but when i try to submit other form csrf token doesn't work anymore i am getting message "The action you have requested is not allowed." with page 403 in console output even after i reload page and try to submit other form that didn't worked.

Is there way to have multiple forms with csrf protection on same page and how to handle that ?

Here is code examples Forms with ajax

<form id="upload-icon" method="POST" enctype="multipart/form-data">
    <input type="hidden" name="csrf_token" id="csrf_token_1" value="<?php echo $this->security->get_csrf_hash(); ?>">
    <input type="file" id="favicon_image" name="favicon_image" accept=".ico">
    <button type="button" id="upload-icon-btn">Upload</button>
</form>

<form id="update-settings" method="POST">
    <input type="hidden" name="csrf_token" id="csrf_token_2" value="<?php echo $this->security->get_csrf_hash(); ?>">
    <input type="text" name="settings_one">
    <input type="text" name="settings_two">
    <input type="text" name="settings_three">
    <button type="button" id="update-settings-btn">Update settings</button>
</form>

<script>
$(document).ready(function() {
    var csrf_token = '';
    // upload favicon form
    $('#upload-favicon-form-btn').on('click', function(e) {
        e.preventDefault();

        var fd = new FormData();
        var files = $('#favicon_image')[0].files[0];
        fd.append('favicon_image', files);

        var favicon = $('#favicon_image').val();
        if (favicon == '') {
            loadModal('Warning', 'Please select <strong>favicon.ico</strong> icon file.');
        } else {
            $.ajax({
                type: 'POST',
                url: '<?php echo base_url('admin/settings/upload_ico'); ?>',
                data: fd,
                contentType: false,
                cache: false,
                processData: false,
                dataType: 'json',
                success: function(response) {
                    csrf_token = response.csrf_token;
                    $('#csrf_token_1').val(csrf_token);

                    // messages output
                },
                error: function() {
                    // error message output
                }
            });
        }
    });

    // update settings form
    $('#update-settings-btn').on('click', function(e) {
        e.preventDefault();
        $.ajax({
            type: 'POST',
            url: '<?php echo base_url('admin/settings/update_settings'); ?>',
            data: $('#update-settings').serialize(),
            dataType: 'json',
            success: function(response) {
                csrf_token = response.csrf_token;
                $('#csrf_token_2').val(csrf_token);

                // messages output
            },
            error: function() {
                // error message output
            }
        });
    });
});
</script>

Settings controller

public function update_settings()
{
    $csrf_token = $this->security->get_csrf_hash();

    $this->form_validation->set_rules('settings_one', 'Setting one', 'trim|required|xss_clean');
    $this->form_validation->set_rules('settings_two', 'Setting two', 'trim|required|xss_clean');
    $this->form_validation->set_rules('settings_three', 'Setting three', 'trim|required|xss_clean');

    if ($this->form_validation->run()) {
        if ($this->Settings_model->UpdateSettings($this->input->post('settings_one'), $this->input->post('settings_two'), $this->input->post('settings_three'))) {
            $data = array(
                'success' => true,
                'message' => 'Settings updated.',
                'csrf_token' => $csrf_token
            );
        } else {
            $data = array(
                'error' => true,
                'message' => 'Settings was not updated.',
                'csrf_token' => $csrf_token
            );
        }
    } else {
        $data = array(
            'error' => true,
            'settings_one_error' => form_error('settings_one'),
            'settings_two_error' => form_error('settings_two'),
            'settings_three_error' => form_error('settings_three'),
            'csrf_token' => $csrf_token
        );
    }

    echo json_encode($data);
}

public function upload_ico()
{
    $csrf_token = $this->security->get_csrf_hash();

    $favicon_upload_path = './upload/';

    if (isset($_FILES['favicon_image']['name'])) {
        $config['upload_path'] = $favicon_upload_path;
        $config['allowed_types'] = 'ico';

        $this->load->library('upload', $config);

        if (!$this->upload->do_upload('favicon_image')) {
            $data = array(
                'error' => true,
                'message' => $this->upload->display_errors(),
                'csrf_token' => $csrf_token
            );

        } else {
            $data = array(
                'success' => true,
                'message' => 'Favicon uploaded.',
                'csrf_token' => $csrf_token
            );
        }
    } else {
        $data = array(
            'error' => true,
            'message' => 'No file selected.',
            'csrf_token' => $csrf_token
        );
    }
    echo json_encode($data);
}

Config.php

$config['csrf_protection'] = TRUE;
$config['csrf_token_name'] = 'csrf_token';
$config['csrf_cookie_name'] = 'csrf_cookie_name';
$config['csrf_expire'] = 7200;
$config['csrf_regenerate'] = TRUE;
$config['csrf_exclude_uris'] = array(
    'admin/settings'
);
1

1 Answers

1
votes

I found issue.

I forgot to send token on first form, those 2 lines fix it

var token = $('#csrf_token_1').val();  // read token value from input
fd.append('csrf_token', token);

Then in script

<script>
$(document).ready(function() {
    var csrf_token = '';
    // upload favicon form
    $('#upload-favicon-form-btn').on('click', function(e) {
        e.preventDefault();

        var fd = new FormData();
        var files = $('#favicon_image')[0].files[0];
        fd.append('favicon_image', files);
        // fix for token that should be sent to controller over ajax
        var token = $('#csrf_token_1').val();  // read token value from input
        fd.append('csrf_token', token);        // append token value to data that need to be send to controller

and on each success in ajax it should update all forms id's with token returned from controller because i use $config['csrf_regenerate'] = TRUE; that is make new token on each request, so i made js function that updates token for all id's on all forms

// function that updates token on all forms
function updateToken(token) {
    $('#csrf_token_1, #csrf_token_2').val(token);
}

function in usage

success: function(response) {
    csrf_token = response.csrf_token;
    updateToken(csrf_token);
}

Maybe it's not best solution but works for me. If anyone have better one please feel free to post it.