CSRF (Cross-Site Request Forgery, "pemalsuan request lintas situs") adalah serangan yang bikin browser korban mengirim request ke situs tempat dia sudah login, tanpa dia sadari. Kuncinya: browser otomatis menempelkan cookie ke setiap request ke situs itu, siapa pun yang menyuruhnya. Bayangin kurir langganan yang selalu menempelkan kartu member-mu di setiap paket ke toko langganan, gak peduli siapa yang menitipkan paketnya. Orang asing cukup menitip paket berisi "transfer sepuluh juta", dan toko melihat kartu member-mu di situ.
🧪 Tebakan cepat: kamu login di
bank.example, dan cookie session-mu pakaiSameSite=Lax. Lalu kamu membukaevil.example, yang diam-diam mengirim form POST kebank.example/transfer. Cookie-mu ikut terkirim atau gak? Tebak dulu, jawabannya ada di bagian "SameSite: kapan cookie ikut terkirim".
Gimana serangannya terjadi
Di Lab, Budi login di bank.example dengan saldo Rp15.000.000. Server memberi cookie session=7f3a9c. Selama cookie itu ada, setiap request ke bank.example dianggap datang dari Budi.
Lalu Budi membuka evil.example dari link di email. Halaman itu menyuruh browser Budi mengirim form POST ke https://bank.example/transfer, tanpa Budi menekan apa pun. Form boleh dikirim ke situs mana pun, jadi browser menurut.
Yang tiba di server kalau cookie-nya boleh ikut:
POST /transfer HTTP/1.1
Host: bank.example
Origin: https://evil.example
Cookie: session=7f3a9c
to=hacker-account&amount=10000000
Server melihat session=7f3a9c, mengenali Budi, dan memindahkan Rp10.000.000. Saldo tinggal Rp5.000.000. Server gak bisa membedakan request ini dari klik Budi sendiri, karena dia cuma melihat cookie. Penyerang juga gak pernah melihat cookie-nya: browser yang menempelkannya.
CSRF butuh tiga syarat: korban sedang login dengan cookie, aksinya cukup satu request yang isinya bisa ditebak, dan server cuma mengandalkan cookie untuk tahu siapa pengirimnya.
SameSite: kapan cookie ikut terkirim
Atribut SameSite di cookie mengatur apakah cookie ikut di request yang datang dari situs lain (cross-site). Server memasangnya waktu mengirim cookie:
Set-Cookie: session=7f3a9c; Path=/; Secure; HttpOnly; SameSite=Lax
Request ke bank.example |
None |
Lax |
Strict |
|---|---|---|---|
Form POST dari evil.example |
ikut | gak ikut | gak ikut |
Klik link dari evil.example (GET, pindah halaman) |
ikut | ikut | gak ikut |
<img> atau fetch dari evil.example |
ikut | gak ikut | gak ikut |
Request dari bank.example sendiri |
ikut | ikut | ikut |
Jadi dengan Lax, form POST dari evil.example dikirim tanpa cookie. Server gak mengenali Budi, transfer gak terjadi. Itu jawaban tebakannya.
Laxcuma mengizinkan cookie ikut saat kamu pindah halaman lewat link (GET). Itu sebabnyaLaxjadi pilihan umum: link dari email tetap membuka akunmu dalam keadaan login.Strictlebih ketat: datang dari link situs lain pun, kamu muncul belum login.Noneberarti selalu ikut, dan wajib dipasangkan denganSecure.- Kalau
SameSitegak ditulis sama sekali, Chrome dan Edge memperlakukannya sebagaiLax, tapi gak semua browser begitu. Tulis sendiri.
CSRF token: rahasia yang gak bisa ditebak penyerang
SameSite saja belum cukup, karena aturannya beda di tiap browser. Pertahanan utama adalah CSRF token: string acak yang dibuat server untuk tiap session, ditaruh di setiap form, lalu dicocokkan saat form dikirim. Halaman evil.example gak bisa membaca isi halaman bank.example (aturan same-origin browser), jadi dia gak tahu token-nya.
Di Laravel, cukup tulis @csrf di form Blade:
<form method="POST" action="/transfer">
@csrf
<input name="to">
<input name="amount">
<button>Transfer</button>
</form>
@csrfberubah jadi<input type="hidden" name="_token" value="..." autocomplete="off">berisi token session itu (40 karakter acak; di Lab disingkatk9Xq2Lm7).- Middleware CSRF Laravel mengecek
_tokendi setiap request POST, PUT, PATCH dan DELETE. Request GET gak dicek. - Token gak cocok atau gak ada? Laravel menolak dengan status 419 Page Expired, sebelum controller jalan.
Form palsu di evil.example gak punya _token yang benar, jadi ditolak 419, walau cookie-nya ikut.
Memeriksa token di server
Ini inti pengecekannya, ditulis sebagai function biasa:
function validateCSRFToken(sessionToken, requestToken) {
if (!sessionToken || !requestToken) return false; // kosong, null, undefined: tolak
return sessionToken === requestToken; // harus sama persis
}
console.log(validateCSRFToken('abc123', 'abc123')); // true
console.log(validateCSRFToken('abc123', 'xyz789')); // false
console.log(validateCSRFToken('abc123', '')); // false
!sessionToken || !requestTokenmenolak string kosong,nulldanundefinedduluan. Tanpa baris ini,'' === ''hasilnyatrue, jadi dua token kosong malah lolos.===membandingkan dua string karakter per karakter.
| Panggilan | Hasil | Kenapa |
|---|---|---|
validateCSRFToken('abc123', 'abc123') |
true |
sama persis |
validateCSRFToken('abc123', 'xyz789') |
false |
beda |
validateCSRFToken('abc123', '') |
false |
token request kosong |
validateCSRFToken('abc123', null) |
false |
token request gak ada |
validateCSRFToken('', '') |
false |
dua-duanya kosong |
Laravel sendiri membandingkan dengan hash_equals, yang butuh waktu sama panjang entah token-nya cocok di huruf pertama atau terakhir, jadi waktunya gak membocorkan apa pun.
💡 Origin header dan API dengan Bearer token
Browser juga mengirim header Origin di request POST. Server bisa menolak request yang Origin-nya bukan https://bank.example, sebagai lapisan tambahan di samping token.
API yang login-nya pakai header Authorization: Bearer ... (bukan cookie) secara alami aman dari CSRF: browser gak pernah menempelkan header itu sendiri, jadi evil.example gak bisa memakainya. Begitu API-mu login pakai cookie, perlakukan sama seperti form: pasang token.
⚠️ Jebakan yang sering ketemu
- Form POST tanpa CSRF token.
<form method="POST" action="/transfer">tanpa@csrfitu rentan. Laravel 13 mungkin tetap meloloskan percobaanmu sendiri, karena middleware-nya juga percaya headerSec-Fetch-Site: same-origindari browser modern, tapi browser lama atau client lain kena 419. Jangan "perbaiki" 419 dengan mematikan middleware-nya, tambahkan@csrf. - Mengubah data lewat GET. Route seperti
GET /transfer?to=...lolos dari dua pertahanan:Laxtetap mengirim cookie saat link diklik, dan CSRF token gak dicek di GET. Aksi yang mengubah data harus POST, PUT, PATCH atau DELETE. - Cuma pakai
===.validateCSRFToken('', '')jaditrue. Tolak nilai kosong dulu. - Mengira CORS melindungi. CORS cuma mengatur siapa yang boleh membaca response. Form POST dari situs lain tetap terkirim dan tetap dijalankan server.
TL;DR: CSRF (Cross-Site Request Forgery) memanfaatkan browser yang otomatis menempelkan cookie: halaman evil.example menyuruh browser korban mengirim POST /transfer ke bank.example, dan server mengira itu korban. SameSite=Lax menahan cookie di form POST dan <img> dari situs lain, tapi tetap mengirimnya saat link diklik; Strict menahan semuanya; None selalu mengirim. Pertahanan utama adalah CSRF token: di Laravel @csrf menaruh _token di form, dan token yang gak cocok ditolak dengan 419. Pengecekannya: tolak token kosong, null atau undefined dulu, lalu bandingkan dengan ===. Jangan pernah mengubah data lewat GET, dan form POST tanpa CSRF token itu rentan.