Nonce Kullanımı: WordPress Form Güvenliği Rehberi

WordPress eklenti geliştirirken en sık atlanan güvenlik katmanlarından biri nonce kullanımı. “Çalışıyor ya, ne gerek var?” diye düşünenler var, anlıyorum. Ben de yıllar önce öyle düşünürdüm. Ta ki bir müşterimin sitesinde CSRF saldırısı görenize kadar. O günden sonra her form için nonce, her istek için doğrulama. Bu yazıda nonce’ın ne olduğunu, nasıl çalıştığını ve gerçek dünya senaryolarında nasıl kullanmanız gerektiğini anlatacağım.

Nonce Nedir ve Neden Önemlidir

Nonce, “number used once” ifadesinin kısaltması. WordPress bağlamında ise tek kullanımlık doğrulama tokeni olarak tanımlayabiliriz. Teknik olarak baktığında, WordPress nonce’ları gerçek anlamda tek kullanımlık değil, zaman sınırlı ve kullanıcıya özgü tokenlar. 24 saatlik bir pencerede geçerliler ve aynı pencere içinde birden fazla kullanılabilirler.

Asıl korudukları şey Cross-Site Request Forgery (CSRF) saldırıları. Kullanıcının tarayıcısı üzerinden yetkisiz istek gönderilmesini engeller. Örneğin, kullanıcınız WordPress yönetim paneline giriş yapmış ve aynı anda kötü niyetli bir siteyi ziyaret ediyorsa, o site kullanıcının tarayıcısı üzerinden sitenize istek gönderebilir. Nonce olmadan bu istek geçerli sayılır.

Nonce kullanımının temel mantığı şu:

  • Sunucu, kullanıcıya özel bir token üretir
  • Bu token forma veya isteğe eklenir
  • Form gönderildiğinde token sunucuda doğrulanır
  • Token geçersizse işlem reddedilir

Temel Nonce Fonksiyonları

WordPress’in sunduğu nonce API’si oldukça kapsamlı. Hangi fonksiyonun ne zaman kullanılacağını bilmek önemli.

wp_create_nonce()

En temel nonce üretme fonksiyonu:

// Basit bir nonce oluşturma
$my_nonce = wp_create_nonce( 'my-action-name' );

// Kullanıcıya özgü nonce
$user_nonce = wp_create_nonce( 'delete-post-' . $post_id );

Aksiyon adı burada kritik. Genel bir isim koymak yerine, işleme ve ilgili nesneye özel bir isim kullanın. “my-action” yerine “delete-user-comment-42” gibi.

wp_verify_nonce()

Nonce doğrulama fonksiyonu. Dönüş değerine dikkat edin, boolean değil integer:

$result = wp_verify_nonce( $nonce_value, 'my-action-name' );

// $result değerleri:
// false  - Nonce geçersiz
// 1      - 0-12 saat arasında üretilmiş, geçerli
// 2      - 12-24 saat arasında üretilmiş, hala geçerli ama eski

if ( false === $result ) {
    wp_die( 'Güvenlik kontrolü başarısız.' );
}

// Daha sıkı kontrol için
if ( 1 !== $result ) {
    // Token eski, kullanıcıyı yenile
    wp_send_json_error( array( 'message' => 'Token süresi dolmuş, sayfayı yenileyin.' ) );
}

check_admin_referer()

Admin paneli formları için kullanılır. Hem nonce doğrulaması hem de referer kontrolü yapar. Başarısız olursa otomatik olarak wp_die() çağırır:

// Form işleyicisinde
check_admin_referer( 'bulk-action-nonce', '_wpnonce' );
// Buraya gelirsek doğrulama başarılı

check_ajax_referer()

AJAX istekleri için özelleştirilmiş versiyon:

// AJAX handler'da
check_ajax_referer( 'my-ajax-nonce', 'security' );

wp_nonce_field()

HTML form içine doğrudan hidden input ekler:

