نانس وردپرس یک لایه امنیتی است که اگر درست پیاده شود، جلوی یکی از شایع‌ترین حملات وب را می‌گیرد؛ ولی اگر اشتباه استفاده شود، توهم امنیت می‌سازد. من در بازبینی افزونه‌های اختصاصی و سایت‌های مشتری، بارها به فرم‌هایی برخورده‌ام که nonce داشتند ولی در برابر حمله Cross-Site Request Forgery — که در فارسی به «جعل درخواست بین‌سایتی» شناخته می‌شود — کاملاً بی‌دفاع بودند. علتش این بود که توسعه‌دهنده nonce را بدون درک کامل مکانیزم و بدون پیاده‌سازی سمت سرور، در فرم قرار داده بود.

اگر با مفاهیم پایه امنیت وردپرس آشنایی کمتری دارید، پیش از ادامه امنیت وردپرس چیست و چرا حیاتی است را بخوانید. این نوشته، لایه مکمل همان بحث است: از مکانیزم دقیق nonce و تفاوتش با توکن CSRF شروع می‌کنم و تا الگوهای پیاده‌سازی در فرم‌ها، AJAX و REST API پیش می‌روم. برای مرور چک‌لیستی مباحث پایه، راهنمای امنیت وردپرس برای مبتدیان نقطه شروع خوبی است.

نانس وردپرس دقیقاً چیست؟

Nonce در وردپرس، یک رشته هش کوتاه است که کنار فرم‌ها یا درخواست‌های AJAX قرار می‌گیرد و به سرور ثابت می‌کند درخواست از سمت کاربر معتبر سایت آمده، نه از یک سایت مهاجم. جالب اینجاست که نام nonce از عبارت «Number Used Once» گرفته شده ولی nonce وردپرس دقیقاً یک بار مصرف نیست؛ این ناهم‌خوانی در نام، خودش یکی از مبهم‌ترین نکاتی است که در پروژه‌های واقعی زیاد می‌بینم.

برای درک دقیق‌تر، باید سه ویژگی nonce وردپرس را بشناسید. ویژگی اول: nonce توسط هسته وردپرس تولید می‌شود و به یک بازه زمانی مشخص گره می‌خورد (پیش‌فرض ۲۴ ساعت). ویژگی دوم: هر nonce به یک شناسه مشخص (action) و به شناسه کاربر و زمان تعلق دارد؛ اگر هر کدام از این‌ها تغییر کند، nonce نامعتبر می‌شود. ویژگی سوم: nonce به‌تنهایی هیچ‌چیز را محافظت نمی‌کند؛ این شما هستید که باید در سمت سرور، اعتبارش را بررسی کنید. اگر این سه ویژگی را ندیدید، احتمالاً با یک پیاده‌سازی ناقص روبرو هستید.

نکته کلیدی دیگر این است که nonce وردپرس، در سمت کاربر ذخیره نمی‌شود و یک مقدار ثابت نیست که در همه بازدیدها یکی باشد. هر صفحه‌ای که باز می‌شود، nonce تازه‌ای تولید می‌کند که به زمان درخواست وابسته است. این ویژگی باعث می‌شود نفوذگر نتواند nonce یک کاربر را به‌طور دائمی داشته باشد و از آن برای حمله استفاده کند.

Nonce در وردپرس یک فیلد تزئینی نیست؛ قطعه‌ای از یک قرارداد دوطرفه است که اگر یک طرفش اجرا نشود، امنیت نفوذپذیر است.

مکانیزم داخلی nonce در هسته وردپرس

برای درک درست nonce، باید بدانید در پس‌صحنه چه اتفاقی می‌افتد. مکانیزم تولید nonce در وردپرس بر پایه سه مقدار است: action، شناسه کاربر، و یک بازه زمانی نیم‌ساعته. هسته وردپرس از ترکیب این سه مقدار با یک salt داخلی، یک هش md5 تولید می‌کند که همان nonce است. اگر هر یک از این سه مقدار تغییر کند، هش تغییر می‌کند و nonce قبلی نامعتبر می‌شود.

تولید nonce

تابع wp_create_nonce هش نهایی را تولید می‌کند. یک نکته مهم که در مستندات رسمی وردپرس هم کم‌رنگ گفته شده: nonce در هر بازه نیم‌ساعته یکسان است، ولی وردپرس برای مدت ۲۴ ساعت آن را معتبر می‌داند. یعنی nonce فعلی، nonce نیم‌ساعت بعد و nonce نیم‌ساعت قبل، همه معتبر هستند. این طراحی برای جلوگیری از مشکلات کاربری در لبه‌های زمانی است و ابزار اضافه‌ای به نفوذگر نمی‌دهد چون بازه اعتبار، همچنان کوچک است.

$nonce = wp_create_nonce( 'my_plugin_save_settings' );

این nonce به action مشخص گره خورده است. اگر مهاجم nonce فرم دیگری را پیدا کند و آن را در فرم شما استفاده کند، بررسی سمت سرور رد می‌شود چون action در دو طرف متفاوت است. این یکی از دلایلی است که در همه فرم‌ها و درخواست‌ها، باید action اختصاصی انتخاب کنید نه یک رشته عمومی مثل save.

