Sejauh ini kita membahas penyerang yang menyuntikkan konten ke aplikasi kita. Cross-Site Request Forgery (CSRF) bekerja dari arah sebaliknya: penyerang tidak perlu menyentuh aplikasi kita sama sekali — cukup membuat browser korban yang sedang login mengirim permintaan atas nama korban. Browser selalu melampirkan cookie aplikasi kita pada setiap permintaan, apa pun asal halamannya, dan di situlah pintu masuknya. Pada chapter ini kita bedah anatomi serangan CSRF, alasan cookie attribute SameSite meredam sebagian besar serangan, dan kerangka pertahanan token yang menuntaskan sisanya.
Anatomi Serangan CSRF #
Bayangkan Budi masih login di bank.example lalu tanpa sengaja membuka halaman promosi di evil.example. Halaman jahat tersebut berisi form yang otomatis mengirim POST ke endpoint transfer di bank.example:
<!-- Halaman di evil.example - form tak terlihat, terkirim otomatis -->
<form action="https://bank.example/transfer" method="POST" hidden>
<input type="hidden" name="rekeningTujuan" value="9999-penyerang">
<input type="hidden" name="jumlah" value="5000000">
</form>
<script>document.forms[0].submit();</script>Form HTML boleh menunjuk ke domain mana pun — tidak ada larangan cross-origin untuk mengirim permintaan. Ketika form terkirim, browser melampirkan cookie bank.example milik Budi karena URL tujuannya memang milik bank.example. Dari sisi server, permintaan ini membawa session Budi yang sah, datang lewat POST dengan parameter lengkap, sehingga aplikasi tidak punya alasan untuk curiga. Alur lengkapnya terlihat pada diagram berikut:
Titik lemahnya bukan di form, melainkan di asumsi aplikasi: “permintaan yang membawa cookie session pasti disadari pemiliknya”. Asumsi itu keliru karena cookie terkirim secara otomatis oleh browser, termasuk saat permintaan dipicu halaman milik orang lain. Perlu dicatat bahwa CORS bukan pertahanan di sini — CORS mengatur apakah JavaScript halaman lain boleh membaca respons kita; permintaan POST sederhana tetap terkirim dan dieksekusi meskipun responsnya tidak bisa dibaca penyerang.
Mengapa SameSite Meredam Serangan #
Sejak 2020-an browser menerapkan atribut cookie SameSite yang membatasi kapan cookie boleh ikut pada permintaan lintas situs. Atribut ini mengubah peta CSRF secara drastis karena form jahat di evil.example menjadi tidak membawa cookie bank.example sama sekali:
| Nilai | Cookie ikut ketika… | Efek terhadap CSRF |
|---|---|---|
Lax | navigasi level atas (klik tautan, GET) | POST lintas situs tidak membawa cookie — form jahat mati |
Strict | tidak pernah untuk permintaan lintas situs | paling ketat, tetapi menimbulkan UX aneh: klik tautan dari email membuat user “logout” |
None | semua permintaan lintas situs | tidak melindungi apa pun; wajib dipasangkan Secure |
Nilai Lax menjadi bawaan browser modern ketika atribut tidak ditulis, sehingga sebagian besar serangan form POST otomatis teredam. Aplikasi tetap tidak boleh berhenti di sini. Browser lama mengabaikan atribut ini, dan permintaan GET lintas situs tetap membawa cookie pada mode Lax — celah yang fatal bila ada endpoint yang mengubah state lewat GET, misalnya dipicu lewat tag gambar:
<!-- Cukup satu tag gambar - permintaan GET terkirim saat halaman dirender -->
<img src="https://bank.example/transfer?tujuan=9999-penyerang&jumlah=5000000" width="0" height="0">Karena itu SameSite diperlakukan sebagai lapisan pertama, bukan pertahanan utama. Detail atribut cookie lainnya seperti HttpOnly dan Secure sudah kita bahas pada chapter Cookies
.
GET Wajib Aman dan Idempotent #
Serangan pada blok HTML terakhir menyingkap aturan desain yang sering dilanggar: GET harus safe dan idempotent — aman dijalankan dan hasilnya sama berapa pun kali diulang. GET hanya boleh membaca data; setiap operasi yang mengubah state wajib lewat POST, PUT, PATCH, atau DELETE. Kita bisa mengaudit definisi rute aplikasi dengan pemeriksa sederhana:
<?php
function metodeAman(string $metode): bool
{
return in_array(strtoupper($metode), ['GET', 'HEAD', 'OPTIONS'], true);
}
$rute = [
['GET', '/artikel', false],
['GET', '/artikel/42', false],
['GET', '/transfer', true],
['POST', '/transfer', true],
['PUT', '/profil/42', true],
['DELETE', '/artikel/42', true],
];
foreach ($rute as [$metode, $path, $mengubahState]) {
if ($mengubahState && metodeAman($metode)) {
echo "BAHAYA: $metode $path mengubah state lewat metode aman\n";
}
}
echo "Audit selesai\n";Output yang Dihasilkan:
BAHAYA: GET /transfer mengubah state lewat metode aman
Audit selesaiMari kita bedah logika auditnya:
metodeAman()memeriksa apakah sebuah metode HTTP termasuk metode “aman” menurut spesifikasi HTTP:GET,HEAD, danOPTIONStidak boleh punya efek samping.- Setiap rute dideklarasikan sebagai pasangan
[metode, path, mengubahState]; flag terakhir menyatakan apakah endpoint menulis data. - Kombinasi berbahaya terdeteksi ketika endpoint mengubah state tetapi metodenya aman — persis kondisi yang bisa dieksploitasi lewat tag
<img>di halaman mana pun. - Hasil audit menunjuk satu rute bermasalah:
GET /transfer. Perbaikannya bukan menambah pemeriksaan, melainkan memindahkan endpoint kePOST— memperbaiki desain, bukan menambal gejala.
Aturan ini juga menutup celah lain: URL berparameter state-changing tersebar di riwayat browser, log server, dan header Referer — semua tempat yang tidak kita kendalikan.
Kerangka Pertahanan Token #
Pertahanan utama CSRF adalah token rahasia yang tidak mungkin diketahui situs lain: nilai acak per session yang disisipkan di setiap form dan diverifikasi ulang di server. Situs jahat bisa memaksa browser mengirim permintaan, tetapi tidak bisa membaca isi halaman aplikasi kita karena Same-Origin Policy — jadi token tidak bisa diselundupkan ke form palsu. Simulasi berikut memperlihatkan inti pemeriksaannya:
<?php
$session = ['user_id' => 42, 'csrf_token' => 'k3unci-r4hasia-server'];
$permintaanSungguhan = ['ke' => 'BCA-9988', 'jumlah' => 150000, 'csrf_token' => 'k3unci-r4hasia-server'];
$permintaanPalsu = ['ke' => 'BCA-7777', 'jumlah' => 99000000, 'csrf_token' => null];
function permintaanDiizinkan(array $session, array $permintaan): bool
{
$tokenSession = $session['csrf_token'] ?? null;
$tokenForm = $permintaan['csrf_token'] ?? null;
return $tokenForm !== null && hash_equals($tokenSession, $tokenForm);
}
var_export(permintaanDiizinkan($session, $permintaanSungguhan)); echo "\n";
var_export(permintaanDiizinkan($session, $permintaanPalsu)); echo "\n";Output yang Dihasilkan:
true
falseDua keputusan pada fungsi di atas sengaja diambil: perbandingan memakai hash_equals() agar waktu pemeriksaan tidak membocorkan isi token, dan permintaan tanpa token ditolak tanpa kecuali. Form jahat dari evil.example selalu jatuh pada kategori kedua — penyerang bisa menyalin struktur form, tetapi tidak pernah bisa menyalin nilainya. Mekanisme lengkapnya — bagaimana token diterbitkan, disimpan, dan diperbarui, termasuk pola per-form token — dibedah tuntas pada chapter CSRF Tokens
, sementara posisinya dalam pertahanan form secara umum ada di Form Security
.
Ringkasan #
CSRF mengeksploitasi kepercayaan aplikasi pada cookie session: browser mengirim cookie secara otomatis, termasuk untuk permintaan yang dipicu halaman pihak lain. Pertahanannya berlapis: SameSite memutus pengiriman cookie lintas situs, disiplin metode HTTP memastikan GET tidak pernah mengubah state, dan token rahasia menjadi bukti bahwa permintaan benar-benar lahir dari form milik aplikasi kita. Dengan aksi pengguna kini terlindungi, kita berpindah ke target favorit berikutnya penyerang: database — pada chapter SQL Injection kita akan melihat bagaimana satu string input bisa membongkar seluruh isi tabel.