Kepraktisan cookie sesi punya sisi gelapnya. Begitu pengguna login, browser secara otomatis melampirkan cookie sesi pada setiap request ke aplikasi kita — termasuk request yang dipicu oleh halaman web orang lain. Celah inilah yang dieksploitasi serangan Cross-Site Request Forgery (CSRF): situs jahat “meminjam” kredensial pengguna kita tanpa pernah mencuri passwordnya. Perlindungan Laravel berdiri di satu directive kecil — @csrf — dan chapter ini menguji pertarungannya secara nyata: form tanpa token ditolak dengan status 419, form dengan token valid lolos, lengkap dengan pengecualian route dan perilaku route API yang stateless.

Anatomi Serangan CSRF #

Bayangkan aplikasi keuangan dengan route POST /transfer. Pengguna Budi sedang login di aplikasi itu, lalu tanpa sengaja membuka halaman di situs lain — misalnya jahat.example. Halaman tersebut berisi form yang diam-diam menargetkan aplikasi kita:

html
<!-- https://jahat.example/menang-hadiah.html -->
<form action="http://aplikasi-keuangan.test/transfer" method="POST" id="form-jahat">
    <input type="hidden" name="rekening" value="987654321">
    <input type="hidden" name="jumlah" value="5000000">
</form>
<script>document.getElementById('form-jahat').submit();</script>

Saat script menjalankan submit(), browser mengirim POST /transfer beserta cookie sesi Budi — karena memang setiap request ke domain itu membawa cookie. Dari sisi server, request ini tampak sah: ada cookie, ada session, ada data transfer. Server tidak bisa membedakan form mana yang benar-benar diisi Budi di aplikasi kita dari form yang direkayasa situs lain.

Cara Kerja @csrf #

Solusinya sederhana: form resmi harus membawa rahasia yang tidak bisa diketahui situs lain. Laravel menyimpan token acak di session pengguna, dan directive @csrf mencetak token tersebut sebagai input tersembunyi di dalam form — persis seperti yang sudah kita lihat pada chapter Forms dan Old Input:

blade
<form method="POST" action="{{ route('tugas.simpan') }}">
    @csrf
    <input type="text" name="judul">
    <button type="submit">Simpan</button>
</form>

Saat form disubmit, middleware ValidateCsrfToken membandingkan nilai _token dari input dengan token di session. Kita jalankan pengujian langsung untuk membuktikan asal token tersebut:

text
=== token @csrf ber-asal dari session ===
token di form == token di session : true
driver session                   : database
kunci payload session            : _token, _previous, _flash
APP_KEY (enkripsi data) terpasang: true

Nilai pada input tersembunyi identik dengan session()->token() — kunci _token terlihat jelas di payload session. Perlu dicatat pembagian tugas di sini: token CSRF hidup di session, sedangkan APP_KEY tidak ikut menandatangani token. Peran APP_KEY adalah mengenkripsi data — terbukti dari kedua cookie laravel-session dan XSRF-TOKEN yang dikirim terenkripsi, bukan sebagai teks polos. Cookie XSRF-TOKEN sendiri disediakan agar request AJAX dari JavaScript dapat membalas token lewat header X-XSRF-TOKEN.

Uji Nyata: Token Hilang atau Salah #

Kita uji route POST /tugas dari chapter sebelumnya dengan kernel runner — pertama tanpa token, lalu dengan token yang diambil dari form, lalu dengan token palsu:

text
POST /tugas tanpa _token → HTTP 419
title halaman error: Page Expired

POST /tugas dengan token cocok → HTTP 302
Location: http://localhost/tugas/sukses

POST dengan token PALSU → HTTP 419

Token yang hilang maupun yang salah sama-sama berakhir di 419 Page Expired. Bandingkan dengan alur yang benar: GET /tugas/baru menaruh token di form sekaligus cookie sesi di browser, POST mengirim keduanya, middleware mencocokkan, request lanjut ke validasi dan akhirnya redirect 302 ke halaman sukses.

Pengecualian Route #

Sebagian kecil route menerima POST dari pihak ketiga yang tidak bisa membawa token — webhook payment gateway misalnya. Pengecualian didaftarkan di bootstrap/app.php, bukan lagi di class VerifyCsrfToken seperti era Laravel 10 ke bawah:

php
->withMiddleware(function (Middleware $middleware): void {
    $middleware->validateCsrfTokens(except: [
        'epay/webhook',
    ]);
})

Pengujian route POST /epay/webhook tanpa token menghasilkan HTTP 200 dengan body {"status":"diterima"} — pengecualian bekerja.

Route API dan Mode Stateless #

Aturan di atas hanya berlaku pada route web. Route di routes/api.php bersifat stateless — middleware group api tidak memuat session sama sekali, sehingga tidak ada token yang bisa dibandingkan. POST ke /api/lokasi tanpa token apa pun langsung berhasil:

text
POST /api/lokasi (route API, stateless) → HTTP 200
body: {"status":"ok"}

Keamanan API tidak bersandar pada session melainkan pada token autentikasi per-request — topik yang kita bahas pada section Laravel Sanctum . Detail lengkap mekanisme CSRF tersedia di dokumentasi CSRF .

Dengan form yang aman, satu kebiasaan lama masih mengganggu kerapian: script dan stylesheet yang menempel di banyak halaman. Blade menyediakan @push dan @stack untuk mengumpulkannya secara teratur — materi chapter berikutnya.