بررسی nonce

در سمت سرور، تابع wp_verify_nonce اعتبار nonce را بررسی می‌کند:

if ( ! isset( $_POST['my_nonce'] ) ) {
    wp_die( esc_html__( 'Invalid request.', 'myplugin' ) );
}

$nonce = sanitize_text_field( wp_unslash( $_POST['my_nonce'] ) );

if ( ! wp_verify_nonce( $nonce, 'my_plugin_save_settings' ) ) {
    wp_die( esc_html__( 'Security check failed.', 'myplugin' ) );
}

دو نکته ظریف در این الگو وجود دارد که در پروژه‌های واقعی زیاد نادیده گرفته می‌شوند. نکته اول: قبل از استفاده از $_POST['my_nonce']، باید وجودش را بررسی کنید؛ اگر غیرموجود را به wp_verify_nonce بدهید، خطای PHP رخ می‌دهد. نکته دوم: از wp_unslash و sanitize_text_field استفاده کنید؛ وردپرس روی همه داده‌های ورودی، backslash اضافه می‌کند و بدون برداشتن این backslash، مقایسه هش ممکن است شکست بخورد. اگر با اصول پاک‌سازی داده آشنا نیستید، پاک‌سازی داده‌ها در کدنویسی وردپرس تفاوت پاک‌سازی و اعتبارسنجی را توضیح می‌دهد.

تابع check_admin_referer و الگوی یک‌خطی

برای فرم‌های ساده که به‌طور کامل در صفحه پیشخوان اجرا می‌شوند، وردپرس تابع check_admin_referer را پیشنهاد می‌کند که ترکیب بررسی nonce و referer است:

check_admin_referer( 'my_plugin_save_settings' );

این تابع، اگر nonce نامعتبر باشد، به‌طور خودکار صفحه را با خطا متوقف می‌کند. برای فرم‌های پیشخوان، این الگو کوتاه‌ترین و امن‌ترین روش است. اما برای درخواست‌های AJAX یا REST، این تابع مناسب نیست چون referer همیشه قابل اتکا نیست. در همین‌جا یکی از اشتباهات رایج پروژه‌ها را یادآوری کنم: استفاده از check_admin_referer در درخواست‌های AJAX، در بعضی مرورگرها و در بعضی محیط‌های CDN، به‌اشتباه شکست می‌خورد چون referer در درخواست‌های AJAX ممکن است ارسال نشود.

چرا CSRF خطرناک است و nonce چگونه جلویش را می‌گیرد؟

برای فهم ارزش nonce، باید ابتدا خطر CSRF را درک کنید. حمله CSRF، که در مخفف Cross-Site Request Forgery است و در مرجع فنی وب به‌عنوان Cross-Site Request Forgery شناخته می‌شود، به این شکل کار می‌کند: نفوذگر یک صفحه مخرب می‌سازد که در آن، یک درخواست به سایت قربانی جاسازی شده است. اگر کاربر سایت شما در همان لحظه، در سایت قربانی لاگین باشد، مرورگر به‌طور خودکار کوکی‌های احراز هویت را به همراه درخواست مخرب ارسال می‌کند و سایت شما، درخواست را معتبر می‌داند.

یک مثال عینی که در پروژه‌های واقعی به آن برخورده‌ام: فرم تغییر رمز عبور در پیشخوان وردپرس. اگر مهاجم بتواند کاربر مدیر را به صفحه‌ای مخرب بکشاند که در پس‌صحنه، درخواست تغییر رمز را به سایت قربانی ارسال می‌کند، آن کاربر بدون اینکه بفهمد چه اتفاقی افتاده، رمز عبورش به رمز انتخابی مهاجم تغییر می‌کند. nonce دقیقاً همین‌جا وارد می‌شود: چون مهاجم نمی‌تواند nonce معتبر کاربر را از یک سایت دیگر تولید کند، درخواستش در سمت سرور رد می‌شود.

چرا کوکی و هدر امن نیستند

خیلی از توسعه‌دهندگان فکر می‌کنند چون وردپرس از کوکی برای احراز هویت استفاده می‌کند و کوکی روی دامنه دیگری قابل خواندن نیست، پس حمله CSRF امکان‌پذیر نیست. این تصور غلط است. حمله CSRF نیازی به خواندن کوکی ندارد؛ فقط کافی است مرورگر، به‌طور خودکار کوکی را در درخواست به سایت قربانی بفرستد. مرورگر این کار را به‌طور پیش‌فرض انجام می‌دهد چون کوکی به دامنه مقصد تعلق دارد، نه به دامنه مبدأ.

همین منطق در مورد هدرهای سفارشی مثل X-Requested-With هم صدق می‌کند. بعضی افزونه‌ها به بررسی این هدر اتکا می‌کنند ولی این روش امن نیست چون در بعضی سناریوها این هدر قابل جعل است. nonce تنها لایه‌ای است که به‌طور قطعی جلوی CSRF را می‌گیرد، چون مهاجم نمی‌تواند آن را از پیش ببیند. اگر روی تکنیک‌های دیگر امنیتی کار می‌کنید، امن‌سازی نشست‌های کاربری مکمل خوبی است.