// Form içinde
wp_nonce_field( 'save-settings-action', 'settings_nonce' );

// Çıktı:
// <input type="hidden" id="settings_nonce" name="settings_nonce" 
//        value="ab12cd34ef">
// <input type="hidden" name="_wp_http_referer" value="/wp-admin/...">

wp_nonce_url()

URL’e nonce parametresi ekler:

$delete_url = wp_nonce_url( 
    admin_url( 'admin.php?action=delete-item&item_id=42' ),
    'delete-item-42',
    '_wpnonce'
);
// Çıktı: https://site.com/wp-admin/admin.php?action=delete-item&item_id=42&_wpnonce=ab12cd34

Gerçek Dünya Senaryo 1: Admin Panel Ayar Formu

Diyelim ki bir eklenti geliştirdiniz ve ayarlar sayfası var. Bu sayfada API anahtarı, e-posta bildirimleri gibi hassas bilgiler kaydediliyor. İşte güvenli bir implementasyon:

<?php
// Ayarlar sayfasını render eden fonksiyon
function my_plugin_settings_page() {
    // Önce form gönderildi mi kontrol et
    if ( isset( $_POST['submit_settings'] ) ) {
        // Nonce doğrula
        if ( ! isset( $_POST['my_plugin_settings_nonce'] ) || 
             ! wp_verify_nonce( $_POST['my_plugin_settings_nonce'], 'my_plugin_save_settings' ) ) {
            wp_die( 
                'Güvenlik kontrolü başarısız. Lütfen sayfayı yenileyip tekrar deneyin.',
                'Güvenlik Hatası',
                array( 'response' => 403, 'back_link' => true )
            );
        }

        // Yetki kontrolü de yapmayı unutma
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_die( 'Bu işlem için yetkiniz yok.' );
        }

        // Güvenli veri işleme
        $api_key = sanitize_text_field( $_POST['api_key'] ?? '' );
        $email   = sanitize_email( $_POST['notification_email'] ?? '' );
        
        update_option( 'my_plugin_api_key', $api_key );
        update_option( 'my_plugin_notification_email', $email );
        
        echo '<div class="notice notice-success"><p>Ayarlar kaydedildi.</p></div>';
    }

    // Mevcut değerleri al
    $api_key = get_option( 'my_plugin_api_key', '' );
    $email   = get_option( 'my_plugin_notification_email', '' );
    ?>
    <div class="wrap">
        <h1>Eklenti Ayarları</h1>
        <form method="post" action="">
            <?php wp_nonce_field( 'my_plugin_save_settings', 'my_plugin_settings_nonce' ); ?>
            
            <table class="form-table">
                <tr>
                    <th>API Anahtarı</th>
                    <td>
                        <input type="text" name="api_key" 
                               value="<?php echo esc_attr( $api_key ); ?>" 
                               class="regular-text">
                    </td>
                </tr>
                <tr>
                    <th>Bildirim E-postası</th>
                    <td>
                        <input type="email" name="notification_email"
                               value="<?php echo esc_attr( $email ); ?>"
                               class="regular-text">
                    </td>
                </tr>
            </table>
            
            <input type="hidden" name="submit_settings" value="1">
            <?php submit_button( 'Ayarları Kaydet' ); ?>
        </form>
    </div>
    <?php
}

Burada dikkat edin: nonce doğrulaması yetki kontrolünden önce geliyor. Bu kasıtlı. Geçersiz bir token ile gelen isteği, kullanıcının yetkisi olsa bile işlememeniz gerekiyor.

Gerçek Dünya Senaryo 2: AJAX ile Asenkron İşlemler

Modern WordPress geliştirmede AJAX çok sık kullanılıyor. Nonce’ı JavaScript tarafına aktarmak ve doğrulamak kritik:

