Chapter CSRF Protection pada section View dan Blade
sudah memperkenalkan directive @csrf sebagai kebiasaan menulis form yang aman. Sekarang kita masuk ke sisi keamanannya secara penuh: bagaimana ancaman CSRF bekerja di balik layar, mengapa HTTP verb tertentu tidak pernah diperiksa, dan bagaimana middleware PreventRequestForgery Laravel 13 menjalankan verifikasi dua lapis — kombinasi antara origin verification via header Sec-Fetch-Site dan token berbasis session. Kita akan membuktikan setiap klaim dengan simulasi serangan nyata: form palsu ditolak dengan status 419, request dengan origin sah lolos, dan pengecualian route dibuktikan berisiko.
Model Ancaman: Cookie yang Terlalu Patuh #
Inti serangan Cross-Site Request Forgery (CSRF) ada pada perilaku browser yang menempelkan cookie sesi secara otomatis ke setiap request yang menuju aplikasi kita — tidak peduli request itu dipicu oleh halaman mana. Setelah Budi login, browser menyimpan cookie sesi. Ketika Budi kemudian membuka halaman di jahat.example, halaman itu bisa mengirim request ke aplikasi kita tanpa pernah tahu password Budi:
<!-- https://jahat.example/hadiah.html -->
<form action="https://aplikasi-kita.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 submit() berjalan, browser melampirkan cookie sesi Budi pada POST /transfer. Dari sisi server, request tampak sepenuhnya sah: cookie valid, session aktif, data lengkap. Inilah mengapa pertahanan CSRF tidak bisa mengandalkan autentikasi cookie saja — server butuh bukti bahwa request benar-benar berasal dari halaman aplikasi kita sendiri.
Mengapa GET Harus Tetap Aman #
Middleware CSRF sengaja membiarkan verb read-only (GET, HEAD, OPTIONS) lolos tanpa pemeriksaan apa pun. Konsekuensinya: request GET yang mengubah state adalah lubang keamanan, karena penyerang bisa memicu GET lintas situs cukup dengan tag <img src="https://aplikasi-kita.test/tugas/5/hapus">. Aturan GET harus selalu aman dan tanpa efek samping bukan formalitas — penyerang tidak butuh form untuk menembakkan GET. Kita buktikan bahwa verb read-only memang lolos meski header menyatakan cross-site:
use Illuminate\Http\Request;
$req = Request::create('/tugas/baru', 'GET');
$req->headers->set('Sec-Fetch-Site', 'cross-site');
$res = $kernel->handle($req);
echo 'HTTP '.$res->getStatusCode()."\n";Output yang Dihasilkan:
HTTP 200Dua Lapis Pertahanan PreventRequestForgery #
Middleware PreventRequestForgery (pengganti ValidateCsrfToken yang kini deprecated) menjalankan pemeriksaan berurutan — request lolos begitu satu kondisi terpenuhi:
| Kondisi | Maknanya |
|---|---|
isReading() | Verb GET/HEAD/OPTIONS tidak pernah diperiksa |
runningUnitTests() | Feature test otomatis lolos agar bisa menguji tanpa token |
inExceptArray() | Route terdaftar di daftar pengecualian |
hasValidOrigin() | Header Sec-Fetch-Site: same-origin — baru di Laravel 13 |
tokensMatch() | Input _token cocok dengan token di session |
Lapis token tetap menjadi fondasi. Saat form dirender, Laravel menyimpan token acak 40 karakter di session dan directive @csrf mencetaknya sebagai input tersembunyi. Saat form disubmit, middleware membandingkan keduanya dengan hash_equals() — perbandingan yang tahan timing attack. Kita buktikan ikatan token-session ini dengan membaca payload session langsung dari tabel sessions:
GET /tugas/baru
panjang token di form : 40 karakter
kunci payload session (database) : _token, _previous, _flash
token form == _token session : trueForm dan session berbagi token yang sama persis — situs jahat yang tidak bisa membaca session Budi tidak akan pernah bisa menebaknya. Selain input _token, middleware juga menerima header X-CSRF-TOKEN serta X-XSRF-TOKEN (versi terenkripsi untuk request AJAX).
Simulasi Serangan: 419 bagi Form Palsu #
Sekarang giliran penyerang. Form palsu dari jahat.example tidak punya token dan tidak punya akses ke session:
POST /tugas tanpa _token : HTTP 419Alur browser yang sah berjalan sebaliknya — GET mengambil token, lalu POST mengirimkannya kembali:
POST /tugas token cocok : HTTP 200 — Tugas tersimpan
POST /tugas token PALSU : HTTP 419Token yang hilang maupun salah sama-sama berakhir 419 Page Expired — bukti asal-usul request tidak ada, middleware menolaknya.
Verifikasi Origin: Fitur Baru Laravel 13 #
Browser modern mengirim header Sec-Fetch-Site pada setiap request untuk menyatakan asal request: same-origin, same-site, atau cross-site. Middleware memercayai deklarasi ini — jika bernilai same-origin, request langsung lolos tanpa pemeriksaan token sama sekali. Jika header tidak ada (browser lama, koneksi non-HTTPS) atau menyatakan cross-site, middleware kembali ke lapis token:
POST tanpa token, Sec-Fetch-Site: cross-site : HTTP 419
POST tanpa token, Sec-Fetch-Site: same-origin : HTTP 200 — Tugas tersimpanDua mode tambahan tersedia di bootstrap/app.php:
->withMiddleware(function (Middleware $middleware): void {
// andalkan origin sepenuhnya: gagal origin = HTTP 403
$middleware->preventRequestForgery(originOnly: true);
// terima juga subdomain: dashboard.example.com -> example.com
$middleware->preventRequestForgery(allowSameSite: true);
})Mode originOnly: true mematikan lapis token sepenuhnya; request yang gagal verifikasi origin menerima 403 alih-alih 419. Perhatikan bahwa Sec-Fetch-Site hanya dikirim browser lewat koneksi aman (HTTPS) — pada aplikasi non-HTTPS, middleware otomatis kembali ke validasi token. Detail lengkapnya ada di dokumentasi CSRF
.
Pengecualian Route dan Bahayanya #
Sebagian kecil route menerima POST dari pihak eksternal yang tidak bisa membawa token — webhook payment gateway misalnya. Pengecualian didaftarkan lewat preventRequestForgery(except: ...):
->withMiddleware(function (Middleware $middleware): void {
$middleware->preventRequestForgery(except: [
'epay/webhook',
]);
})Pengujian nyata menunjukkan route tersebut menerima POST tanpa token:
POST /epay/webhook tanpa token : HTTP 200 — {"status":"diterima"}Route API berbeda kasus: group api bersifat stateless, tidak memuat session, sehingga tidak ada token untuk dibandingkan. POST /api/lokasi tanpa token apa pun langsung berhasil dengan HTTP 200 — keamanan API bersandar pada token autentikasi per-request seperti yang kita bahas di section Laravel Sanctum
. Sebagai lapisan ketiga, konfigurasi cookie sesi di config/session.php mengirim same_site dengan nilai default lax, yang membuat browser modern menolak mengirim cookie pada POST lintas situs — pertahanan di sisi klien.
Perlindungan request kini berlapis. Ancaman berikutnya tidak lagi memalsukan request pengguna, melainkan menyuntikkan perintah SQL ke query aplikasi — kita bongkar di chapter SQL Injection Prevention .