Bali, Indonesia
Insights

Mengira Plugin Limit Login Cukup, Padahal Brute-Force Tetap Membebani PHP

October 27, 2026
0:00
Banyak pemilik situs WordPress merasa urusan brute-force sudah selesai begitu plugin limit login aktif. Dashboard plugin menampilkan daftar IP yang terkunci, notifikasi lockout masuk ke email, jadi kelihatannya serangan sudah tertahan. Masalahnya, CPU tetap tinggi, proses PHP tetap penuh, dan access log masih dibanjiri POST /wp-login.php. Plugin itu memang bekerja, hanya saja ia bekerja di tempat yang terlalu dalam untuk melindungi resource server.
Plugin limit login adalah bagian dari WordPress. Artinya, plugin itu baru bisa mengambil keputusan setelah WordPress sendiri berjalan. Untuk sampai ke titik itu, setiap request login harus melewati semua lapisan di bawahnya dulu: web server menerima koneksi, meneruskan request ke handler PHP (PHP-FPM, LSAPI, atau sejenisnya), lalu PHP memuat WordPress. Jadi ketika plugin bilang IP sekian sudah diblokir, yang sebenarnya terjadi adalah: request dari IP itu tetap masuk, tetap memakai satu worker PHP, tetap memuat core WordPress, dan baru di tengah jalan plugin memutuskan untuk menolak. Penolakannya murah dibanding login sungguhan, tapi biaya untuk sampai ke penolakan itu tidak murah. Ini yang sering membingungkan di sisi support. Pelanggan melihat log plugin penuh lockout dan menyimpulkan serangannya gagal. Dari sisi keamanan akun, mungkin benar. Dari sisi beban server, serangannya tetap berhasil menghabiskan resource.
Kalau diurutkan, satu percobaan login kira-kira melewati tahapan ini:
  1. Web server menerima request dan melihat targetnya file PHP.
  2. Request diteruskan ke worker PHP. Kalau semua worker sedang sibuk, request antre.
  3. PHP memuat wp-config.php, membuka koneksi ke MySQL, memuat core WordPress, tema, dan plugin aktif.
  4. Plugin limit login mengecek apakah IP atau username sudah melewati batas. Pengecekan ini biasanya butuh query ke database.
  5. Kalau belum diblokir, WordPress memverifikasi password. Kalau gagal, plugin mencatat percobaan itu, sering kali dengan menulis ke database.
  6. Respons dikirim balik.
