Skip to content Skip to sidebar Skip to footer

Mengatasi CSRF AJAX Error 403 Forbidden di CodeIgniter

Ketika menggunakan fitur CSRF di CodeIgniter, maka setiap kali kita merefresh atau selesai melakukan aksi di form, token CSRF tersebut akan berubah dan dicek setiap kali kita melakukan aksi di form tersebut. Jadi kita tidak bisa sembarang melakukan reload pada form tersebut.

Karena alasan keamanan, pada dasarnya CSRF ini berguna agar aplikasi yang diakses oleh pengguna benar-benar berasal dari aplikasi sumbernya, bukan dari sembarang aplikasi atau istilahnya "cloning app/form".

Namun, masalah muncul saat saya menggunakan fitur CSRF ini pada AJAX. Dikarenakan CSRF harus dibawa terus ketika kita submit form, jadi hasilnya saya mengalami error 403 forbidden berkepanjangan yang hampir membuat saya frustrasi haha.

Solusi:

Berbagai solusi saya cari mulai dari blog ke blog hingga situs sekelas Stackoverflow pun, masih belum bisa mengatasi masalah ini.

Karena solusi-solusinya hanya bisa untuk satu kali request aja, jadi ketika saya mencoba request berikutnya masih 403 forbidden.

Akhirnya, masalah solved/teratasi saat saya menggunakan codingan dari masrud.com. Masrud sudah tak asing lagi bagi saya, beberapa aplikasinya sering saya gunakan sebagai referensi terutama untuk CI.

Awalnya saya penasaran kok bisa codingan dia berhasil ketika menggunakan AJAX untuk login dan fitur CSRFnya aktif. Usut punya usut, saya ulik lagi codingan dia dan akhirnya menemukan "hidden gem" bisa saya katakan seperti itu. Bagaimana tidak, kok bisa-bisanya dia meletakkan coding full JavaScript di file jquery?

Haha saya juga tak habis pikir, jadi sebenarnya codingannya itu berasal dari library jquery-cookie, kalau mau download/menggunakan library JS ini bisa di link berikut.

Plugin jquery-cookie ini berguna untuk membaca, menulis, dan menghapus hal-hal yang berkaitan dengan cookie pada browser.

Jadi, karena konsepnya CSRF di CodeIgniter ini harus dibawa setiap kali kita melakukan request dengan method POST, kita hanya simpel menggunakan fungsi dari jquery-cookie ini untuk membaca cookie dari CSRF tersebut.

  • Pertama kita includekan dulu library jquery-cookienya:
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.min.js" integrity="sha512-3j3VU6WC5rPQB4Ld1jnLV7Kd5xr+cq9avvhwqzbH/taCRNURoeEpoPBK9pDyeukwSxwRPJ8fDgvYXd6SkaZ2TA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
  • Kemudian, berikut ini contoh codenya:
$.ajax({
    url: "url_yg_ingin_dikirim_datanya",
    method: "POST",
    dataType: "html",
    data: {
        id: id,
        csrf_test_name: $.cookie('csrf_cookie_name') // ini fungsi dari jquery-cookie
    },
    beforeSend: function() {
        // biasanya tampilkan preloader
    },
    success: function(data) {
        // output yang ingin ditampilkan kalau sukses
    },
    error: function(xhr, textStatus, error) {
        console.log(xhr.statusText);
        console.log(textStatus);
        console.log(error);
    },
    complete: function() {
        // jika selesai, jangan tampilkan preloadernya
    }
});

csrf_test_name dan csrf_cookie_name disesuaikan dengan nama token dan cookie CSRF di application/config/config.php pada CodeIgniter. Biasanya seperti ini:

$config['csrf_protection'] = TRUE;
$config['csrf_token_name'] = 'csrf_test_name';
$config['csrf_cookie_name'] = 'csrf_cookie_name';
$config['csrf_expire'] = 7200;
$config['csrf_regenerate'] = TRUE;
$config['csrf_exclude_uris'] = array();

Penasaran dengan codingannya Masrud? sebenarnya sama aja dengan jquery-cookie yang ada di CDNJS, tapi yang ini versi fullnya.

(function (factory) {
if (typeof define === 'function' && define.amd) {
    // AMD (Register as an anonymous module)
    define(['jquery'], factory);
} else if (typeof exports === 'object') {
    // Node/CommonJS
    module.exports = factory(require('jquery'));
} else {
    // Browser globals
    factory(jQuery);
}
}(function ($) {

var pluses = /\+/g;

function encode(s) {
    return config.raw ? s : encodeURIComponent(s);
}

function decode(s) {
    return config.raw ? s : decodeURIComponent(s);
}

function stringifyCookieValue(value) {
    return encode(config.json ? JSON.stringify(value) : String(value));
}

function parseCookieValue(s) {
    if (s.indexOf('"') === 0) {
        // This is a quoted cookie as according to RFC2068, unescape...
        s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
    }

    try {
        // Replace server-side written pluses with spaces.
        // If we can't decode the cookie, ignore it, it's unusable.
        // If we can't parse the cookie, ignore it, it's unusable.
        s = decodeURIComponent(s.replace(pluses, ' '));
        return config.json ? JSON.parse(s) : s;
    } catch(e) {}
}

function read(s, converter) {
    var value = config.raw ? s : parseCookieValue(s);
    return $.isFunction(converter) ? converter(value) : value;
}

var config = $.cookie = function (key, value, options) {

    // Write

    if (arguments.length > 1 && !$.isFunction(value)) {
        options = $.extend({}, config.defaults, options);

        if (typeof options.expires === 'number') {
            var days = options.expires, t = options.expires = new Date();
            t.setMilliseconds(t.getMilliseconds() + days * 864e+5);
        }

        return (document.cookie = [
            encode(key), '=', stringifyCookieValue(value),
            options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
            options.path    ? '; path=' + options.path : '',
            options.domain  ? '; domain=' + options.domain : '',
            options.secure  ? '; secure' : ''
        ].join(''));
    }

    // Read

    var result = key ? undefined : {},
        // To prevent the for loop in the first place assign an empty array
        // in case there are no cookies at all. Also prevents odd result when
        // calling $.cookie().
        cookies = document.cookie ? document.cookie.split('; ') : [],
        i = 0,
        l = cookies.length;

    for (; i < l; i++) {
        var parts = cookies[i].split('='),
            name = decode(parts.shift()),
            cookie = parts.join('=');

        if (key === name) {
            // If second argument (value) is a function it's a converter...
            result = read(cookie, value);
            break;
        }

        // Prevent storing a cookie that we couldn't decode.
        if (!key && (cookie = read(cookie)) !== undefined) {
            result[name] = cookie;
        }
    }

    return result;
};

config.defaults = {};

$.removeCookie = function (key, options) {
    // Must not alter options, thus extending a fresh object...
    $.cookie(key, '', $.extend({}, options, { expires: -1 }));
    return !$.cookie(key);
};

}));

Penutup:

Saya sangat berterima kasih kepada Masrud, mungkin kalau bukan dari dia entah saya harus mencari kemana lagi untuk mengatasi solusi ini ðŸ˜Š

Rinaldi Pratama Putra
Rinaldi Pratama Putra Reality is a lovely place, but I wouldn't wanna live there.