کوکی، شناسنامه کاربر است؛ nonce، مهر و امضای درخواست. برای امنیت فرم، هر دو لازم است ولی هیچ‌کدام جای دیگری را نمی‌گیرد.

نانس وردپرس در برابر توکن CSRF استاندارد

یکی از سؤالاتی که در پروژه‌های حرفه‌ای زیاد می‌شنوم این است که «تفاوت nonce وردپرس با توکن CSRF استاندارد چیست؟» پاسخ کوتاه این است که هر دو هدف مشابهی دارند ولی در جزئیات پیاده‌سازی متفاوتند و این تفاوت‌ها در سناریوهای خاص، خودشان را نشان می‌دهند.

ویژگیNonce وردپرستوکن CSRF استاندارد
مبنای تولیدaction + کاربر + زمانتصادفی + ذخیره در session
طول عمر۲۴ ساعت با بازه ۱۲ ساعتهمعمولاً تا پایان session
ذخیره سمت سرورنیاز نیستالزاماً در session
مصرف چندبارهبله، تا پایان عمرمعمولاً یک‌بارمصرف
مناسب REST APIمحدودبسته به پیاده‌سازی

تفاوت کلیدی در این است که nonce وردپرس نیازی به ذخیره سمت سرور ندارد؛ هش از ترکیب سه مقدار تولید می‌شود و در سمت سرور، با محاسبه مجدد همان هش بررسی می‌شود. این طراحی در وردپرس، به‌دلیل حجم بالای درخواست‌ها و ماهیت چند کاربری سایت، کارایی بالاتری دارد چون از نوشتن و خواندن session صرف‌نظر می‌کند. در مقابل، توکن CSRF استاندارد که به session وابسته است، در معماری‌های stateless کاربرد ندارد.

از نظر امنیتی، هر دو رویکرد در برابر CSRF محافظت می‌کنند ولی با فرضیات متفاوت. nonce وردپرس به بازه زمانی اتکا می‌کند و در صورت لو رفتن یک nonce، مهاجم فقط تا پایان بازه اعتبارش می‌تواند از آن استفاده کند. توکن CSRF یک‌بارمصرف، در صورت لو رفتن، بلافاصله بی‌اعتبار می‌شود ولی پیاده‌سازی پیچیده‌تری دارد. برای اکثر سایت‌های وردپرسی، nonce انتخاب درستی است؛ برای APIهای حساس با معماری stateless، توکن استاندارد مناسب‌تر است.

پیاده‌سازی nonce در فرم‌های معمولی

پیاده‌سازی nonce در فرم‌های معمولی، ساده‌ترین و پرکاربردترین سناریوی nonce در وردپرس است. این الگو در فرم‌های تنظیمات افزونه، فرم‌های پیشخوان، و فرم‌های عمومی سایت استفاده می‌شود. الگوی استانداردی که در همه پروژه‌ها رعایت می‌کنم، سه بخش دارد: تولید در سمت نمایش، ارسال به همراه داده، و بررسی در سمت سرور.

تولید و نمایش nonce در فرم

برای نمایش nonce در فرم، تابع wp_nonce_field کار را ساده می‌کند:

<form method="post" action="">
    <?php wp_nonce_field( 'my_plugin_save_settings', 'my_nonce_field' ); ?>

    <label>
        <?php esc_html_e( 'Setting Name', 'myplugin' ); ?>
        <input type="text" name="setting_name" />
    </label>

    <?php submit_button( __( 'Save', 'myplugin' ) ); ?>
</form>

پارامتر اول این تابع، action است که در بررسی سمت سرور هم باید دقیقاً یکی باشد. پارامتر دوم، نام فیلد مخفی است؛ اگر آن را ندهید، نام پیش‌فرض _wpnonce استفاده می‌شود. توصیه من این است که همیشه نام فیلد را صریح مشخص کنید، چون در فرم‌هایی که چند nonce دارند (مثلاً فرم با دو بخش مستقل)، نام پیش‌فرض باعث تداخل می‌شود. اگر روی ساخت صفحه تنظیمات کار می‌کنید، ساخت صفحه تنظیمات اختصاصی در وردپرس این الگو را در بستر کامل‌تری نشان می‌دهد.

بررسی nonce در سمت سرور

بررسی nonce، به‌ترتیب زیر انجام می‌شود:

function myplugin_handle_form_submission() {
    if ( ! isset( $_POST['my_nonce_field'] ) ) {
        wp_die( esc_html__( 'Invalid request.', 'myplugin' ) );
    }

    $nonce = sanitize_text_field( wp_unslash( $_POST['my_nonce_field'] ) );

    if ( ! wp_verify_nonce( $nonce, 'my_plugin_save_settings' ) ) {
        wp_die( esc_html__( 'Security check failed.', 'myplugin' ) );
    }

    // ادامه پردازش فرم
}
add_action( 'admin_post_myplugin_save', 'myplugin_handle_form_submission' );