<?php
// Eklenti yüklenirken script'e nonce aktar
function my_plugin_enqueue_scripts() {
    wp_enqueue_script( 
        'my-plugin-js', 
        plugin_dir_url( __FILE__ ) . 'assets/js/my-plugin.js',
        array( 'jquery' ),
        '1.0.0',
        true
    );

    // Nonce'ı JavaScript'e aktar
    wp_localize_script( 'my-plugin-js', 'myPluginData', array(
        'ajax_url'    => admin_url( 'admin-ajax.php' ),
        'nonce'       => wp_create_nonce( 'my-plugin-ajax-nonce' ),
        'user_id'     => get_current_user_id(),
    ));
}
add_action( 'wp_enqueue_scripts', 'my_plugin_enqueue_scripts' );

// AJAX handler - giriş yapmış kullanıcılar için
function my_plugin_ajax_handler() {
    // Nonce kontrolü - başarısız olursa -1 döner ve die() çağırır
    check_ajax_referer( 'my-plugin-ajax-nonce', 'security' );

    // Yetki kontrolü
    if ( ! is_user_logged_in() ) {
        wp_send_json_error( array( 'message' => 'Giriş yapmanız gerekiyor.' ), 401 );
    }

    // İsteği işle
    $item_id = absint( $_POST['item_id'] ?? 0 );
    
    if ( ! $item_id ) {
        wp_send_json_error( array( 'message' => 'Geçersiz öğe ID.' ) );
    }

    // İşlemi gerçekleştir
    $result = do_something_with_item( $item_id );
    
    if ( $result ) {
        wp_send_json_success( array( 'message' => 'İşlem başarılı.' ) );
    } else {
        wp_send_json_error( array( 'message' => 'İşlem başarısız.' ) );
    }
}
add_action( 'wp_ajax_my_plugin_action', 'my_plugin_ajax_handler' );
// Giriş yapmamış kullanıcılar için
add_action( 'wp_ajax_nopriv_my_plugin_action', 'my_plugin_ajax_handler' );

JavaScript tarafı da şöyle görünmeli:

// my-plugin.js
jQuery(document).ready(function($) {
    $('#my-action-btn').on('click', function(e) {
        e.preventDefault();
        
        var itemId = $(this).data('item-id');
        
        $.ajax({
            url: myPluginData.ajax_url,
            type: 'POST',
            data: {
                action: 'my_plugin_action',
                security: myPluginData.nonce,  // Nonce burada gönderiliyor
                item_id: itemId
            },
            success: function(response) {
                if (response.success) {
                    alert(response.data.message);
                } else {
                    alert('Hata: ' + response.data.message);
                }
            },
            error: function(xhr) {
                if (xhr.status === 403) {
                    alert('Güvenlik doğrulaması başarısız. Sayfayı yenileyip tekrar deneyin.');
                }
            }
        });
    });
});

Gerçek Dünya Senaryo 3: Toplu İşlemler (Bulk Actions)

Özel post type’larınız için liste sayfasında toplu işlem yapıyorsanız, her aksiyon için ayrı nonce oluşturmanız gerekiyor:

<?php
// Toplu işlem formuna nonce ekle
function my_plugin_add_bulk_nonce( $which ) {
    global $typenow;
    
    if ( 'my_custom_post' !== $typenow ) {
        return;
    }
    
    wp_nonce_field( 'my_plugin_bulk_action', '_bulk_nonce' );
}
add_action( 'manage_posts_extra_tablenav', 'my_plugin_add_bulk_nonce' );

// Toplu işlemi handle et
function my_plugin_handle_bulk_action( $redirect_url, $action, $post_ids ) {
    if ( 'my_bulk_approve' !== $action ) {
        return $redirect_url;
    }
    
    // Nonce kontrolü
    $nonce = $_REQUEST['_bulk_nonce'] ?? '';
    if ( ! wp_verify_nonce( $nonce, 'my_plugin_bulk_action' ) ) {
        wp_die( 'Güvenlik kontrolü başarısız.', 403 );
    }
    
    $processed = 0;
    foreach ( $post_ids as $post_id ) {
        // Her post için yetki kontrolü
        if ( current_user_can( 'edit_post', $post_id ) ) {
            update_post_meta( $post_id, '_approved', true );
            $processed++;
        }
    }
    
    return add_query_arg( 
        array( 'my_bulk_done' => $processed ), 
        $redirect_url 
    );
}
add_filter( 'handle_bulk_actions-edit-my_custom_post', 'my_plugin_handle_bulk_action', 10, 3 );

