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.

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:

html
<!-- 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:

php
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:

text
HTTP 200

Dua Lapis Pertahanan PreventRequestForgery #

Middleware PreventRequestForgery (pengganti ValidateCsrfToken yang kini deprecated) menjalankan pemeriksaan berurutan — request lolos begitu satu kondisi terpenuhi:

KondisiMaknanya
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:

text
GET /tugas/baru
panjang token di form            : 40 karakter
kunci payload session (database) : _token, _previous, _flash
token form == _token session     : true

Form 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:

text
POST /tugas tanpa _token : HTTP 419

Alur browser yang sah berjalan sebaliknya — GET mengambil token, lalu POST mengirimkannya kembali:

text
POST /tugas token cocok          : HTTP 200 — Tugas tersimpan
POST /tugas token PALSU          : HTTP 419

Token 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:

text
POST tanpa token, Sec-Fetch-Site: cross-site : HTTP 419
POST tanpa token, Sec-Fetch-Site: same-origin : HTTP 200 — Tugas tersimpan

Dua mode tambahan tersedia di bootstrap/app.php:

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: ...):

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

Pengujian nyata menunjukkan route tersebut menerima POST tanpa token:

text
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 .