CSRF & kerentanan web umum
Laravel menutup sebagian besar kerentanan klasik secara default. Yang berbahaya justru itu: perlindungan yang tidak terlihat gampang dimatikan oleh seseorang yang sedang buru-buru menghilangkan error 419.
Intisari
- CSRF: penyerang membuat browser korban mengirim permintaan ke situsmu memakai sesinya. Tokenlah penawarnya.
@csrfdi setiap form; rutewebdilindungi otomatis. Laravel 13 memperkuatnya lewatPreventRequestForgerydengan verifikasi origin.- 419 Page Expired berarti token hilang atau sesi kedaluwarsa โ jangan diperbaiki dengan mengecualikan rutenya.
- XSS:
{{ }}aman,{!! !!}tidak. SQL injection: query builder aman, string yang disambung tidak. - Rute API tidak butuh CSRF karena tidak memakai cookie โ asalkan ia benar-benar tidak memakai cookie.
Apa yang sebenarnya dicegah
Kamu sedang login di portal.test. Kamu membuka tab lain berisi situs jahat, yang diam-diam memuat:
<form method="POST" action="https://portal.test/artikel/12/hapus">
<script>document.forms[0].submit()</script>
Browser dengan patuh mengirim cookie sesimu, dan artikel terhapus. Token CSRF menutupnya: situs jahat tidak bisa
membaca nilai token milik portal.test, jadi permintaannya ditolak sebelum menyentuh controller.
<form method="POST" action="{{ route('artikel.hapus', $artikel) }}">
@csrf
@method('DELETE')
<button>Hapus</button>
</form>
{{-- Untuk JavaScript --}}
<meta name="csrf-token" content="{{ csrf_token() }}">
Laravel 13 memformalkan perlindungan ini sebagai PreventRequestForgery, yang menambahkan
verifikasi berbasis origin di samping token. Artinya permintaan yang datang dari asal yang tidak
dikenal ditolak walaupun entah bagaimana membawa token yang benar โ pertahanan berlapis untuk kelas serangan
yang sama.
419 dan cara yang salah memperbaikinya
| Sebab | Perbaikan yang benar |
|---|---|
Form tanpa @csrf | Tambahkan |
| Sesi kedaluwarsa saat halaman lama dibuka | Naikkan SESSION_LIFETIME, atau tangani 419 di frontend |
| Permintaan AJAX tanpa header token | Kirim X-CSRF-TOKEN |
Session driver file di banyak kontainer | Pindahkan ke Redis |
SESSION_DOMAIN salah | Samakan dengan domain yang benar-benar dipakai |
Yang tidak boleh dilakukan: mengecualikan rute dari perlindungan CSRF supaya errornya hilang. Pengecualian hanya sah untuk endpoint yang memang dipanggil mesin dan punya autentikasi lain โ webhook dari penyedia pembayaran, misalnya. Dan webhook seperti itu wajib memverifikasi tanda tangan pengirimnya.
// bootstrap/app.php โ hanya untuk webhook yang tanda tangannya diverifikasi
->withMiddleware(function (Middleware $middleware) {
$middleware->validateCsrfTokens(except: [
'webhook/pembayaran',
]);
})
Peta kerentanan lain
| Kerentanan | Sudah dilindungi? | Cara melubanginya tanpa sadar |
|---|---|---|
| SQL injection | Ya โ semua nilai di-bind | whereRaw("nama = '$input'") |
| XSS | Ya โ {{ }} meng-escape | {!! $input !!}, atau v-html di frontend |
| Mass assignment | Ya โ $fillable | $guarded = [] |
| CSRF | Ya โ rute web | Menambah rute ke daftar pengecualian |
| Objek milik orang lain | Tidak | Lupa policy, atau binding bersarang tanpa scopeBindings() |
| Kebocoran lewat error | Tidak | APP_DEBUG=true di produksi |
| Redirect terbuka | Tidak | redirect($request->input('next')) |
| Serangan tebak password | Tidak, kecuali kamu memasangnya | Rute login tanpa throttle |
Dua yang paling sering terlewat
// Redirect terbuka โ penyerang mengirim korban ke situsnya lewat domainmu
return redirect($request->input('next')); // BAHAYA
return redirect()->to(
Str::startsWith($request->input('next', '/'), '/') // hanya path internal
? $request->input('next')
: '/'
);
// Webhook: verifikasi tanda tangan, dan bandingkan dengan cara yang tahan timing attack
$tandaTangan = hash_hmac('sha256', $request->getContent(), config('services.bayar.secret'));
abort_unless(
hash_equals($tandaTangan, $request->header('X-Signature', '')),
403,
);
hash_equals(), bukan ===. Perbandingan string biasa berhenti di karakter
pertama yang berbeda, sehingga waktu eksekusinya membocorkan seberapa banyak awalan yang sudah benar.
hash_equals() selalu membandingkan seluruh panjangnya.
Header keamanan
// Middleware global
$response->headers->add([
'X-Content-Type-Options' => 'nosniff',
'X-Frame-Options' => 'SAMEORIGIN',
'Referrer-Policy' => 'strict-origin-when-cross-origin',
'Strict-Transport-Security' => 'max-age=31536000; includeSubDomains',
]);
Di Fase 9, header seperti ini lebih tepat dipasang di CloudFront lewat response headers policy โ supaya juga berlaku untuk berkas statis yang tidak pernah menyentuh PHP.
Latihan: buat form penghapusan tanpa @csrf, kirim, dan pastikan kamu menerima 419.
Lalu buat endpoint webhook yang memverifikasi HMAC dengan hash_equals(), dan uji dengan tanda
tangan yang benar dan yang salah.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.