ترتیب این بررسی‌ها مهم است. اول بررسی وجود فیلد، بعد پاک‌سازی و بعد بررسی اعتبار. اگر ترتیب را جابه‌جا کنید، ممکن است با خطای PHP روبرو شوید یا در بررسی، از داده‌ای استفاده کنید که به‌درستی پاک‌سازی نشده است. نکته دیگر اینکه پس از بررسی nonce، باقی داده‌های فرم را هم باید اعتبارسنجی کنید. nonce فقط جلوی CSRF را می‌گیرد، ولی داده‌های خراب یا مخرب (مثلاً تزریق کد) نیاز به لایه دیگری از اعتبارسنجی دارند. تفاوت این دو لایه در اعتبارسنجی داده‌ها در کدنویسی وردپرس توضیح داده شده است.

استفاده از hook مناسب برای پردازش

برای فرم‌های پیشخوان، سه hook اصلی وجود دارد که nonce با آن‌ها همراه می‌شود: admin_post_{action} برای فرم‌های کاربران لاگین، admin_post_nopriv_{action} برای فرم‌های کاربران ناشناس، و admin_post برای همه. انتخاب hook مناسب، بخشی از معماری امنیتی است. اگر فرم باید فقط برای کاربران خاص اجرا شود، از admin_post_{action} استفاده کنید؛ اگر برای همه، از admin_post_nopriv_{action}. جزئیات کار با هوک‌ها در نحوه استفاده از add_action در وردپرس آمده است.

نانس در درخواست‌های AJAX

درخواست‌های AJAX، یکی از سناریوهای پرکاربرد nonce در وردپرس هستند و از نظر پیاده‌سازی، کمی پیچیده‌تر از فرم‌های معمولی هستند. چون درخواست‌های AJAX در پیشخوان از طریق فایل admin-ajax.php پردازش می‌شوند، nonce باید در دو طرف، به‌شکل متفاوتی مدیریت شود.

ارسال nonce از سمت کلاینت

در سمت کلاینت، nonce باید به‌عنوان بخشی از داده درخواست ارسال شود. الگوی استاندارد استفاده از wp_localize_script برای انتقال nonce به فایل جاوااسکریپت است:

function myplugin_enqueue_admin_scripts() {
    wp_enqueue_script(
        'myplugin-admin',
        plugin_dir_url( __FILE__ ) . 'assets/js/admin.js',
        [ 'jquery' ],
        MYPLUGIN_VERSION,
        true
    );

    wp_localize_script( 'myplugin-admin', 'mypluginData', [
        'ajaxUrl' => admin_url( 'admin-ajax.php' ),
        'nonce'   => wp_create_nonce( 'myplugin_ajax_action' ),
    ] );
}
add_action( 'admin_enqueue_scripts', 'myplugin_enqueue_admin_scripts' );

در فایل جاوااسکریپت، nonce به‌عنوان بخشی از داده ارسال می‌شود:

jQuery.ajax( {
    url: mypluginData.ajaxUrl,
    method: 'POST',
    data: {
        action: 'myplugin_ajax_action',
        nonce:  mypluginData.nonce,
        param1: value1,
    },
    success: function ( response ) {
        // پردازش پاسخ
    }
} );

نکته مهم در این الگو: نام فیلد nonce در جاوااسکریپت، با نامی که در سمت سرور بررسی می‌شود باید یکی باشد. عدم تطابق این دو، یکی از شایع‌ترین دلایل خطای «Security check failed» در پروژه‌ها است.

بررسی nonce در سمت سرور برای AJAX

در سمت سرور، الگوی بررسی مشابه فرم‌های معمولی است:

function myplugin_handle_ajax_request() {
    check_ajax_referer( 'myplugin_ajax_action', 'nonce' );

    $param1 = isset( $_POST['param1'] )
        ? sanitize_text_field( wp_unslash( $_POST['param1'] ) )
        : '';

    // پردازش درخواست

    wp_send_json_success( [ 'message' => 'OK' ] );
}
add_action( 'wp_ajax_myplugin_ajax_action', 'myplugin_handle_ajax_request' );

تابع check_ajax_referer در این حالت ترکیب بررسی nonce و referer است ولی در AJAX، به‌طور پیش‌فرض فقط nonce را بررسی می‌کند. اگر می‌خواهید فقط nonce را بررسی کنید (بدون referer)، از پارامتر چهارم این تابع استفاده کنید یا مستقیماً wp_verify_nonce را صدا بزنید. در پروژه‌های واقعی، من به‌طور پیش‌فرض از check_ajax_referer با پارامتر referer غیرفعال استفاده می‌کنم چون در محیط‌های CDN و proxy، referer گاهی تغییر می‌کند.

nonce در fetch API و جاوااسکریپت مدرن

اگر پروژه شما از fetch API یا جاوااسکریپت مدرن استفاده می‌کند، الگوی ارسال nonce کمی متفاوت است ولی اصول همان است:

