Mekanisme login dan logout sudah kita kuasai di section Authentication . Chapter ini membedah sisi lainnya: apa yang bisa dipelajari penyerang dari alur login kita. Dua percobaan login yang gagal bisa bocor informasi berbeda — “email tidak terdaftar” versus “password salah” — dan perbedaan kecil itu cukup untuk memetakan basis pengguna aplikasi. Kita menguji empat titik lemah secara nyata: pesan error yang membocorkan keberadaan akun, session fixation, logout yang kotor, dan password yang tercuri dari kebocoran layanan lain.
Email Enumeration lewat Pesan Error #
Penyerang mengirim kredensial palsu ke route login dan membaca responsnya. Jika pesan error membedakan “email tidak ada” dari “password salah”, penyerang tinggal memetakan email valid untuk nantinya di-phishing. Kunci pertahanannya satu: semua kegagalan login harus menghasilkan pesan yang persis sama. Controller berikut memakai Auth::attempt() dan menolak dengan pesan generik:
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
Route::post('/login', function (Request $request) {
if (! Auth::attempt($request->only('email', 'password'))) {
return response()->json([
'message' => 'Kredensial tidak cocok dengan data kami.',
], 422);
}
return response()->json(['message' => 'Login berhasil.']);
})->middleware(['web', 'throttle:login']);Kita uji dengan dua akun — email terdaftar milik Budi dan email fiktif — keduanya dengan password yang salah:
login [email protected] + password salah : HTTP 422 | Kredensial tidak cocok dengan data kami.
login [email protected] + password salah: HTTP 422 | Kredensial tidak cocok dengan data kami.
pesan error identik: YA (aman)Halaman registrasi sering membocorkan hal yang sama lewat pesan “email sudah terdaftar”. Kita bandingkan dua implementasi:
=== registrasi VERSI BOCOR ===
email lama [email protected]: HTTP 422 | Email [email protected] sudah terdaftar.
email baru [email protected]: HTTP 201 | Registrasi berhasil.
=== registrasi VERSI AMAN ===
email lama [email protected]: HTTP 202 | Jika email belum terdaftar, tautan verifikasi telah dikirim.
email baru [email protected]: HTTP 202 | Jika email belum terdaftar, tautan verifikasi telah dikirim.
pesan identik: YA (aman)
email terkirim untuk KEDUA percobaan (array mailer): 2Versi aman selalu menjawab 202 dengan kalimat yang sama, lalu mengirim email verifikasi baik untuk email baru maupun lama — pola yang juga dipakai alur password reset di Password Reset
. Penyerang tidak mendapat sinyal apa pun; pengguna baru tetap menerima tautan verifikasi sebagaimana dibahas di Email Verification
.
Session Fixation: Regenerate Saat Login #
Session fixation terjadi ketika penyerang menanam ID session di browser korban (misalnya lewat tautan berisi ?session_id=xyz), lalu menunggu korban login dengan ID tersebut. Setelah login, penyerang memakai ID yang sudah dikenalnya untuk masuk ke akun korban. Pertahanannya adalah membuang ID session lama setiap kali login. Laravel melakukannya otomatis di dalam Auth::login() — mari kita buktikan:
=== session fixation: Auth::login() otomatis regenerate ===
session ID sebelum login : 8NVyNn3Hh15w…
session ID sesudah login : eZIqDlpwJUN1…
ID berubah (anti session fixation) : YA (bawaan Laravel)
token CSRF ikut diperbarui : YA
data lama ikut bermigrasi (keranjang tetap ada): YAKode internal SessionGuard memanggil $this->session->regenerate(true) setiap login — ID baru diterbitkan, token CSRF diperbarui, namun data seperti isi keranjang belanja ikut dipindahkan. Selama controller kita memakai Auth::login() atau Auth::attempt() — bukan menulis status login ke session secara manual — perlindungan ini sudah aktif.
Logout yang Bersih #
Auth::logout() hanya mengeluarkan user dari guard. Session lama — beserta ID dan token CSRF-nya — masih hidup di server. Kita uji perbandingannya:
=== logout TANPA hygiene ===
ID session berubah : TIDAK (masih bisa dibajak)
token CSRF berubah : TIDAK (replay masih mungkin)
payload session masih menyimpan data lama: YA (2 kunci)
=== logout DENGAN hygiene lengkap ===
ID session berubah : YA (session dibuang)
token CSRF berubah : YA
data lama terhapus : YA
state login : sudah keluarPola lengkapnya tiga baris — urutan logout dari Login dan Logout yang sebaiknya dijadikan kebiasaan di semua tempat logout:
Auth::logout();
session()->invalidate();
session()->regenerateToken();Baris invalidate() membuang seluruh data session sekaligus menghancurkan ID-nya, sedangkan regenerateToken() mencegah token CSRF lama diputar ulang. Jika aplikasi Anda punya fitur “logout dari semua perangkat”, tambahkan penulisan ulang kolom remember_token — topik yang kita bahas di Remember Me
.
Credential Stuffing dan Password Tercuri #
Credential stuffing adalah login memakai username dan password asli yang dicuri dari layanan lain. Dua lapis pertahanan menutup pola ini. Pertama, rate limit pada route login — sudah terpasang lewat middleware throttle:login di atas, dibedah tuntas di chapter Rate Limiting
. Kedua, tolak password yang sudah diketahui bocor secara publik lewat rule Password::uncompromised():
use Illuminate\Validation\Rules\Password;
$request->validate([
'password' => [
'required',
'confirmed',
Password::min(8)->uncompromised(),
],
]);Rule ini memeriksa kredensial ke basis data kebocoran publik milik haveibeenpwned dengan teknik k-anonymity — hanya lima karakter pertama dari hash SHA-1 password yang dikirim, jadi password itu sendiri tidak pernah meninggalkan server. Detail lengkap rule Password tersedia di dokumentasi validation
.
Terakhir, pastikan perbandingan password selalu lewat Hash::check(), bukan ===. Hash bcrypt memang dirancang lambat, dan password_verify() di baliknya membandingkan dengan waktu konstan. Kita verifikasi perilaku dasarnya:
password benar : cocok
password salah : tidak cocok
hash_equals beda 1 karakter: tidak cocokUntuk kasus perbandingan rahasia di luar password (misalnya signature webhook), gunakan hash_equals() — fungsi bawaan PHP yang membandingkan string dengan waktu konstan, berbeda dari === yang berhenti di byte pertama yang berbeda. Detail hashing sudah dibahas di Password Hashing
dan kebijakan password di Password Security
.
Autentikasi yang rapat hanya separuh cerita — penyerang yang gagal masuk ke akun Budi berikutnya akan mencoba membaca data Budi lewat URL. Kebocoran lintas pengguna inilah materi chapter berikutnya: Authorization Security .