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:
<!-- 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:
<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:
=== 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: trueNilai 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:
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 419Token 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:
->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:
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.