async function saveSettings( formData ) {
    formData.append( 'action', 'myplugin_ajax_action' );
    formData.append( 'nonce', mypluginData.nonce );

    const response = await fetch( mypluginData.ajaxUrl, {
        method: 'POST',
        credentials: 'same-origin',
        body:   formData,
    } );

    return response.json();
}

پارامتر credentials: 'same-origin' تضمین می‌کند کوکی‌ها به همراه درخواست ارسال شوند؛ بدون این پارامتر، درخواست بدون کوکی ارسال می‌شود و وردپرس نمی‌تواند کاربر را شناسایی کند. این نکته در پروژه‌های مدرن که از fetch استفاده می‌کنند، بسیار حیاتی است.

نانس در REST API وردپرس

نانس در REST API وردپرس، الگوی متفاوتی دارد و یکی از جاهایی است که در پروژه‌های واقعی زیاد به آن برمی‌خورم. REST API از یک مکانیزم دیگر برای احراز هویت استفاده می‌کند — هدر X-WP-Nonce — ولی همچنان به nonce وابسته است.

ارسال nonce در هدر

در REST API، nonce به‌جای فیلد POST، در هدر HTTP ارسال می‌شود:

async function fetchPost( postId ) {
    const response = await fetch( `${ mypluginData.restUrl }/posts/${ postId }`, {
        method: 'DELETE',
        headers: {
            'Content-Type': 'application/json',
            'X-WP-Nonce':   mypluginData.nonce,
        },
        credentials: 'same-origin',
    } );

    return response.json();
}

نکته کلیدی این است که نام هدر باید دقیقاً X-WP-Nonce باشد. اگر نام دیگری انتخاب کنید، وردپرس آن را به‌عنوان nonce نمی‌شناسد. برای تولید این nonce، از همان تابع wp_create_nonce استفاده می‌شود ولی باید action آن با wp_rest یکسان باشد:

$rest_nonce = wp_create_nonce( 'wp_rest' );

این نکته ظریف، منبع اشتباهات زیادی در پروژه‌های واقعی است. اگر action را اشتباه انتخاب کنید، وردپرس nonce را نامعتبر می‌داند و درخواست شما با خطای ۴۰۳ رد می‌شود.

بررسی nonce در endpoints سفارشی

در endpointهای سفارشی REST، خودتان مسئول بررسی nonce هستید. الگوی استاندارد استفاده از permission_callback است:

register_rest_route( 'myplugin/v1', '/settings', [
    'methods'             => 'POST',
    'callback'            => 'myplugin_save_settings',
    'permission_callback' => function ( $request ) {
        return current_user_can( 'manage_options' );
    },
] );

در این الگو، بررسی احراز هویت به‌طور خودکار توسط وردپرس انجام می‌شود؛ چون nonce در هدر وجود دارد و وردپرس آن را بررسی می‌کند. permission_callback لایه دوم است که سطح دسترسی کاربر را چک می‌کند. نبود این callback در endpointهای سفارشی، یکی از شایع‌ترین مشکلات امنیتی افزونه‌های REST است. برای آشنایی عمیق‌تر با این معماری، REST API در وردپرس مسیر کامل را توضیح می‌دهد.

nonce در درخواست‌های خارج از وردپرس

اگر کلاینت شما خارج از مرورگر باشد — مثلاً یک اپلیکیشن موبایل یا سرور دیگر — nonce وردپرس مناسب نیست. چون nonce به کوکی کاربر وابسته است و در محیط بدون مرورگر، این کوکی وجود ندارد. برای این سناریو، از Application Passwords (رمزهای کاربردی) یا OAuth استفاده کنید. این تفکیک مهم است: nonce برای محافظت از CSRF در محیط مرورگر طراحی شده، نه برای احراز هویت API در محیط‌های خارجی.

Nonce وردپرس، ابزار محافظت از CSRF در محیط مرورگر است؛ برای احراز هویت API از راه دور، از ابزارهای مناسب‌تر استفاده کنید.

نانس در صفحه تنظیمات و متاباکس

صفحه تنظیمات و متاباکس، دو سناریوی پرکاربرد دیگر nonce در وردپرس هستند که هر کدام الگوی خاص خودشان را دارند. اگر با مکانیزم متاباکس آشنایی کمتری دارید، کار با متاباکس‌ها در کدنویسی وردپرس پیش‌نیاز خوبی است.

nonce در صفحه تنظیمات با Settings API

اگر از Settings API وردپرس برای ساخت صفحه تنظیمات استفاده می‌کنید، nonce به‌طور خودکار توسط settings_fields اضافه می‌شود:

<form method="post" action="options.php">
    <?php settings_fields( 'myplugin_settings_group' ); ?>
    <?php do_settings_sections( 'myplugin_settings' ); ?>
    <?php submit_button(); ?>
</form>

