WordPress’te Giriş Denemelerini Sınırlama: Brute Force Koruması
WordPress sitenize her gün yüzlerce, hatta binlerce otomatik giriş denemesi yapılıyor olabilir. Bu saldırılar, “brute force” yani kaba kuvvet saldırıları olarak adlandırılır ve botlar aracılığıyla sistematik şekilde kullanıcı adı/şifre kombinasyonlarını deniyor. Çoğu site sahibi bu durumun farkında bile değil. Sunucu loglarına baktığınızda /wp-login.php endpoint’ine gelen istek sayısı sizi şaşırtabilir. Bu yazıda, WordPress’in functions.php dosyasına ekleyebileceğiniz kod parçacıklarıyla bu saldırıları nasıl sınırlayabileceğinizi, engelleyebileceğinizi ve izleyebileceğinizi adım adım ele alacağız.
Brute Force Saldırısı Nedir ve Neden Önemlidir?
Kaba kuvvet saldırısı, bir saldırganın doğru şifreyi bulana kadar farklı kombinasyonları denediği bir yöntemdir. WordPress özelinde bu saldırılar genellikle üç farklı şekilde gerçekleşir:
- Klasik brute force: Tüm karakter kombinasyonlarını sırayla dener
- Dictionary attack: Yaygın şifre listelerini kullanır
- Credential stuffing: Başka sitelerden çalınmış kullanıcı adı/şifre çiftlerini dener
Gerçek dünya senaryosu olarak şunu düşünün: E-ticaret siteniz WooCommerce üzerinde çalışıyor, müşterilerinizin ödeme bilgileri var ve bir saldırgan admin hesabına erişirse ne olur? Sonuçlar felaket olabilir. Müşteri verileri çalınır, siteniz kara listeye girer, SEO sıralamanız yerle bir olur.
WordPress Varsayılan Güvenlik Açığı
WordPress, kutudan çıktığı haliyle giriş denemelerini sınırlandırmaz. Yani bir bot, /wp-login.php adresine dakikada binlerce istek gönderebilir. Bu durum hem güvenlik açığı hem de sunucu kaynakları açısından ciddi bir sorundur.
functions.php ile bu açığı kapatmak için eklenti kurmak zorunda değilsiniz. Native WordPress kancaları ve PHP ile son derece etkili bir koruma sistemi kurabilirsiniz.
Temel Giriş Denemesi Sınırlama Sistemi
İlk olarak, başarısız giriş denemelerini takip eden ve belirli bir eşiği aşınca kullanıcıyı geçici olarak engelleyen temel sistemi kuralım.
// functions.php - Temel Brute Force Koruması
function limit_login_attempts() {
// Maksimum deneme sayısı
$max_attempts = 5;
// Kilit süresi (saniye cinsinden - 15 dakika)
$lockout_duration = 900;
// İstemci IP adresini al
$ip_address = $_SERVER['REMOTE_ADDR'];
// X-Forwarded-For header'ı kontrol et (proxy arkasındaki gerçek IP)
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip_parts = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
$ip_address = trim($ip_parts[0]);
}
$transient_key = 'failed_login_' . md5($ip_address);
$attempts_data = get_transient($transient_key);
if ($attempts_data !== false) {
$attempt_count = $attempts_data['count'];
if ($attempt_count >= $max_attempts) {
$remaining_time = $attempts_data['lockout_time'] - time();
if ($remaining_time > 0) {
$minutes = ceil($remaining_time / 60);
wp_die(
sprintf(
'<strong>Güvenlik Uyarısı:</strong> Çok fazla başarısız giriş denemesi. %d dakika sonra tekrar deneyin.',
$minutes
),
'Erişim Engellendi',
array('response' => 403)
);
}
}
}
}
add_action('login_init', 'limit_login_attempts');
Bu kod, her başarısız girişi WordPress transient sistemiyle takip eder. Transient sistemi kullanmak, veritabanı üzerinde fazla yük oluşturmadan hızlı okuma/yazma imkanı sağlar.
Başarısız Girişleri Kaydetme
Sistemi tamamlamak için başarısız giriş denemelerini kaydetmemiz gerekiyor. wp_login_failed kancası bu iş için biçilmiş kaftan.
// Başarısız giriş denemelerini kaydet
function record_failed_login($username) {
$max_attempts = 5;
$lockout_duration = 900; // 15 dakika
$ip_address = $_SERVER['REMOTE_ADDR'];
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip_parts = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
$ip_address = trim($ip_parts[0]);
}
$transient_key = 'failed_login_' . md5($ip_address);
$attempts_data = get_transient($transient_key);
if ($attempts_data === false) {
// İlk başarısız deneme
$attempts_data = array(
'count' => 1,
'first_attempt' => time(),
'lockout_time' => 0,
'username' => sanitize_text_field($username)
);
} else {
$attempts_data['count']++;
$attempts_data['username'] = sanitize_text_field($username);
}
// Maksimum denemeye ulaşıldıysa kilit zamanını ayarla
if ($attempts_data['count'] >= $max_attempts) {
$attempts_data['lockout_time'] = time() + $lockout_duration;
// Yöneticiye e-posta gönder
notify_admin_brute_force($ip_address, $username, $attempts_data['count']);
}
// Transient'i kaydet (kilit süresi kadar tut)
set_transient($transient_key, $attempts_data, $lockout_duration * 2);
// Veritabanı loguna kaydet
log_failed_attempt($ip_address, $username);
}
add_action('wp_login_failed', 'record_failed_login');
Başarılı Girişte Sayacı Sıfırlama
Meşru bir kullanıcı başarılı giriş yaptığında, o IP için deneme sayacını sıfırlamamız gerekiyor. Aksi takdirde gerçek kullanıcılar şifrelerini doğru girdikten sonra bile sorun yaşayabilir.
// Başarılı girişte sayacı temizle
function reset_login_attempts($user_login, $user) {
$ip_address = $_SERVER['REMOTE_ADDR'];
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip_parts = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
$ip_address = trim($ip_parts[0]);
}
$transient_key = 'failed_login_' . md5($ip_address);
delete_transient($transient_key);
// Başarılı girişi logla
$log_data = array(
'time' => current_time('mysql'),
'ip' => $ip_address,
'username' => $user_login,
'status' => 'success'
);
$existing_log = get_option('wp_login_log', array());
array_unshift($existing_log, $log_data);
// Logu 500 kayıtla sınırla
if (count($existing_log) > 500) {
$existing_log = array_slice($existing_log, 0, 500);
}
update_option('wp_login_log', $existing_log);
}
add_action('wp_login', 'reset_login_attempts', 10, 2);
Yönetici E-posta Bildirimi
Bir IP adresi çok fazla başarısız deneme yaptığında yöneticiye haber vermek kritik önem taşır. Bu sayede gerçek zamanlı tepki verebilirsiniz.
// Brute force tespitinde yönetici bildirimi
function notify_admin_brute_force($ip_address, $username, $attempt_count) {
$admin_email = get_option('admin_email');
$site_name = get_bloginfo('name');
$site_url = get_site_url();
// Aynı IP için 1 saatte bir bildirim gönder (spam önleme)
$notification_key = 'bf_notified_' . md5($ip_address);
if (get_transient($notification_key)) {
return;
}
$subject = sprintf('[%s] Brute Force Saldırısı Tespit Edildi', $site_name);
$message = sprintf(
"Sitenizde şüpheli giriş aktivitesi tespit edildi.nn" .
"Site: %sn" .
"IP Adresi: %sn" .
"Hedef Kullanıcı Adı: %sn" .
"Deneme Sayısı: %dn" .
"Tarih/Saat: %snn" .
"Bu IP adresi 15 dakika süreyle otomatik olarak engellendi.n" .
"Kalıcı engelleme için .htaccess veya güvenlik duvarı kuralı eklemeniz önerilir.nn" .
"IP Arama: https://www.abuseipdb.com/check/%s",
$site_url,
$ip_address,
$username,
$attempt_count,
current_time('d.m.Y H:i:s'),
$ip_address
);
wp_mail($admin_email, $subject, $message);
// 1 saat boyunca tekrar bildirim gönderme
set_transient($notification_key, true, 3600);
}
Veritabanı Loglama Sistemi
Başarısız giriş denemelerini takip etmek için basit ama etkili bir loglama sistemi kuralım. Bu loglar, saldırı örüntülerini analiz etmenizi sağlar.
// Başarısız giriş denemelerini veritabanına logla
function log_failed_attempt($ip_address, $username) {
global $wpdb;
$table_name = $wpdb->prefix . 'login_attempts';
// Tablo yoksa oluştur
if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") != $table_name) {
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE $table_name (
id mediumint(9) NOT NULL AUTO_INCREMENT,
attempt_time datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
ip_address varchar(45) NOT NULL,
username varchar(60) NOT NULL,
user_agent varchar(255),
PRIMARY KEY (id),
KEY ip_address (ip_address),
KEY attempt_time (attempt_time)
) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($sql);
}
$wpdb->insert(
$table_name,
array(
'attempt_time' => current_time('mysql'),
'ip_address' => $ip_address,
'username' => sanitize_text_field($username),
'user_agent' => isset($_SERVER['HTTP_USER_AGENT'])
? sanitize_text_field($_SERVER['HTTP_USER_AGENT'])
: 'Unknown'
),
array('%s', '%s', '%s', '%s')
);
// 30 günden eski kayıtları temizle
$wpdb->query(
$wpdb->prepare(
"DELETE FROM $table_name WHERE attempt_time < %s",
date('Y-m-d H:i:s', strtotime('-30 days'))
)
);
}
Whitelist ve Blacklist Yönetimi
Bazı IP adreslerini daima izin vermek (whitelist) ya da daima engellemek (blacklist) isteyebilirsiniz. Örneğin kendi ofis IP adresinizi whitelist’e ekleyebilirsiniz.
// IP Whitelist ve Blacklist yönetimi
function check_ip_lists() {
$ip_address = $_SERVER['REMOTE_ADDR'];
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip_parts = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
$ip_address = trim($ip_parts[0]);
}
// Whitelist - Bu IP'ler hiçbir zaman engellenmez
$whitelist = array(
'192.168.1.1', // Ofis IP
'10.0.0.1', // VPN IP
'203.0.113.50', // Güvenilir geliştirici IP
);
// Dinamik whitelist (veritabanından)
$dynamic_whitelist = get_option('login_ip_whitelist', array());
$whitelist = array_merge($whitelist, $dynamic_whitelist);
if (in_array($ip_address, $whitelist)) {
return; // Whitelist'teki IP'ler için kontrol yapma
}
// Blacklist - Bu IP'ler daima engellenir
$blacklist = get_option('login_ip_blacklist', array());
// Manuel eklenen statik blacklist
$static_blacklist = array(
'185.220.101.0', // Bilinen Tor çıkış düğümü
'45.33.32.156', // Örnek kötü niyetli IP
);
$blacklist = array_merge($blacklist, $static_blacklist);
if (in_array($ip_address, $blacklist)) {
wp_die(
'Bu IP adresi güvenlik nedeniyle engellenmiştir. Sorun yaşıyorsanız site yöneticisiyle iletişime geçin.',
'Erişim Engellendi',
array('response' => 403)
);
}
}
add_action('login_init', 'check_ip_lists', 1);
// Dinamik olarak IP blacklist'e ekleme
function add_ip_to_blacklist($ip_address) {
$blacklist = get_option('login_ip_blacklist', array());
if (!in_array($ip_address, $blacklist)) {
$blacklist[] = sanitize_text_field($ip_address);
update_option('login_ip_blacklist', $blacklist);
}
}
// Otomatik blacklist - 3 kilit cezası sonrası kalıcı engel
function auto_blacklist_repeat_offenders($ip_address) {
$offense_key = 'lockout_count_' . md5($ip_address);
$offense_count = get_option($offense_key, 0);
$offense_count++;
update_option($offense_key, $offense_count);
// 3 kez kilitlenirse kalıcı blacklist'e al
if ($offense_count >= 3) {
add_ip_to_blacklist($ip_address);
// Yöneticiye bildir
$admin_email = get_option('admin_email');
wp_mail(
$admin_email,
'[Güvenlik] IP Kalıcı Olarak Engellendi',
sprintf('%s IP adresi 3 kez kilitlenme nedeniyle kalıcı blacklist'e alındı.', $ip_address)
);
}
}
Kullanıcı Adı Numaralandırma Koruması
Brute force saldırılarının ilk adımı genellikle geçerli kullanıcı adlarını bulmaktır. WordPress varsayılan olarak yanlış kullanıcı adı ile yanlış şifreyi birbirinden ayıran hata mesajları gösterir. Bunu düzeltelim.
// Giriş hata mesajlarını gizle (kullanıcı adı/şifre ipucu verme)
function obscure_login_errors($error) {
global $errors;
$generic_message = '<strong>Hata:</strong> Kullanıcı adı veya şifre hatalı.';
// Tüm giriş hatalarını aynı mesajla değiştir
$err_codes = array(
'invalid_username',
'invalid_email',
'incorrect_password',
'invalidcombo'
);
if (is_wp_error($errors)) {
$error_codes = $errors->get_error_codes();
foreach ($error_codes as $code) {
if (in_array($code, $err_codes)) {
return $generic_message;
}
}
}
return $error;
}
add_filter('login_errors', 'obscure_login_errors');
// REST API üzerinden kullanıcı numaralandırmayı engelle
function disable_user_enumeration_rest($response, $user, $request) {
if (!current_user_can('list_users')) {
// E-posta ve diğer hassas verileri kaldır
unset($response->data['email']);
unset($response->data['slug']);
}
return $response;
}
add_filter('rest_prepare_user', 'disable_user_enumeration_rest', 10, 3);
// ?author=1 numaralandırma saldırısını engelle
function disable_author_enumeration() {
if (!is_admin() && isset($_GET['author'])) {
wp_redirect(home_url(), 301);
exit;
}
}
add_action('init', 'disable_author_enumeration');
Admin Dashboard’da İstatistik Görüntüleme
Tüm bu verileri topladık, şimdi bunları yönetim panelinde gösterelim. Bu sayede güvenlik durumunuzu kolayca takip edebilirsiniz.
// Admin dashboard widget'ı - Giriş güvenlik istatistikleri
function login_security_dashboard_widget() {
wp_add_dashboard_widget(
'login_security_stats',
'Giriş Güvenlik İstatistikleri',
'render_login_security_widget'
);
}
add_action('wp_dashboard_setup', 'login_security_dashboard_widget');
function render_login_security_widget() {
global $wpdb;
$table_name = $wpdb->prefix . 'login_attempts';
// Tablo var mı kontrol et
if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") != $table_name) {
echo '<p>Henüz giriş denemesi kaydedilmedi.</p>';
return;
}
// Son 24 saat istatistikleri
$last_24h = $wpdb->get_var(
"SELECT COUNT(*) FROM $table_name
WHERE attempt_time > DATE_SUB(NOW(), INTERVAL 24 HOUR)"
);
// Son 7 gün istatistikleri
$last_7d = $wpdb->get_var(
"SELECT COUNT(*) FROM $table_name
WHERE attempt_time > DATE_SUB(NOW(), INTERVAL 7 DAY)"
);
// En çok deneme yapan IP'ler
$top_ips = $wpdb->get_results(
"SELECT ip_address, COUNT(*) as attempt_count
FROM $table_name
WHERE attempt_time > DATE_SUB(NOW(), INTERVAL 7 DAY)
GROUP BY ip_address
ORDER BY attempt_count DESC
LIMIT 5"
);
// En çok hedef alınan kullanıcı adları
$top_usernames = $wpdb->get_results(
"SELECT username, COUNT(*) as attempt_count
FROM $table_name
WHERE attempt_time > DATE_SUB(NOW(), INTERVAL 7 DAY)
GROUP BY username
ORDER BY attempt_count DESC
LIMIT 5"
);
echo '<div style="display:flex; gap:20px;">';
echo '<div><strong>Son 24 Saat:</strong> ' . intval($last_24h) . ' başarısız deneme</div>';
echo '<div><strong>Son 7 Gün:</strong> ' . intval($last_7d) . ' başarısız deneme</div>';
echo '</div>';
if (!empty($top_ips)) {
echo '<h4>En Aktif Saldırgan IP'ler (7 gün)</h4><ul>';
foreach ($top_ips as $ip_data) {
echo '<li>' . esc_html($ip_data->ip_address) . ' - ' .
intval($ip_data->attempt_count) . ' deneme</li>';
}
echo '</ul>';
}
if (!empty($top_usernames)) {
echo '<h4>En Çok Hedef Alınan Kullanıcı Adları</h4><ul>';
foreach ($top_usernames as $user_data) {
echo '<li>' . esc_html($user_data->username) . ' - ' .
intval($user_data->attempt_count) . ' deneme</li>';
}
echo '</ul>';
}
$blacklist = get_option('login_ip_blacklist', array());
echo '<p><strong>Aktif Blacklist:</strong> ' . count($blacklist) . ' IP engelli</p>';
}
Gerçek Dünya Senaryosu: WooCommerce Mağazası Güvenliği
Bir WooCommerce mağazası işletiyorsanız, müşteri hesaplarını da korumanız gerekir. Yalnızca admin girişini değil, my-account sayfasını da güvence altına alın.
WooCommerce’in kendi giriş formu /wp-login.php yerine genellikle my-account sayfasını kullanır. Bu sayfa için de aynı korumayı uygulayabilirsiniz:
woocommerce_process_login_errorskancasını kullanarak WooCommerce giriş hatalarını yakalayın- Müşteri hesapları için daha düşük eşik belirleyin (örneğin 3 deneme)
- Hesap kilitleme durumunda müşteriye e-posta gönderin ve şifre sıfırlama bağlantısı sunun
- Toplu sipariş verme veya kupon kötüye kullanımını da aynı mantıkla sınırlayın
Gerçekten yaşanan bir örnek: Bir mağaza sahibi, Cuma gecesi başlayan bir saldırıyı Pazartesi sabahı fark etti. Saldırganlar 72 saatte 50.000’den fazla deneme yapmıştı. Bu sistem yerinde olsaydı, ilk 5 denemeden sonra IP engellenecek ve sunucu kaynakları korunacaktı.
Performans Optimizasyonu
Bu sistemin sunucu performansına etkisini minimize etmek için dikkat etmeniz gerekenler:
- Transient kullan, her zaman veritabanına gitme: Sık okunan veriler için WordPress transient API mükemmeldir
- Cron ile log temizliği yap: 30 günden eski kayıtları düzenli temizle
- Object cache kullan: Redis veya Memcached varsa transient’lar otomatik olarak bunları kullanır
- İndeks ekle:
login_attemptstablosunaip_addressveattempt_timeüzerinde indeks eklediğinizden emin olun
// Günlük otomatik temizlik cron'u
function schedule_login_cleanup() {
if (!wp_next_scheduled('cleanup_login_logs')) {
wp_schedule_event(time(), 'daily', 'cleanup_login_logs');
}
}
add_action('wp', 'schedule_login_cleanup');
function run_login_log_cleanup() {
global $wpdb;
$table_name = $wpdb->prefix . 'login_attempts';
// 30 günden eski kayıtları sil
$wpdb->query(
"DELETE FROM $table_name WHERE attempt_time < DATE_SUB(NOW(), INTERVAL 30 DAY)"
);
// Eski offense sayaçlarını temizle
global $wpdb;
$wpdb->query(
"DELETE FROM {$wpdb->options}
WHERE option_name LIKE 'lockout_count_%'
AND option_value < 3"
);
}
add_action('cleanup_login_logs', 'run_login_log_cleanup');
Ek Güvenlik Katmanları
functions.php ile yapabileceklerinizin yanı sıra, sunucu seviyesinde de birkaç önlem almanız önerilir:
- Nginx rate limiting:
limit_req_zonedirektifi ile/wp-login.phpiçin istek hızı sınırı koyun - Cloudflare WAF: Eğer Cloudflare kullanıyorsanız, giriş sayfası için özel WAF kuralı oluşturun
- 2FA (İki Faktörlü Doğrulama): Brute force’u geçen biri bile 2FA olmadan giremez
- Login URL değiştirme:
/wp-login.phpyerine özel bir URL belirlemek otomatik saldırıların büyük kısmını engeller - XML-RPC’yi devre dışı bırakma: Brute force için sıkça kullanılan bu endpoint’i kapatın
// XML-RPC'yi tamamen devre dışı bırak
add_filter('xmlrpc_enabled', '__return_false');
// XML-RPC header'larını da kaldır
remove_action('wp_head', 'rsd_link');
remove_action('wp_head', 'wlwmanifest_link');
// Giriş sayfası için ek HTTP güvenlik headerları
function add_security_headers_login() {
if ($GLOBALS['pagenow'] === 'wp-login.php') {
header('X-Frame-Options: DENY');
header('X-Content-Type-Options: nosniff');
header('Referrer-Policy: no-referrer');
}
}
add_action('send_headers', 'add_security_headers_login');
Dikkat Edilmesi Gereken Noktalar
Bu sistemi kurarken bazı tuzaklara düşmemek için:
- Kendi IP’nizi whitelist’e alın: Şifreyi unutup kendinizi kitlerseniz, sunucuya erişim için SSH veya hosting paneline ihtiyaç duyarsınız
- CDN arkasında gerçek IP’yi alın: Cloudflare kullanıyorsanız
HTTP_CF_CONNECTING_IPheader’ını kontrol edin - IPv6 desteği ekleyin: Modern saldırılar IPv6 adreslerini de kullanıyor,
varchar(45)bu yüzden yeterli - Test edin: Canlıya almadan önce staging ortamında test yapın
- Yedek giriş yöntemi hazırlayın: Her zaman bir plan B olsun, örneğin sunucu SSH erişimi
Sonuç
functions.php aracılığıyla kurulan bu brute force koruma sistemi, eklenti bağımlılığı olmadan WordPress sitenizi önemli ölçüde güçlendirir. Temel prensip basit: her başarısız girişi izle, eşiği aşanı engelle, tekrar eden suçluları kalıcı olarak listele ve tüm bunları yönetici olarak takip et.
Bu sistemin gerçek gücü katmanlı yapısında yatıyor. Tek bir önlem hiçbir zaman yeterli değildir. Giriş sınırlama, hata mesajı gizleme, kullanıcı adı numaralandırma koruması ve otomatik bildirimler bir arada çalışınca saldırganların işi gerçekten zorlaşıyor.
Son olarak şunu hatırlatayım: Güvenlik statik değil, dinamik bir süreçtir. Loglarınızı düzenli kontrol edin, yeni saldırı vektörlerine karşı sisteminizi güncel tutun ve ciddi bir tehdit altında olduğunuzu düşünüyorsanız profesyonel bir güvenlik denetimi yaptırmaktan çekinmeyin. Sitenizin güvenliği hem sizin hem de kullanıcılarınızın sorumluluğudur.