Ada dua hal yang membuat pola ini berat. Pertama, page cache tidak membantu. Cache halaman biasanya hanya berlaku untuk request GET dari pengunjung yang belum login, sedangkan percobaan login adalah POST dan harus diproses dinamis. Kedua, banyak plugin limit login justru menambah kerja database karena setiap percobaan gagal dicatat. Di serangan besar, tabel atau option tempat menyimpan data lockout bisa ikut membengkak. Lalu ada masalah klasik yang membuat limit per IP kurang efektif: brute-force modern sering datang dari banyak IP sekaligus. Kalau batasnya, contohnya, 5 percobaan per IP, sementara serangan datang dari ribuan IP yang masing-masing hanya mencoba 2 atau 3 kali, plugin hampir tidak pernah memblokir apa-apa. Semua request lolos sampai tahap verifikasi password.
Sebelum memilih cara blokir, kita perlu tahu bentuk serangannya. Access log web server adalah sumber terbaik karena mencatat semua request, termasuk yang tidak pernah sampai ke plugin. Lokasi file-nya tergantung setup: di Nginx umumnya di /var/log/nginx/, di Apache di /var/log/apache2/ atau /var/log/httpd/, dan di cPanel tiap domain punya log sendiri di direktori domlogs. Ganti path di contoh berikut sesuai server kamu. Langkah pertama, hitung IP mana yang paling sering mengirim POST ke wp-login.php:
BASH
grep 'POST /wp-login.php' /path/ke/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -20
Perintah ini hanya membaca log, aman dijalankan kapan saja. Kolom pertama hasilnya adalah jumlah request, kolom kedua IP-nya. Asumsinya format log standar (combined), di mana IP klien ada di kolom pertama. Langkah kedua, lihat ritmenya per menit. Ini membantu membedakan serangan yang sedang berjalan dari sisa aktivitas beberapa jam lalu:
BASH
grep 'POST /wp-login.php' /path/ke/access.log | awk '{print $4}' | cut -d: -f1-3 | uniq -c | tail -30
Kolom keempat di format combined berisi timestamp seperti [26/Oct/2026:16:41:54, dan cut memotongnya sampai menit. Hasilnya jumlah POST login per menit. Dari dua hasil ini biasanya polanya kelihatan:
  • Segelintir IP dengan jumlah request sangat tinggi. Ini serangan terkonsentrasi, paling gampang ditangani dengan blokir IP.
  • Daftar IP panjang dengan jumlah kecil per IP, tapi total per menit tetap tinggi. Ini serangan terdistribusi. Blokir per IP akan terasa seperti main kejar-kejaran, dan plugin limit login hampir pasti tidak terpicu.
Perhatikan juga kode status. Di WordPress standar, login gagal biasanya mengembalikan 200 (halaman login ditampilkan ulang dengan pesan error), sedangkan login berhasil biasanya 302 redirect ke dashboard. Kalau ada 302 dari IP yang tidak dikenal, itu tanda perlu cek akun admin dan ganti password, bukan sekadar urusan beban.
Aturannya sederhana: makin dekat blokir ke pintu masuk, makin sedikit resource yang terbuang. Firewall menolak di level jaringan, web server menolak sebelum PHP jalan, WAF atau CDN menolak sebelum request sampai ke server sama sekali. Untuk serangan terkonsentrasi dari sedikit IP, blokir di firewall server sudah cukup, misalnya lewat CSF, iptables/nftables, atau fail2ban yang membaca access log dan memblokir IP otomatis setelah ambang tertentu. Bedanya dengan plugin, fail2ban bekerja di luar WordPress, jadi request berikutnya dari IP itu tidak pernah menyentuh PHP. Untuk serangan terdistribusi, blokir per IP kurang efektif. Yang lebih masuk akal adalah membatasi akses ke path-nya. Beberapa opsi, dari yang paling ketat:
  • Batasi wp-login.php hanya untuk IP tertentu. Cocok kalau admin selalu login dari IP kantor yang tetap. Tidak cocok untuk situs dengan banyak member yang login dari mana saja.
  • Pasang HTTP Basic Auth di depan wp-login.php. Di Apache atau LiteSpeed bisa lewat blok Files di .htaccess dengan AuthType Basic dan file .htpasswd. Autentikasi ini diproses web server sebelum PHP jalan, jadi bot yang tidak tahu password Basic Auth langsung ditolak dengan 401. Cek dulu apakah ada plugin atau tema yang memanggil wp-login.php lewat AJAX, karena bisa ikut terkena.
  • Rate limit di web server. Ini menjaga login tetap terbuka tapi membatasi seberapa cepat satu klien boleh mencoba.
Contoh rate limit di Nginx (angkanya hanya ilustrasi):
NGINX
# Di dalam blok http: zona memori 10 MB, 5 request per menit per IP (contoh)
limit_req_zone $binary_remote_addr zone=wplogin:10m rate=5r/m;

# Di dalam blok server
location = /wp-login.php {
    limit_req zone=wplogin burst=3 nodelay;

    # Salin direktif fastcgi dari blok location ~ \.php$ yang sudah ada,
    # karena location exact match ini mengambil alih request ke wp-login.php
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME document_rootfastcgi_script_name;
    fastcgi_pass unix:/path/ke/php-fpm.sock;
}
Request yang melewati batas ditolak Nginx (default dengan 503, bisa diganti lewat limit_req_status) tanpa memanggil PHP. Bagian fastcgi_pass wajib disesuaikan dengan socket atau port PHP-FPM di server kamu, lalu uji dengan nginx -t sebelum reload. Rate limit per IP tetap punya kelemahan yang sama dengan plugin saat serangan terdistribusi, tapi setidaknya penolakannya terjadi di level web server yang jauh lebih ringan. Kalau serangannya besar dan terdistribusi, WAF atau CDN yang bisa memberi challenge khusus untuk path /wp-login.php biasanya opsi paling efektif, karena request bot berhenti sebelum masuk ke server. Fitur dan cara konfigurasinya berbeda per layanan, jadi cek dokumentasi WAF yang kamu pakai untuk rule berbasis path dan metode POST.
Pertama, kalau situs berada di belakang CDN atau reverse proxy, IP di access log bisa jadi IP milik proxy, bukan IP penyerang. Blokir IP atau rate limit per IP dalam kondisi ini malah bisa mengenai pengunjung asli. Pastikan modul real IP sudah dikonfigurasi dulu. Penjelasan lengkapnya ada di Blocking IPs From Your Access Log Won't Stop Bots Coming Through Cloudflare. Kedua, wp-login.php bukan satu-satunya pintu. xmlrpc.php juga menerima kredensial dan sering jadi target saat login form sudah diperketat. Kalau situs tidak memakai XML-RPC (misalnya untuk aplikasi mobile atau integrasi tertentu), blokir akses ke file itu di level web server juga. Jalankan perintah hitung di atas dengan mengganti path-nya untuk melihat apakah serangan pindah ke sana. Ketiga, jangan langsung mencopot plugin limit login. Plugin itu masih berguna untuk hal yang tidak bisa dilakukan web server, seperti mengunci username tertentu, memberi notifikasi ke admin, atau mencatat percobaan dengan konteks WordPress. Peran yang tepat untuk plugin adalah lapisan terakhir untuk keamanan akun, bukan pertahanan utama untuk resource server. Jadi kalau CPU tinggi padahal plugin limit login sudah aktif, mulai dari access log, bukan dari dashboard plugin. Hitung POST ke wp-login.php per IP dan per menit, tentukan apakah serangannya terkonsentrasi atau terdistribusi, lalu pindahkan blokirnya ke lapisan yang menolak request sebelum PHP sempat jalan. Setelah itu, cek ulang grafik CPU dan jumlah worker PHP yang aktif untuk memastikan blokirnya benar-benar memotong beban, bukan sekadar memindahkan serangan ke path lain.