تابع settings_fields علاوه بر nonce، فیلد option_page و _wp_http_referer را هم اضافه می‌کند. نکته مهم این است که در این الگو، خودتان نیازی به بررسی دستی nonce ندارید چون وردپرس این کار را در فایل options.php انجام می‌دهد. اگر می‌خواهید پردازش فرم را به‌طور کامل خودتان انجام دهید، باید از الگوی دستی که در بخش فرم‌ها توضیح دادم استفاده کنید.

nonce در متاباکس

متاباکس‌ها فرم‌هایی هستند که در صفحه ویرایش نوشته یا برگه نمایش داده می‌شوند. الگوی nonce در متاباکس مشابه فرم‌های معمولی است ولی نکته ظریفی دارد: هنگام ذخیره نوشته، اگر nonce معتبر نباشد، باید ذخیره‌سازی متاباکس متوقف شود ولی خود نوشته همچنان ذخیره شود. الگوی استاندارد:

function myplugin_save_metabox( $post_id ) {
    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
        return;
    }

    if ( ! isset( $_POST['myplugin_meta_nonce'] ) ) {
        return;
    }

    $nonce = sanitize_text_field( wp_unslash( $_POST['myplugin_meta_nonce'] ) );

    if ( ! wp_verify_nonce( $nonce, 'myplugin_metabox_save' ) ) {
        return;
    }

    if ( ! current_user_can( 'edit_post', $post_id ) ) {
        return;
    }

    // ذخیره داده‌های متاباکس
}
add_action( 'save_post', 'myplugin_save_metabox' );

سه شرط در این الگو وجود دارد: Autosave، nonce، و سطح دسترسی. نبود هرکدام، یا باعث ذخیره‌سازی نامناسب می‌شود یا حفره امنیتی ایجاد می‌کند. شرط Autosave مهم است چون در Autosave، فرم به‌طور کامل ارسال نمی‌شود و nonce ممکن است نامعتبر باشد.

دام‌های رایج در پیاده‌سازی nonce

در بازبینی افزونه‌های اختصاصی و پروژه‌های مشتری، چند دام تکرارشونده دیده‌ام که هر کدام به‌شکل متفاوتی امنیت را تضعیف می‌کنند. این فهرست، چک‌لیست پیش از انتشار افزونه‌های من است.

دام اول: nonce بدون بررسی سمت سرور

رایج‌ترین دام. توسعه‌دهنده با wp_nonce_field nonce را در فرم قرار می‌دهد ولی در سمت سرور، آن را بررسی نمی‌کند. این وضعیت، بدترین حالت است چون هم توهم امنیت می‌سازد و هم شما را از بررسی‌های واقعی باز می‌دارد. الگوی درست، همیشه باید شامل wp_verify_nonce یا check_admin_referer در سمت سرور باشد. این دام در پروژه‌های تازه‌کار رایج است و در بازبینی‌های واقعی، درصد قابل توجهی از افزونه‌های مبتدی را درگیر خود می‌کند.

دام دوم: استفاده از action مشترک در چند فرم

اگر action را در همه فرم‌های افزونه یکی بگذارید، nonce یک فرم در فرم دیگر هم معتبر است. این یعنی مهاجم می‌تواند nonce را از فرم کم‌حساس بگیرد و در فرم حساس استفاده کند. الگوی درست، استفاده از action اختصاصی برای هر فرم است. مثلاً myplugin_save_settings، myplugin_delete_item، myplugin_update_status — هر کدام یک action مستقل.

دام سوم: انتقال nonce از طریق URL

در بعضی پروژه‌ها، nonce به‌جای فیلد POST، در URL ارسال می‌شود. این روش کار می‌کند ولی مشکل امنیتی دارد: URL در لاگ‌های سرور، در تاریخچه مرورگر، و در Referer هدرها ذخیره می‌شود. اگر nonce از طریق URL جایی لو برود، مهاجم می‌تواند تا پایان عمر nonce از آن استفاده کند. توصیه من: همیشه nonce را از طریق فیلد POST یا هدر HTTP ارسال کنید، نه از طریق URL.

دام چهارم: عدم پاک‌سازی nonce قبل از بررسی

وردپرس روی همه ورودی‌های $_GET، $_POST و $_COOKIE backslash اضافه می‌کند. اگر قبل از wp_verify_nonce از wp_unslash استفاده نکنید، هش مقایسه‌شده با هش اصلی متفاوت می‌شود و بررسی به‌طور کاذب شکست می‌خورد. این دام در بعضی افزونه‌های امنیتی که خودشان backslash را دستی حذف می‌کنند، دو بار رخ می‌دهد و باعث رفتار غیرقابل پیش‌بینی می‌شود.

دام پنجم: استفاده از nonce برای محافظت از endpointهایی که باید احراز هویت شوند

nonce جلوی CSRF را می‌گیرد ولی احراز هویت نیست. اگر endpoint شما باید فقط برای کاربران با نقش خاص اجرا شود، nonce تنها کافی نیست. باید لایه current_user_can هم اضافه کنید. تجربه میدانی من این است که حدود نیمی از افزونه‌های تازه‌کار، این لایه را نادیده می‌گیرند. مثال الگوی درست:

