WordPress’te Sanitize ve Escape: Veri Güvenliğini Sağlamanın Temel Yolları
Yıllar önce bir müşterimin WooCommerce mağazasında tuhaf bir şey fark ettim: ürün yorumları arasında JavaScript kodu çalışıyordu. Birisi yorum alanına alert('XSS') yazmış, sistem de bunu olduğu gibi sayfaya basmıştı. Müşteri beni arayıp “Sitemde bir şeyler oluyor, ziyaretçiler şikayet ediyor” dedi. Üç saatlik bir kriz yönetimi sonucunda sorunu çözdük ama o günden beri WordPress eklenti geliştirirken veri güvenliğini hiç ödün vermeden uyguluyorum. Bu yazıda sanitize ve escape kavramlarını, neden birbirinden farklı olduklarını ve hangi durumda hangisini kullanmanız gerektiğini gerçek senaryolarla aktaracağım.
Sanitize ve Escape: İkisi Aynı Şey Değil
Çoğu geliştirici bu iki kavramı birbirine karıştırıyor ya da aynı şeymiş gibi kullanıyor. Oysa aralarında kritik bir fark var.
Sanitize (temizleme): Veriyi sisteminize alırken uyguladığınız işlemdir. Kullanıcıdan gelen ham veriyi veritabanına yazmadan önce zararlı içerikten arındırırsınız. Düşünün ki bir kapıda güvenlik görevlisi var; içeri girecek her şeyi kontrol ediyor, tehlikeli görünenleri çıkarıyor.
Escape (kaçış): Veriyi sisteminizden çıkarırken, yani ekrana basarken uyguladığınız işlemdir. Veritabanından çektiğiniz ya da bir değişkende tuttuğunuz veriyi HTML, JavaScript veya SQL bağlamında güvenli hale getirirsiniz. Aynı metafor üzerinden gidersek: çıkışta da bir kontrol var, içeriden çıkacak her şeyin formata uygun olması gerekiyor.
Kural şu: Girişte sanitize, çıkışta escape. Bu iki adımı birlikte uyguladığınızda çoğu güvenlik açığının önüne geçmiş olursunuz.
Neden Her İkisi de Gerekli?
Sadece sanitize yeterli değil mi diye sorabilirsiniz. Değil. Şöyle düşünün: Bir metni veritabanına kaydederken zararlı HTML taglarını temizlediniz. Ama iki ay sonra o veriyi farklı bir bağlamda, mesela bir JavaScript değişkenine atayarak kullandınız. O noktada escape uygulamadıysanız yeni bir açık oluştu.
Ya da tam tersi: Sadece escape uyguladınız, sanitize etmediniz. Veriler veritabanında kirli biçimde duruyorsa, bir gün yanlışlıkla escape’siz bir yerde kullandığınızda felaket kaçınılmaz olur.
WordPress bu konuyu çok ciddiye almış ve her iki adım için kapsamlı fonksiyonlar sunuyor.
Sanitize Fonksiyonları
sanitize_text_field()
En sık kullandığınız fonksiyon bu olacak. Düz metin alanları için ideal. HTML taglarını, fazla boşlukları ve sekme karakterlerini temizler.
<?php
// Kullanıcıdan gelen isim alanını temizle
$isim = sanitize_text_field( $_POST['kullanici_isim'] );
// Senaryo: Eklenti ayar sayfasından gelen veri
function plugin_ayarlari_kaydet() {
if ( isset( $_POST['sirket_adi'] ) ) {
$sirket_adi = sanitize_text_field( $_POST['sirket_adi'] );
update_option( 'sirket_adi', $sirket_adi );
}
}
Dikkat edin: Bu fonksiyon çok katmanlı veri için uygun değil. Eğer dizi içinde dizi varsa her elementi ayrı ayrı işlemeniz gerekiyor.
sanitize_email()
E-posta adresleri için özel olarak tasarlanmış. Geçersiz karakterleri temizler ama adresin gerçekten var olup olmadığını kontrol etmez. Bunun için is_email() fonksiyonuyla birlikte kullanın.
<?php
function iletisim_formu_isle() {
$email_ham = $_POST['email'] ?? '';
$email_temiz = sanitize_email( $email_ham );
if ( ! is_email( $email_temiz ) ) {
wp_send_json_error( 'Geçersiz e-posta adresi.' );
return;
}
// Artık güvenle kullanabilirsiniz
update_user_meta( get_current_user_id(), 'iletisim_email', $email_temiz );
}
sanitize_url() ve esc_url_raw()
URL’ler için iki farklı seçenek var ve bunlar farklı amaçlara hizmet ediyor.
sanitize_url() (eski adıyla esc_url_raw()), URL’yi veritabanına kaydetmeden önce kullanılır. Protokolü kontrol eder, tehlikeli karakterleri temizler ama HTML encoding uygulamaz.
esc_url() ise ekrana basmadan önce kullanılır ve HTML-safe hale getirir.
<?php
// Veritabanına kaydetmeden önce
function profil_guncelle( $user_id, $data ) {
if ( isset( $data['website'] ) ) {
$website = esc_url_raw( $data['website'] );
update_user_meta( $user_id, 'website_url', $website );
}
}
// Veritabanından çekip gösterirken (escape kısmı)
function profil_goster( $user_id ) {
$website = get_user_meta( $user_id, 'website_url', true );
echo '<a href="' . esc_url( $website ) . '">Web Sitesi</a>';
}
wp_kses() ve wp_kses_post()
Belirli HTML taglarına izin vermeniz gerektiğinde kullanırsınız. Mesela kullanıcının kalın yazı, italik ve bağlantı ekleyebildiği ama script veya iframe ekleyemediği bir alan için idealdir.
wp_kses_post() WordPress’in editör bağlamında izin verdiği tagları kullanır. Çoğu içerik alanı için yeterli.
<?php
// Kullanıcının HTML girişine izin ver ama sınırlı tut
function yorum_kaydet( $yorum_metni ) {
// Sadece belirli taglara izin ver
$izinli_taglar = array(
'a' => array( 'href' => array(), 'title' => array() ),
'strong' => array(),
'em' => array(),
'p' => array(),
);
$temiz_yorum = wp_kses( $yorum_metni, $izinli_taglar );
return $temiz_yorum;
}
// Post içeriği için wp_kses_post() kullan
function icerik_kaydet( $icerik ) {
$temiz_icerik = wp_kses_post( $icerik );
return $temiz_icerik;
}
sanitize_key()
Veritabanı anahtarları, option isimleri, meta key değerleri için. Sadece küçük harf, rakam, tire ve alt çizgiye izin verir.
<?php
// Option key oluştururken
function ozel_option_kaydet( $key, $value ) {
$temiz_key = sanitize_key( $key );
$temiz_value = sanitize_text_field( $value );
// Artık güvenle kullanılabilir
update_option( 'eklentim_' . $temiz_key, $temiz_value );
}
// Kullanım
ozel_option_kaydet( 'API Key!!', 'abc123' );
// Kaydedilen: 'eklentim_api-key' => 'abc123'
absint() ve intval()
Sayısal değerler için bu ikisini sıkça kullanıyorum. absint() pozitif tam sayı döndürür, intval() ise negatif değerleri de kabul eder.
<?php
// Sayfa ID'si veya post ID'si alırken
function sayfa_icerigi_getir() {
$sayfa_id = absint( $_GET['sayfa_id'] ?? 0 );
if ( $sayfa_id === 0 ) {
return false;
}
return get_post( $sayfa_id );
}
// Fiyat veya miktar için
function siparis_isle( $post_data ) {
$miktar = absint( $post_data['miktar'] );
$indirim_orani = intval( $post_data['indirim'] ); // Negatif olabilir
// İşlem devam eder...
}
Escape Fonksiyonları
Şimdi çıkış tarafına geçelim. Escape fonksiyonları bağlama göre değişiyor. HTML mi, JavaScript mi, URL mi? Her biri için farklı fonksiyon kullanmalısınız.
esc_html()
HTML bağlamında metin gösterirken kullanın. <, >, &, " gibi karakterleri HTML entity’lerine çevirir. Script injection’a karşı temel savunma budur.
<?php
// Kötü uygulama - ASLA yapmayın
function kullanici_adi_goster_yanlis( $user_id ) {
$isim = get_user_meta( $user_id, 'display_name', true );
echo $isim; // Tehlikeli!
}
// Doğru uygulama
function kullanici_adi_goster( $user_id ) {
$isim = get_userdata( $user_id )->display_name;
echo esc_html( $isim );
}
// Template dosyasında kullanım
function ayarlar_sayfasi_render() {
$baslik = get_option( 'eklentim_baslik', 'Varsayılan Başlık' );
?>
<h2><?php echo esc_html( $baslik ); ?></h2>
<?php
}
esc_attr()
HTML attribute değerlerinde kullanın. value="", class="", data-id="" gibi yerlerde.
<?php
function input_alani_olustur( $field_name, $current_value ) {
$clean_name = sanitize_key( $field_name );
$clean_value = esc_attr( $current_value );
echo '<input type="text"
name="' . esc_attr( $clean_name ) . '"
value="' . $clean_value . '"
class="regular-text" />';
}
// WooCommerce ürün sayfasında özel field
function urun_ozel_field_goster( $product ) {
$renk = get_post_meta( $product->get_id(), '_urun_renk', true );
?>
<div class="urun-renk" data-renk="<?php echo esc_attr( $renk ); ?>">
<?php echo esc_html( $renk ); ?>
</div>
<?php
}
esc_js()
JavaScript içinde PHP değeri kullanmanız gerektiğinde. Bu fonksiyonu inline JavaScript için kullanın.
<?php
function js_konfigurasyonu_yazdir() {
$site_adi = get_bloginfo( 'name' );
$ajax_url = admin_url( 'admin-ajax.php' );
?>
<script type="text/javascript">
var eklentimConfig = {
siteAdi: '<?php echo esc_js( $site_adi ); ?>',
ajaxUrl: '<?php echo esc_js( $ajax_url ); ?>',
nonce: '<?php echo esc_js( wp_create_nonce( 'eklentim_nonce' ) ); ?>'
};
</script>
<?php
}
// wp_localize_script ile daha temiz bir yol (önerilen)
function script_verilerini_yukle() {
wp_enqueue_script( 'eklentim-script', plugin_dir_url( __FILE__ ) . 'js/eklentim.js', array('jquery'), '1.0', true );
wp_localize_script( 'eklentim-script', 'eklentimData', array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'eklentim_nonce' ),
'siteAdi' => get_bloginfo( 'name' ),
));
}
wp_localize_script() kullanmak genellikle daha güvenli ve temizdir. Ama bazen inline script kaçınılmaz oluyor, o zaman esc_js() şart.
esc_textarea()
Textarea elementleri için özel. HTML entity dönüşümü yapar ama satır sonlarını da korur.
<?php
function aciklama_formu_goster( $kayitli_aciklama ) {
?>
<textarea name="aciklama" rows="5" cols="50">
<?php echo esc_textarea( $kayitli_aciklama ); ?>
</textarea>
<?php
}
Prepared Statements: SQL Injection’a Karşı
Sanitize ve escape’in yanında SQL güvenliği de kritik. WordPress’in $wpdb->prepare() metodu SQL injection saldırılarına karşı birincil savunmanız.
<?php
global $wpdb;
// YANLIŞ - SQL injection açığı var
function kullanici_getir_yanlis( $username ) {
$sql = "SELECT * FROM {$wpdb->users} WHERE user_login = '$username'";
return $wpdb->get_row( $sql );
}
// DOĞRU - Prepared statement kullan
function kullanici_getir( $username ) {
global $wpdb;
$sql = $wpdb->prepare(
"SELECT * FROM {$wpdb->users} WHERE user_login = %s",
$username
);
return $wpdb->get_row( $sql );
}
// Birden fazla parametre
function siparisler_getir( $user_id, $durum ) {
global $wpdb;
$sql = $wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wc_orders
WHERE customer_id = %d
AND status = %s
LIMIT %d",
absint( $user_id ),
sanitize_text_field( $durum ),
100
);
return $wpdb->get_results( $sql );
}
%s string için, %d integer için, %f float için kullanılır. Parametre sayısını ve türünü doğru eşleştirin.
Nonce ile Güvenliği Tamamlayın
Sanitize ve escape tek başına yeterli değil. Form verisi işlerken nonce doğrulaması yapmadan CSRF saldırılarına açık kalırsınız.
<?php
// Form oluştururken nonce ekle
function ayar_formu_olustur() {
?>
<form method="post" action="">
<?php wp_nonce_field( 'eklentim_ayar_guncelle', 'eklentim_nonce' ); ?>
<input type="text" name="api_anahtari" />
<input type="submit" value="Kaydet" />
</form>
<?php
}
// Form işlerken nonce doğrula
function ayar_formu_isle() {
// Önce nonce kontrolü
if ( ! isset( $_POST['eklentim_nonce'] ) ||
! wp_verify_nonce( $_POST['eklentim_nonce'], 'eklentim_ayar_guncelle' ) ) {
wp_die( 'Güvenlik doğrulaması başarısız.' );
}
// Sonra yetki kontrolü
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( 'Bu işlem için yetkiniz yok.' );
}
// Şimdi güvenle sanitize edip kaydedebilirsiniz
$api_anahtari = sanitize_text_field( $_POST['api_anahtari'] );
update_option( 'eklentim_api_anahtari', $api_anahtari );
}
Gerçek Dünya Senaryosu: WooCommerce Sipariş Notu Eklentisi
Her şeyi bir araya getirelim. WooCommerce siparişlerine müşterinin özel not ekleyebildiği bir eklenti yapalım:
<?php
/**
* Sipariş notu ekleme fonksiyonu - Güvenli uygulama
*/
// Frontend'de formu göster
function musteri_not_formu( $order_id ) {
$mevcut_not = get_post_meta( $order_id, '_musteri_ozel_notu', true );
$nonce = wp_create_nonce( 'siparis_not_' . $order_id );
?>
<div class="siparis-not-alani">
<h3><?php esc_html_e( 'Siparişinize Not Ekleyin', 'eklentim' ); ?></h3>
<form method="post" class="siparis-not-formu">
<input type="hidden" name="siparis_id" value="<?php echo esc_attr( $order_id ); ?>" />
<input type="hidden" name="nonce" value="<?php echo esc_attr( $nonce ); ?>" />
<textarea name="siparis_notu" rows="4" maxlength="500">
<?php echo esc_textarea( $mevcut_not ); ?>
</textarea>
<button type="submit" name="not_kaydet">
<?php esc_html_e( 'Notu Kaydet', 'eklentim' ); ?>
</button>
</form>
</div>
<?php
}
// Form verisini işle
function musteri_not_isle() {
if ( ! isset( $_POST['not_kaydet'] ) ) {
return;
}
// Nonce doğrula
$order_id = absint( $_POST['siparis_id'] ?? 0 );
if ( ! wp_verify_nonce( $_POST['nonce'] ?? '', 'siparis_not_' . $order_id ) ) {
wc_add_notice( 'Güvenlik hatası. Lütfen tekrar deneyin.', 'error' );
return;
}
// Kullanıcı bu siparişin sahibi mi?
$order = wc_get_order( $order_id );
if ( ! $order || $order->get_customer_id() !== get_current_user_id() ) {
wc_add_notice( 'Bu siparişe erişim yetkiniz yok.', 'error' );
return;
}
// Notu sanitize et
$not_metni = sanitize_textarea_field( $_POST['siparis_notu'] ?? '' );
// Uzunluk kontrolü
if ( mb_strlen( $not_metni ) > 500 ) {
$not_metni = mb_substr( $not_metni, 0, 500 );
}
// Kaydet
update_post_meta( $order_id, '_musteri_ozel_notu', $not_metni );
wc_add_notice( 'Notunuz kaydedildi.', 'success' );
}
add_action( 'template_redirect', 'musteri_not_isle' );
// Notu admin panelinde göster
function admin_siparis_notu_goster( $order ) {
$not = get_post_meta( $order->get_id(), '_musteri_ozel_notu', true );
if ( empty( $not ) ) {
return;
}
echo '<div class="musteri-ozel-not">';
echo '<strong>' . esc_html__( 'Müşteri Notu:', 'eklentim' ) . '</strong>';
echo '<p>' . esc_html( $not ) . '</p>';
echo '</div>';
}
Bu örnekte her adımı nasıl uyguladığımıza bakın:
- Form oluştururken
esc_attr()veesc_textarea()kullandık - Form işlerken önce nonce, sonra yetki kontrolü yaptık
absint()ile sipariş ID’sini temizlediksanitize_textarea_field()ile notu temizledik- Gösterirken
esc_html()uyguladık
Hangi Fonksiyonu Ne Zaman Kullanacaksınız?
Kısa bir özet geçeyim, günlük işlerde aklınızda tutacağınız sıralama:
- Veritabanına yazarken:
sanitize_text_field(),sanitize_email(),sanitize_url(),wp_kses(),absint() - Veritabanından okuyup HTML’e yazarken:
esc_html(),esc_attr(),esc_url() - Textarea’ya yazarken:
esc_textarea() - JavaScript’e yazarken:
esc_js()veyawp_localize_script() - SQL sorgusunda:
$wpdb->prepare() - URL olarak kullanırken:
esc_url()(HTML) /esc_url_raw()(veritabanı)
Sonuç
Açıkcası bu konuyu ilk öğrendiğimde “bu kadar fonksiyon neden var ki, biri yetmez mi?” diye düşünmüştüm. Ama her fonksiyonun farklı bir bağlamı hedeflediğini kavradıkça mantık oturdu. HTML bağlamı, JavaScript bağlamı, URL bağlamı ve SQL bağlamı birbirinden farklı tehdit modelleri içeriyor.
WordPress’in bu fonksiyon setini sunması büyük nimet aslında. Sıfırdan güvenlik katmanı oluşturmak zorunda kalmıyorsunuz. Tek yapmanız gereken hangi bağlamda olduğunuzu bilmek ve doğru aracı seçmek.
Bir eklenti geliştirirken kendinize şu soruyu sorun: “Bu veri şu an nereye gidiyor?” Cevaba göre sanitize mi, escape mi, yoksa her ikisi birden mi uygulamanız gerektiğine karar verin. Bu alışkanlığı edindikten sonra XSS, SQL injection ve CSRF saldırılarının büyük çoğunluğunu otomatik olarak engellemiş olursunuz.
Kodun kalitesi sadece ne yaptığıyla değil, ne yapmadığıyla da ölçülür. Güvenli kod yazmak bazen can sıkıcı görünebilir, ama müşterinizin sitesi saldırıya uğradığında sizi aradığında can sıkıcı olan başka bir şeyin ne olduğunu çok iyi anlarsınız.
