Seorang editor di situs membership milik klien tiba-tiba tidak bisa login. Password-nya benar, dia yakin, dan untuk memastikan kami reset sekalian. Tetap gagal. Yang muncul cuma pesan generik: invalid credentials, or too many attempts. Tidak ada detail, tidak ada petunjuk, cuma tembok. Dan bagian yang bikin saya meringis: tembok itu saya yang bangun. Throttle login-nya buatan saya sendiri.
Ini juga bukan kali pertama kode itu makan tuannya. Beberapa waktu sebelumnya, throttle yang sama mengunci IP dan username tester kami sendiri selama berhari-hari. Waktu itu kami tidak sadar throttle-nya yang bekerja; gejalanya menyamar rapi jadi "bug session cookie", dan kami menghabiskan waktu mengejar tersangka yang salah.
Jalan buntu yang saya coba dulu
Refleks pertama: kredensial. Password sudah direset, jadi bukan itu. Refleks kedua, karena trauma insiden tester: curiga session cookie lagi. Clear cookie, ganti browser, coba incognito. Tetap ditolak.
Baru setelah itu saya buka data throttle-nya sendiri. Throttle ini menyimpan counter per-username, jadi saya cek bucket username si editor. Hasilnya: bersih. Nol attempt gagal, tidak ada lockout.
Ini titik paling menyesatkan dari seluruh cerita. Kalau bucket username-nya bersih, dari mana datangnya "too many attempts"? Selama beberapa menit saya sempat berpikir throttle-nya tidak bersalah dan masalahnya ada di lapisan lain.
Akar masalahnya: bucket IP kantor, bukan bucket username
Throttle ini saya desain dengan dua kunci: per-IP dan per-username, masing-masing dengan exponential backoff. Bucket username si editor memang bersih. Yang terkunci adalah bucket IP-nya.
Dan di sinilah detail yang menjelaskan semuanya: si editor login dari kantor. IP kantor itu shared, satu alamat publik di belakang NAT untuk semua orang di baliknya. Ketika saya bongkar isi bucket IP tersebut, isinya bukan jejak si editor sama sekali. Isinya bot: 4337 attempt ke username admin dan 386 attempt ke username editor, pola spray klasik.
Exponential backoff bikin makin parah. Ribuan attempt berarti masa kunci IP itu memanjang terus sampai praktis permanen. Dan setiap orang yang login dari balik NAT itu, dengan kredensial sebenar apa pun, dianggap datang dari IP yang sama dengan bot-nya. Ini persis pola yang diperingatkan OWASP soal lockout berbasis IP: di belakang NAT atau proxy bersama, satu aktor jahat cukup untuk membuat semua orang lain jadi collateral.
Perbaikannya
Padamkan apinya dulu
Sebelum mikir redesign, user harus bisa masuk hari itu juga. Throttle ini menyimpan state-nya sebagai transient dengan prefix fin_lf_, jadi pembersihan daruratnya cukup lewat PHP CLI:
php -r 'require "wp-load.php"; global $wpdb; $k=$wpdb->get_col("SELECT REPLACE(option_name,\"_transient_\",\"\") FROM $wpdb->options WHERE option_name LIKE \"_transient_fin_lf_%\""); foreach($k as $t) delete_transient($t); echo count($k);'Satu perintah, semua counter bersih, si editor langsung bisa login lagi.
Redesign: identity-based, bukan sekadar IP-based
Prinsip barunya muat dalam satu kalimat: blokir IP hanya boleh dipicu oleh username yang tidak resolve ke akun nyata.
Logikanya begini. Bot enumeration menebak-nebak username yang mayoritas tidak ada di sistem. User asli, hampir selalu, mengetik username yang memang ada. Jadi bucket IP sekarang hanya menghitung dan menahan percobaan ke username yang tidak dikenal. Percobaan ke akun yang benar-benar eksis tidak pernah menyentuh blokir IP; untuk mereka yang berlaku tetap bucket per-user dengan exponential backoff, jadi brute force ke satu akun tetap tertahan.
$user = get_user_by( 'login', $username );
if ( ! $user ) {
// Username tidak resolve ke akun nyata: hanya ini yang dihitung bucket IP.
// Pola khas bot enumeration.
bump_ip_bucket( $ip );
return;
}
// Akun nyata tidak pernah kena blokir IP.
// Mereka hanya tunduk pada bucket per-user dengan exponential backoff.
bump_user_bucket( $user->ID );Dengan desain ini, spray bot ke admin dan editor tetap tertampung di bucket IP asalnya, tapi orang kantor yang login dengan akun sungguhan lewat begitu saja.
Satu langkah bonus yang hasilnya paling memuaskan: kami rename username admin. Spray-nya berhenti, counter-nya turun ke nol. Target favorit bot ternyata cukup dihilangkan dari papan.
Pelajaran
- Bucket username bersih bukan berarti user tidak di-throttle. Cek semua dimensi kuncinya: IP, username, dan kombinasinya.
- Lockout berbasis IP di belakang NAT atau shared IP menghukum orang yang tidak bersalah. OWASP sudah lama memperingatkan pola ini.
- Desain yang lebih adil: blokir IP hanya untuk username yang tidak resolve ke akun nyata; user eksis cukup ditahan bucket per-user.
- Sediakan jalur darurat untuk menghapus state throttle via CLI, karena korban pertamanya biasanya user kamu sendiri, atau malah kamu.
- Rename akun
adminitu murah, dan di kasus ini terbukti menghentikan spray sampai nol.