if ( ! current_user_can( 'manage_options' ) ) {
    wp_die( esc_html__( 'Insufficient permissions.', 'myplugin' ) );
}

nonce و سطح دسترسی، دو لایه مکمل هستند: nonce ثابت می‌کند درخواست از سمت کاربر معتبر است، و current_user_can ثابت می‌کند آن کاربر مجاز به انجام این عمل است. اشتباهات رایج امنیتی که در همین حوزه رخ می‌دهد، در اشتباهات امنیتی رایج در وردپرس به‌طور گسترده‌تر بررسی شده است.

دام ششم: نادیده گرفتن بازه اعتبار در سناریوهای طولانی

اگر فرمی دارید که کاربر ممکن است چند ساعت یا چند روز آن را باز بگذارد — مثل فرمی که در یک تب مرورگر رها می‌شود — nonce ممکن است در فاصله بین باز کردن فرم و ارسال، منقضی شود. وردپرس در این حالت پیام «Are you sure you want to do this?» را نشان می‌دهد ولی تجربه کاربری مطلوبی نیست. راه‌حل، رفرش دوره‌ای nonce با AJAX یا اطلاع‌رسانی به کاربر قبل از ارسال است. این نکته در فرم‌های طولانی مثل فرم‌های ثبت سفارش یا تنظیمات پیچیده، اهمیت بالایی دارد.

پرسش‌های پرتکرار درباره nonce وردپرس

تفاوت nonce وردپرس با توکن CSRF استاندارد چیست؟ هر دو هدف مشابهی دارند ولی مکانیزم متفاوتی. nonce وردپرس بر پایه ترکیب action، شناسه کاربر و زمان تولید می‌شود و نیازی به ذخیره سمت سرور ندارد. توکن CSRF استاندارد به session وابسته است و معمولاً یک‌بارمصرف است. انتخاب بین این دو، بر اساس معماری پروژه انجام می‌شود.

آیا nonce باید یک بار مصرف باشد؟ در وردپرس نه. nonce تا پایان عمر اعتبارش (حدود ۲۴ ساعت) می‌تواند استفاده شود. این طراحی برای کارایی بالا و سازگاری با حالت‌های مختلف درخواست‌ها انجام شده. اگر به‌دنبال nonce یک‌بارمصرف هستید، وردپرس این امکان را به‌طور بومی ارائه نمی‌دهد و باید خودتان با ترکیب nonce و یک ردیف در دیتابیس آن را پیاده‌سازی کنید.

چرا با اینکه nonce در فرم قرار داده‌ام، باز هم هک می‌شوم؟ سه دلیل عمده. اول، nonce را در سمت سرور بررسی نمی‌کنید. دوم، nonce را برای محافظت از عملیاتی استفاده می‌کنید که نیاز به بررسی سطح دسترسی هم دارد. سوم، پروژه شما حفره‌های امنیتی دیگری دارد که nonce پوشش نمی‌دهد. برای بررسی امنیتی جامع، چگونه امنیت وردپرس را تقویت کنیم راهنمای کامل است.

آیا می‌توانم nonce را در کوکی ذخیره کنم؟ نه. nonce باید در هر صفحه‌ای که فرم یا درخواست AJAX وجود دارد، تازه تولید شود. ذخیره در کوکی باعث می‌شود nonce ثابت بماند و مدت اعتبارش طولانی شود که ریسک امنیتی دارد. الگوی درست، تولید nonce در همان صفحه با wp_create_nonce و نمایش یا ارسال آن به‌عنوان بخشی از درخواست است.

آیا nonce برای کاربران مهمان هم کار می‌کند؟ بله، ولی با تفاوت. برای کاربران لاگین، nonce با شناسه کاربر گره می‌خورد. برای کاربران مهمان، شناسه کاربر صفر است و nonce بر پایه زمان و action تولید می‌شود. این یعنی nonce کاربران مهمان، در بازه زمانی مشخص برای همه یکسان است، که ریسک امنیتی آن را بالا می‌برد. توصیه من: برای فرم‌های کاربران مهمان، لایه‌های امنیتی بیشتری مثل CAPTCHA و Rate Limit اضافه کنید.

چرا در AJAX خطای «Security check failed» می‌گیرم؟ چهار دلیل رایج. اول، action nonce در سمت کلاینت و سرور یکی نیست. دوم، nonce به‌درستی پاک‌سازی نشده (backslash حذف نشده). سوم، nonce در سمت کلاینت قدیمی شده (کاربر مدتی در صفحه مانده). چهارم، پارامتر credentials در fetch به‌درستی تنظیم نشده و کوکی‌ها ارسال نمی‌شوند. برای دیباگ، ابتدا مطمئن شوید nonce در سمت کلاینت تولید می‌شود و همان مقدار به سرور می‌رسد.

آیا nonce می‌تواند جلوی حمله XSS را هم بگیرد؟ نه. nonce برای مقابله با CSRF طراحی شده، نه XSS. اگر سایت شما در برابر XSS آسیب‌پذیر باشد، مهاجم می‌تواند nonce معتبر را از صفحه کاربر بخواند و از آن برای حملات دیگر استفاده کند. برای مقابله با XSS، باید داده‌ها را در زمان نمایش با توابع مناسب پاک‌سازی کنید. این تفکیک، یکی از اصول پایه امنیت وب است.