Sık Yapılan Hatalar ve Çözümleri

Yıllar içinde code review yaparken gördüğüm yaygın hataları paylaşayım:

Hata 1: Nonce olmadan silme işlemi

Çok sık görüyorum bunu. URL tabanlı silme işlemleri:

// YANLIŞ - Nonce yok!
if ( isset( $_GET['delete_item'] ) ) {
    $item_id = absint( $_GET['delete_item'] );
    wp_delete_post( $item_id );
}

// DOĞRU
if ( isset( $_GET['delete_item'] ) && isset( $_GET['_wpnonce'] ) ) {
    $item_id = absint( $_GET['delete_item'] );
    
    if ( ! wp_verify_nonce( $_GET['_wpnonce'], 'delete-item-' . $item_id ) ) {
        wp_die( 'Geçersiz istek.' );
    }
    
    if ( current_user_can( 'delete_post', $item_id ) ) {
        wp_delete_post( $item_id );
    }
}

// Silme linki oluştururken
$delete_url = wp_nonce_url(
    add_query_arg( array( 'delete_item' => $item_id ), admin_url() ),
    'delete-item-' . $item_id
);

Hata 2: Nonce’ı yanlış yerde kontrol etmek

// YANLIŞ - Sanitize işleminden SONRA kontrol
$data = sanitize_text_field( $_POST['data'] );
if ( ! wp_verify_nonce( $_POST['_nonce'], 'my-action' ) ) {
    wp_die( 'Hata' );
}

// DOĞRU - Nonce ÖNCE kontrol edilmeli
if ( ! isset( $_POST['_nonce'] ) || 
     ! wp_verify_nonce( $_POST['_nonce'], 'my-action' ) ) {
    wp_die( 'Güvenlik kontrolü başarısız.' );
}
$data = sanitize_text_field( $_POST['data'] ?? '' );

Önbellek ve Nonce Uyumluluğu

Sayfa önbellekleme kullanan sitelerde (WP Rocket, W3 Total Cache vb.) nonce’lar sorun çıkarabilir. Sayfanın HTML’i önbelleklendiğinde, içindeki nonce de önbelleğe alınır ve bir süre sonra geçersiz hale gelir.

Çözüm: Nonce’ı önbelleğe alınan HTML yerine JavaScript üzerinden dinamik olarak yükleyin:

<?php
// REST API endpoint veya admin-ajax üzerinden nonce al
function my_plugin_get_fresh_nonce() {
    if ( ! is_user_logged_in() ) {
        wp_send_json_error( 'Giriş gerekli.', 401 );
    }
    
    wp_send_json_success( array(
        'nonce' => wp_create_nonce( 'my-plugin-ajax-nonce' ),
        'expires_in' => 43200 // 12 saat, saniye cinsinden
    ));
}
add_action( 'wp_ajax_get_plugin_nonce', 'my_plugin_get_fresh_nonce' );

// JavaScript'te: Önce nonce al, sonra asıl isteği gönder
// Özellikle uzun süre açık kalan sayfalar için
// veya nonce süresi dolduğunda yenileme mantığı ekle

Alternatif olarak nonce’ı inline script ile sayfaya gömmek yerine, JavaScript dosyasını önbellek dışında tutarak wp_localize_script ile aktarabilirsiniz. Ama bu da kendi sorunlarını yaratır. En temiz çözüm: AJAX işlemlerinde nonce hatası aldığınızda kullanıcıya “Sayfanız güncel değil, yenileniyor…” mesajı gösterip otomatik yenileme yapmak.