چرا nonce در بعضی مرورگرها یا محیط‌های CDN به‌طور غیرمنتظره شکست می‌خورد؟ در محیط‌های CDN، اگر صفحه‌ای کش شود و nonce در آن قدیمی باشد، کاربر nonce قدیمی را ارسال می‌کند و بررسی شکست می‌خورد. در بعضی مرورگرهای قدیمی، رفتار کوکی در درخواست‌های cross-origin متفاوت است و می‌تواند باعث عدم ارسال nonce شود. راه‌حل، اطمینان از عدم کش شدن صفحات دارای nonce است.

آیا با هر بازدید، باید nonce تازه تولید شود؟ بله، در همان لحظه تولید صفحه. ولی چون nonce در بازه‌های نیم‌ساعته یکسان است، تازه تولید کردن در بازه‌های کمتر از نیم ساعت، همان nonce را می‌دهد. برای همین، نیازی به کش کردن nonce در سشن یا کوکی نیست — تولید مجدد، هزینه‌ای ندارد.

آیا nonce برای محافظت از درخواست‌های DELETE و PUT هم کاربرد دارد؟ بله، در REST API. برای درخواست‌های با متدهای غیر GET، nonce از طریق هدر X-WP-Nonce ارسال می‌شود و وردپرس آن را بررسی می‌کند. این الگو در پروژه‌های Headless WordPress بسیار کاربرد دارد. نکته مهم این است که وردپرس به‌طور پیش‌فرض، متدهای GET را با nonce محافظت نمی‌کند چون فرض بر این است که متد GET نباید عملیات حساس انجام دهد.

چگونه بفهمم nonce در سایت من به‌درستی پیاده شده است؟ چند نشانه ساده. اول، در زمان ارسال فرم، اگر nonce نامعتبر باشد، پیام خطا نمایش داده می‌شود. دوم، اگر nonce را از فیلد فرم حذف کنید و فرم را ارسال کنید، باید خطا بگیرید. سوم، در لاگ سرور، درخواست‌های مشکوک به‌عنوان failed security check لاگ می‌شوند. این سه نشانه، تأیید می‌کنند که nonce در سمت سرور به‌درستی بررسی می‌شود.

نانس، ابزار دقیق نه چکش

در پایان این مسیر، یک حقیقت را باید بپذیریم: nonce وردپرس یکی از دقیق‌ترین لایه‌های امنیتی سایت است، ولی این دقت به‌شرطی کار می‌کند که با درک درست پیاده شود. من در بازبینی‌های واقعی، دو دسته توسعه‌دهنده دیده‌ام: دسته اول کسانی که nonce را در همه‌جا می‌گذارند بدون اینکه بدانند کجا لازم است و کجا نیست؛ دسته دوم کسانی که چون نمی‌دانند چطور کار می‌کند، از آن استفاده نمی‌کنند. هر دو دسته، امنیت سایت را از دست می‌دهند ولی به‌شکل‌های متفاوت.

در پروژه‌های خودم، سه اصل ساده را رعایت می‌کنم که در طول این نوشته به‌تدریج باز کردم. اصل اول: هر عملیات نوشتن باید nonce داشته باشد ولی هر عملیات خواندن نه. اصل دوم: nonce همیشه در ترکیب با بررسی سطح دسترسی استفاده شود، نه به‌جای آن. اصل سوم: nonce به‌تنهایی محافظت نیست، بخشی از یک زنجیره امنیتی است که شامل اعتبارسنجی ورودی، پاک‌سازی خروجی، و منطق کسب‌وکار امن می‌شود. این سه اصل، چارچوب تصمیم‌گیری من در همه پروژه‌ها است.

اگر در ابتدای مسیر یادگیری هستید، سه تمرین را پیشنهاد می‌کنم. اول، یک افزونه ساده با یک فرم بسازید و با حذف nonce از فیلد مخفی، ببینید فرم چه رفتاری می‌کند و چطور می‌شود آن را در سمت سرور تشخیص داد. دوم، یک درخواست AJAX ساده با و بدون nonce ارسال کنید و رفتار وردپرس را در دو حالت مقایسه کنید. سوم، در یک محیط staging، nonce یک فرم را با DevTools مرورگر دست‌کاری کنید و ببینید چه اتفاقی می‌افتد. این سه تمرین، در چند ساعت، درک عمیقی از nonce به شما می‌دهد که هیچ مقاله‌ای جایگزینش نمی‌شود.

اگر تجربه‌ای از پیاده‌سازی nonce در پروژه‌ای واقعی دارید — چه با موفقیت، چه با دام‌های غیرمنتظره — در دیدگاه بنویسید. آن تجربه برای کسی که همین امروز تصمیم می‌گیرد امنیت فرم‌های سایتش را تقویت کند یا نه، ارزشمندتر از هر مستند رسمی است. 🔐