REST API ve Nonce

WordPress REST API kullanıyorsanız, nonce mekanizması biraz farklı çalışıyor:

<?php
// REST API için nonce
function my_plugin_rest_scripts() {
    wp_localize_script( 'my-plugin-js', 'wpApiSettings', array(
        'root'  => esc_url_raw( rest_url() ),
        'nonce' => wp_create_nonce( 'wp_rest' ),  // REST API için özel action
    ));
}
add_action( 'wp_enqueue_scripts', 'my_plugin_rest_scripts' );

// REST endpoint'te doğrulama
add_action( 'rest_api_init', function() {
    register_rest_route( 'my-plugin/v1', '/data', array(
        'methods'             => 'POST',
        'callback'            => 'my_plugin_rest_callback',
        'permission_callback' => function() {
            // REST API nonce'ı header üzerinden otomatik doğrulanır
            // current_user_can ile yetki kontrolü yapman yeterli
            return current_user_can( 'edit_posts' );
        },
    ));
});

JavaScript tarafında REST API çağrısı yaparken nonce’ı header olarak gönderin:

// Fetch API ile REST isteği
fetch(wpApiSettings.root + 'my-plugin/v1/data', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-WP-Nonce': wpApiSettings.nonce  // Header olarak gönder
    },
    body: JSON.stringify({ key: 'value' })
})
.then(response => {
    if (response.status === 403) {
        // Nonce geçersiz, sayfayı yenile
        location.reload();
        return;
    }
    return response.json();
})
.then(data => console.log(data));

Nonce Ömrünü Özelleştirme

Varsayılan nonce ömrü 24 saat. Bazı durumlarda bunu değiştirmek isteyebilirsiniz. Örneğin hassas finansal işlemler için daha kısa bir süre uygun olabilir:

<?php
// Nonce ömrünü 1 saate düşür
add_filter( 'nonce_life', function( $life ) {
    // Sadece belirli bir context'te düşür
    if ( defined( 'DOING_MY_SENSITIVE_OPERATION' ) && DOING_MY_SENSITIVE_OPERATION ) {
        return HOUR_IN_SECONDS; // 3600 saniye
    }
    return $life; // Diğer durumlarda değiştirme
});

Bu filtreyi dikkatli kullanın. Global olarak uygulamak, tüm WordPress admin işlemlerini etkiler. Mümkünse sadece kendi eklentinizin context’inde çalışmasını sağlayın.

Sonuç

Nonce kullanımı gerçekten öyle karmaşık bir şey değil. Birkaç satır kod ekleyip ciddi bir güvenlik açığını kapatıyorsunuz. Özetleyecek olursam:

  • Her form için wp_nonce_field() kullanın
  • Silme, düzenleme gibi kritik işlemlerin URL’lerine wp_nonce_url() ile nonce ekleyin
  • AJAX handler’larınızda check_ajax_referer() veya wp_verify_nonce() ile doğrulama yapın
  • Nonce doğrulamasını her zaman diğer işlemlerden önce, en başa koyun
  • Sadece nonce yetmez, current_user_can() ile yetki kontrolünü de atlamamın
  • Önbellekli sitelerde nonce sorunlarına hazırlıklı olun
  • REST API için X-WP-Nonce header kullanımını öğrenin

Eklenti geliştirirken sık sık duyduğum “Bu eklenti sadece admin kullanıyor, güvenliğe ne gerek var” argümanına katılmıyorum. Admin hesabı ele geçirilmiş olabilir, sosyal mühendislik saldırısına maruz kalmış olabilir, açık bir tarayıcı sekmesi olabilir. Savunma katmanlarını ne kadar çok koyarsanız, saldırganın işi o kadar zorlaşır. Nonce, bu katmanların en ucuz ve en etkili olanlarından biri.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir