پیاده‌سازی نانس در فرم‌های سفارشی وردپرس، جایی است که خیلی از توسعه‌دهندگان به‌نظر امن، در واقع آسیب‌پذیر می‌مانند. من در بازبینی پروژه‌هایی که فرم اختصاصی داشتند — از فرم درخواست قیمت گرفته تا فرم ثبت‌نام اعضای VIP — بارها به الگویی برخورده‌ام که ظاهراً درست به نظر می‌رسد ولی در لحظه حمله Cross-Site Request Forgery (جعل درخواست بین‌سایتی) از هم می‌پاشد. ریشه این مشکل همیشه یک چیز نیست: گاهی نانس در فرم قرار گرفته ولی در سمت سرور بررسی نمی‌شود، گاهی action اشتباه انتخاب شده و گاهی هم لایه سطح دسترسی کاملاً نادیده گرفته شده.

اگر با مکانیزم پایه نانس آشنایی کمتری دارید، پیش از ادامه نانس وردپرس و نقش آن در امنیت فرم‌ها را بخوانید؛ این نوشته لایه پیاده‌سازی را عمیق‌تر باز می‌کند. آنجا مکانیزم تولید نانس، تفاوتش با توکن CSRF استاندارد و اصول کلی را توضیح داده‌ام. اینجا وارد جزئیات می‌شویم: از فرم ساده پیشخوان تا فرم چندمرحله‌ای، آپلود فایل، AJAX و حتی فرم‌های کاربران مهمان که پیچیدگی خاص خودشان را دارند.

چرا فرم سفارشی با نانس پیچیده‌تر از فرم‌های استاندارد است؟

وقتی از Settings API وردپرس یا فرم‌های خود پیشخوان استفاده می‌کنید، هسته وردپرس بخش بزرگی از کار امنیتی را برای شما انجام می‌دهد. در فرم سفارشی — چه فرمی که خودتان در یک صفحه قالب می‌سازید، چه فرمی که در پیشخوان روی endpoint اختصاصی پردازش می‌شود — این بار کاملاً روی دوش شماست. تجربه‌ام می‌گوید در پروژه‌های واقعی، این جابه‌جایی مسئولیت، همان‌جایی است که امنیت از دست می‌رود.

سه دلیل اصلی این پیچیدگی را در بازبینی‌های واقعی دیده‌ام. دلیل اول: فرم سفارشی، مسیر پردازش استانداردی ندارد. در Settings API، فایل options.php هسته کار بررسی و ذخیره‌سازی را انجام می‌دهد و نانس را هم خودش چک می‌کند؛ در فرم سفارشی، شما مسئول هر خط هستید. دلیل دوم: فرم سفارشی معمولاً داده‌های پیچیده‌تری می‌فرستد — چند فیلد مرتبط، فایل، ساختار JSON و مشابه — و برای هر کدام باید لایه پاک‌سازی جداگانه‌ای بگذارید. دلیل سوم: گاهی فرم سفارشی روی درخواست‌های خارجی یا AJAX سوار می‌شود و همین باعث می‌شود که نانس در لایه‌های متعددی (کلاینت، هدر، سمت سرور) مدیریت شود.

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

کوکی، هویت را ثابت می‌کند؛ نانس، اراده را. فرم سفارشی بدون نانس، اراده کاربر را می‌دزدد، حتی اگر هویتش سالم باشد.

آناتومی درست یک نانس در فرم سفارشی

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

عنصر اول: انتخاب action اختصاصی

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

$action = 'myplugin_request_quote_form';
$nonce  = wp_create_nonce( $action );

این انتخاب که در نگاه اول ساده به‌نظر می‌رسد، یکی از مؤثرترین لایه‌های امنیتی است. هر action، یک دامنه اعتبار می‌سازد و نانس را محدود می‌کند.

عنصر دوم: قرارگیری درست فیلد مخفی

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

wp_nonce_field( 'myplugin_request_quote_form', 'myplugin_quote_nonce' );

در فرم‌های خیلی پیچیده که ممکن است چند نانس روی یک صفحه باشند (مثلاً فرم ثبت‌نام و فرم ورود در کنار هم)، این تفکیک نام فیلد، منبع اشتباهات را از بین می‌برد.

عنصر سوم: بررسی وجود قبل از بررسی اعتبار

در سمت سرور، اولین کاری که باید انجام دهید این است که بررسی کنید فیلد نانس وجود دارد یا نه. اگر این ترتیب را رعایت نکنید، با ارسال درخواستی بدون نانس، خطای PHP رخ می‌دهد:

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

این خط، در ظاهر ساده است ولی در بازبینی‌های واقعی بارها دیده‌ام که توسعه‌دهنده مستقیم به بررسی اعتبار می‌رود و در نتیجه، درخواست‌های خالی باعث fatal error یا رفتار غیرمنتظره در سرور می‌شوند.

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

وردپرس روی همه ورودی‌های $_GET، $_POST و $_COOKIE کاراکتر backslash اضافه می‌کند. این رفتار، بخشی از مکانیزم محافظت در برابر SQL Injection در دیتابیس است ولی اگر قبل از بررسی نانس از wp_unslash استفاده نکنید، مقایسه هش به‌طور کاذب شکست می‌خورد. الگوی درست:

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

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

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

عنصر پنجم: بررسی سطح دسترسی

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

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

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

پنج عنصر نانس، مثل پنج زنجیر یک قفل است؛ اگر یکی نباشد، قفل باز می‌شود، حتی اگر بقیه سالم باشند.

الگوی کامل فرم سفارشی با بررسی سمت سرور

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

سمت نمایش فرم

در سمت نمایش، فرم را به‌طور معمول می‌سازید و نانس را در آن قرار می‌دهید. یک نکته ظریف: از admin_url( 'admin-post.php' ) استفاده کنید نه مستقیم به فایل خودتان. این کار اجازه می‌دهد از هوک‌های امن وردپرس برای پردازش استفاده کنید:

<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
    <input type="hidden" name="action" value="myplugin_handle_quote" />

    <?php wp_nonce_field( 'myplugin_request_quote_form', 'myplugin_quote_nonce' ); ?>

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

    <label>
        <?php esc_html_e( 'Email', 'myplugin' ); ?>
        <input type="email" name="email" required />
    </label>

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

دقت کنید که فیلد action نقش متفاوتی از action نانس دارد: فیلد action به وردپرس می‌گوید این فرم باید به کدام هوک ارسال شود، و پارامتر action در wp_nonce_field شناسه امنیتی است. این دو نباید اشتباه گرفته شوند. اگر روی ساخت صفحه سفارشی کار می‌کنید، ساخت صفحه تنظیمات اختصاصی در وردپرس همین الگو را در بستر پیشخوان توضیح می‌دهد.

سمت پردازش فرم

در سمت سرور، پردازش فرم با هوک admin_post_myplugin_handle_quote انجام می‌شود. اگر فرم برای کاربران مهمان هم در دسترس است، باید هر دو هوک admin_post و admin_post_nopriv را ثبت کنید:

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

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

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

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

    $email = isset( $_POST['email'] )
        ? sanitize_email( wp_unslash( $_POST['email'] ) )
        : '';

    if ( empty( $full_name ) || ! is_email( $email ) ) {
        wp_die( esc_html__( 'Please fill the form correctly.', 'myplugin' ) );
    }

    myplugin_store_quote_request( $full_name, $email );

    wp_safe_redirect( add_query_arg( 'status', 'success', wp_get_referer() ) );
    exit;
}
add_action( 'admin_post_myplugin_handle_quote', 'myplugin_process_quote_request' );
add_action( 'admin_post_nopriv_myplugin_handle_quote', 'myplugin_process_quote_request' );

چند نکته ظریف در این الگو. اول، نانس قبل از هر کاری بررسی می‌شود، حتی قبل از بررسی سطح دسترسی. اگرچه در فرم‌های کاربران مهمان current_user_can معنا ندارد، ولی در فرم‌های پیشخوان این ترتیب، هزینه اضافه ندارد و امنیت را افزایش می‌دهد. دوم، بعد از پردازش موفق، از wp_safe_redirect استفاده می‌کنیم نه wp_redirect، چون تابع اول، URL را با لیست سفید بررسی می‌کند و جلوی حمله Open Redirect را می‌گیرد. سوم، بعد از redirect از exit استفاده می‌کنیم تا کد ادامه پیدا نکند. اشتباه در این مرحله، یکی از رایج‌ترین اشتباهاتی است که در نوشتن کد PHP امن برای وردپرس به آن پرداخته‌ام.

ترتیب هوک‌ها و بارگذاری

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

نانس در فرم‌های کاربران مهمان و ثبت‌نام

فرم‌های کاربران مهمان، از نظر مکانیزم نانس یک تفاوت مهم با فرم‌های کاربران لاگین دارند. نانس در وردپرس بر پایه شناسه کاربر و زمان تولید می‌شود؛ برای کاربران مهمان، شناسه کاربر صفر است. نتیجه اینکه نانس کاربران مهمان در بازه‌های نیم‌ساعته، برای همه کاربران مهمان یکسان می‌شود.

پیامدهای امنیتی این رفتار

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

الگوهای کاهش ریسک

سه لایه مکمل که در فرم‌های کاربران مهمان اضافه می‌کنم. لایه اول، CAPTCHA برای مقابله با ربات‌ها. لایه دوم، محدودیت نرخ بر اساس IP برای جلوگیری از ارسال انبوه. لایه سوم، تأیید ایمیل برای فرم‌های حساس مثل ثبت‌نام. الگوی تأیید ایمیل، عملاً از یک لایه امنیتی مجزا استفاده می‌کند که مستقل از نانس است.

یک نکته ظریف در فرم‌های ثبت‌نام: اگر از این فرم برای ساختن کاربر جدید استفاده می‌کنید، حتماً قبل از wp_insert_user، هم نانس را بررسی کنید و هم یک لایه محدودیت نرخ بر اساس IP اضافه کنید. تجربه‌ام این است که فرم‌های ثبت‌نام بدون Rate Limit، در هفته اول به هدف ربات‌های اسپم تبدیل می‌شوند. اگر با مکانیزم نشست کاربران آشنایی کمتری دارید، امن‌سازی نشست‌های کاربری لایه مرتبط را توضیح می‌دهد.

ترکیب نانس با Referer

در فرم‌های کاربران مهمان، بعضی توسعه‌دهندگان از ترکیب نانس و Referer استفاده می‌کنند. این رویکرد امنیت را بالا می‌برد ولی مشکل دارد: اگر سایت شما از طریق CDN یا proxy ارائه شود، Referer ممکن است تغییر کند یا کلاً ارسال نشود. توصیه من این است که در محیط‌های دارای CDN، به نانس و Rate Limit اتکا کنید، نه به Referer. اگر روی هاست خاصی کار می‌کنید، تاثیر هاست بر سرعت سایت چقدر است توضیح می‌دهد که این تفاوت‌ها از کجا ناشی می‌شود.

نانس در فرم‌های چندمرحله‌ای و ویزاردها

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

دو استراتژی مواجهه

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

الگوی تولید نانس در هر مرحله

الگوی استاندارد، تولید نانس اختصاصی برای هر مرحله است:

$step = absint( $_GET['step'] ?? 1 );

switch ( $step ) {
    case 2:
        wp_nonce_field( 'myplugin_wizard_step_2', 'myplugin_wizard_nonce' );
        break;
    case 3:
        wp_nonce_field( 'myplugin_wizard_step_3', 'myplugin_wizard_nonce' );
        break;
    default:
        wp_nonce_field( 'myplugin_wizard_step_1', 'myplugin_wizard_nonce' );
}

در سمت سرور، action نانس باید بر اساس مرحله فعلی بررسی شود:

$step  = absint( $_POST['step'] ?? 1 );
$nonce = sanitize_text_field( wp_unslash( $_POST['myplugin_wizard_nonce'] ?? '' ) );

$expected_action = 'myplugin_wizard_step_' . $step;

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

نکته امنیتی مهم در این الگو: مرحله جاری باید سمت کلاینت قابل تنظیم باشد، ولی بررسی نانس سمت سرور، بر اساس همان مرحله انجام شود. اگر مرحله را به‌عنوان ورودی کاربر در نظر بگیرید و بر اساس آن، action نانس را تغییر دهید، مهاجم می‌تواند action را دست‌کاری کند. راه‌حل، نگه‌داشتن وضعیت مراحل در سمت سرور و ارسال آن به‌عنوان بخشی از نانس است.

ذخیره موقت داده بین مراحل

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

متاباکس‌های چندمرحله‌ای

اگر فرم سفارشی شما در بستر متاباکس پیاده می‌شود، الگوی نانس مشابه است ولی باید دقت کنید که Autosave وردپرس، فرم شما را نیمه‌کاره ارسال نکند. نکات دقیق متاباکس در کار با متاباکس‌ها در کدنویسی وردپرس آمده است.

نانس در فرم‌های AJAX بدون بازخوانی صفحه

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

رویکرد اول: نانس در فیلد POST

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

async function submitForm( formData ) {
    formData.append( 'action', 'myplugin_submit_quote' );
    formData.append( 'nonce', mypluginData.nonce );

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

    return response.json();
}

نکته کلیدی در این رویکرد، پارامتر credentials: 'same-origin' است. بدون این پارامتر، مرورگر کوکی‌ها را به همراه درخواست ارسال نمی‌کند و وردپرس نمی‌تواند کاربر را شناسایی کند.

رویکرد دوم: نانس در هدر HTTP

در REST API وردپرس، نانس از طریق هدر X-WP-Nonce ارسال می‌شود. الگوی دقیق:

const response = await fetch( mypluginData.restUrl + '/quote', {
    method: 'POST',
    credentials: 'same-origin',
    headers: {
        'Content-Type': 'application/json',
        'X-WP-Nonce':   mypluginData.restNonce,
    },
    body: JSON.stringify( payload ),
} );

action نانس در این حالت باید دقیقاً wp_rest باشد. اگر با REST API آشنا نیستید، REST API در وردپرس مرور کاملی از این معماری دارد.

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

در رویکرد سوم که در پروژه‌های مدرن زیاد دیده می‌شود، نانس در تگ meta صفحه قرار می‌گیرد:

<meta name="myplugin-nonce" content="<?php echo esc_attr( wp_create_nonce( 'myplugin_form_action' ) ); ?>" />

در جاوااسکریپت، نانس از این متادیتا خوانده می‌شود:

const nonce = document.querySelector( 'meta[name="myplugin-nonce"]' ).content;

این رویکرد برای فرم‌هایی مناسب است که در چند صفحه استفاده می‌شوند. نقطه ضعفش این است که نانس در HTML صفحه قرار می‌گیرد و اگر صفحه‌ای کش شود، نانس قدیمی می‌ماند.

در فرم‌های AJAX، محل قرارگیری نانس به اندازه خودش مهم است؛ نانس در فیلد POST، هدر HTTP یا متادیتا، هر کدام سناریوی خودش را دارد.

نانس در فرم‌های آپلود فایل

فرم‌های آپلود فایل، از نظر پیاده‌سازی نانس یک تفاوت ظریف با فرم‌های معمولی دارند. تفاوت در این است که فرآیند آپلود ممکن است چند دقیقه طول بکشد و در این فاصله، نانس اولیه منقضی شود. این وضعیت در پروژه‌های واقعی، در سمت کاربر به‌عنوان خطای «Security check failed» ظاهر می‌شود.

الگوی درست آپلود با نانس

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

<form method="post" enctype="multipart/form-data" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
    <input type="hidden" name="action" value="myplugin_upload_file" />
    <?php wp_nonce_field( 'myplugin_upload_file_action', 'myplugin_upload_nonce' ); ?>

    <input type="file" name="my_file" required />
    <?php submit_button( __( 'Upload', 'myplugin' ) ); ?>
</form>

در سمت سرور، علاوه بر بررسی نانس، باید نوع فایل، اندازه و مسیر ذخیره‌سازی هم بررسی شود. نانس فقط ثابت می‌کند درخواست از سمت کاربر معتبر است، ولی نوع فایل و محتوای آن را بررسی نمی‌کند. برای این لایه، از توابع وردپرس مثل wp_check_filetype و wp_handle_upload استفاده کنید که همه بررسی‌های امنیتی را انجام می‌دهند.

آپلود با AJAX

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

async function uploadFile( file ) {
    const nonce = await fetch( mypluginData.ajaxUrl + '?action=myplugin_get_fresh_nonce', {
        credentials: 'same-origin',
    } ).then( r => r.json() );

    const formData = new FormData();
    formData.append( 'action', 'myplugin_upload_file' );
    formData.append( 'nonce', nonce.value );
    formData.append( 'file', file );

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

    return response.json();
}

این الگو در پروژه‌های واقعی، به‌طور محسوس نرخ خطاها را کاهش می‌دهد. نقطه ضعفش این است که یک درخواست AJAX اضافه ایجاد می‌کند.

نانس در فرم‌های عمومی سایت (تماس، کامنت)

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

فرم تماس با ما

فرم تماس با ما، یکی از فرم‌هایی است که بیشترین حمله را متحمل می‌شود، چون عملیات آن ممکن است ارسال ایمیل به مدیر سایت باشد. نانس جلوی CSRF را می‌گیرد، ولی جلوی اسپم رباتیک را نمی‌گیرد. برای این لایه، از CAPTCHA یا تله‌های پنهان استفاده کنید:

<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
    <input type="hidden" name="action" value="myplugin_contact" />
    <?php wp_nonce_field( 'myplugin_contact_form', 'myplugin_contact_nonce' ); ?>

    <input type="text" name="full_name" required />
    <input type="email" name="email" required />
    <textarea name="message" required></textarea>

    <input type="text" name="website" class="hidden-honeypot" />

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

فیلد website که با کلاس hidden-honeypot مخفی شده، یک تله است: ربات‌ها این فیلد را پر می‌کنند ولی کاربران واقعی آن را نمی‌بینند. در سمت سرور، اگر این فیلد پر بود، درخواست را رد کنید.

فرم ثبت کامنت

فرم کامنت وردپرس، نانس را به‌طور خودکار با comment_form اضافه می‌کند. اگر فرم کامنت خودتان را می‌سازید، باید نانس را با action comment_form یا action اختصاصی خودتان اضافه کنید. توجه کنید که پردازش کامنت در وردپرس از طریق wp-comments-post.php انجام می‌شود، بنابراین شما فرآیند پردازش را کنترل نمی‌کنید ولی باید مطمئن شوید نانس درست تنظیم شده است.

CAPTCHA به‌عنوان لایه مکمل

نانس و CAPTCHA دو لایه مستقل‌اند: نانس جلوی CSRF را می‌گیرد، CAPTCHA جلوی ربات‌ها را. اگر فرم عمومی سایت شما با هجوم اسپم مواجه است، نانس راه‌حل نیست و باید CAPTCHA یا Rate Limit اضافه کنید. راهنمای امنیت وردپرس برای مبتدیان سایر لایه‌های امنیتی را توضیح می‌دهد.

دام‌های رایج در پیاده‌سازی نانس فرم سفارشی

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

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

رایج‌ترین دام. نانس در فرم قرار گرفته ولی در سمت سرور بررسی نمی‌شود. این وضعیت بدترین حالت است چون توهم امنیت می‌سازد. اگر نانس در فرم دیدید ولی wp_verify_nonce یا check_admin_referer در سرور نبود، آن فرم در برابر CSRF آسیب‌پذیر است.

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

اگر action نانس را یکسان بگذارید، نانس یک فرم در فرم دیگر معتبر می‌شود. الگوی درست، استفاده از action اختصاصی برای هر فرم است.

دام سوم: عدم پاک‌سازی با wp_unslash

بدون wp_unslash، مقایسه نانس به‌طور کاذب شکست می‌خورد. این دام در بعضی افزونه‌های امنیتی که خودشان backslash را دستی حذف می‌کنند، دو بار رخ می‌دهد و رفتار غیرقابل پیش‌بینی می‌سازد.

دام چهارم: عدم بررسی وجود فیلد

اگر مستقیم به wp_verify_nonce بروید و فیلد وجود نداشته باشد، خطای PHP رخ می‌دهد. همیشه قبل از بررسی اعتبار، وجود فیلد را چک کنید.

دام پنجم: نادیده گرفتن سطح دسترسی

نانس، احراز هویت نیست. اگر فرم شما حذف داده‌ها را انجام می‌دهد، باید لایه current_user_can هم داشته باشید. نبود این لایه، یکی از حفره‌های شایع در افزونه‌های اختصاصی است.

دام ششم: استفاده از نانس در URL

نانس در URL، در لاگ‌های سرور، تاریخچه مرورگر و Referer ذخیره می‌شود. این یعنی نانس ممکن است به‌طور غیرمستقیم لو برود. همیشه نانس را از طریق فیلد POST یا هدر HTTP ارسال کنید.

دام هفتم: نادیده گرفتن Rate Limit

نانس جلوی CSRF را می‌گیرد ولی جلوی ارسال مکرر را نمی‌گیرد. اگر یک کاربر (یا ربات) بتواند ده بار در ثانیه فرم را ارسال کند، نانس هیچ کمکی نمی‌کند. برای فرم‌های حساس، Rate Limit بر اساس IP اضافه کنید.

دام هشتم: کش کردن صفحه‌های دارای نانس

اگر صفحه‌ای که فرم دارد توسط CDN یا افزونه کش، کش شود، کاربر ممکن است نانس قدیمی را دریافت کند و بررسی سمت سرور شکست بخورد. برای فرم‌های حساس، در افزونه کش استثنا کنید یا نانس را از طریق AJAX بارگذاری کنید.

دام نهم: نانس در فرم‌های API خارجی

نانس وردپرس برای احراز هویت API در محیط‌های خارج از مرورگر مناسب نیست، چون به کوکی کاربر وابسته است. برای APIهای خارجی، از Application Passwords یا OAuth استفاده کنید. پیاده‌سازی اشتباه نانس در این سناریو، هم مشکل امنیتی می‌سازد و هم تجربه کاربری را خراب می‌کند.

دام دهم: عدم ثبت هوک در زمان درست

اگر هوک پردازش فرم را در جای اشتباهی ثبت کنید، ممکن است قبل از بررسی نانس اجرا شود. الگوی امن، ثبت هوک در admin_post و admin_post_nopriv است، نه در init یا template_redirect. برای مرور سایر اصول مرتبط با اعتبارسنجی، اعتبارسنجی داده‌ها در کدنویسی وردپرس لایه مکمل را توضیح می‌دهد.

هر دام نانس، یک حفره امنیتی است که تا لحظه حمله، در سکوت باقی می‌ماند؛ عیب‌یابی فعال، تنها راه پیشگیری است.

پرسش‌های پرتکرار درباره نانس در فرم سفارشی

چرا فرم سفارشی من با اینکه نانس دارد، باز هم CSRF می‌خورد؟ رایج‌ترین دلیل این است که نانس در سمت سرور بررسی نمی‌شود. فقط قرار دادن نانس در فرم کافی نیست؛ باید در سرور با wp_verify_nonce یا check_admin_referer بررسی شود. دلیل دوم، استفاده از action عمومی است که نانس یک فرم را در فرم دیگر معتبر می‌کند.

در فرم‌های کاربران مهمان، نانس امنیت کافی می‌دهد؟ نانس کاربران مهمان، در بازه‌های نیم‌ساعته برای همه یکسان است. این یعنی اگر مهاجم نانس را در آن بازه به دست بیاورد، می‌تواند از آن برای همه کاربران مهمان استفاده کند. برای فرم‌های حساس، نانس را با CAPTCHA و Rate Limit ترکیب کنید.

در فرم چندمرحله‌ای، نانس را در هر مرحله تازه کنم یا یک نانس واحد بسازم؟ توصیه من، تولید نانس اختصاصی برای هر مرحله است. این رویکرد، هم ریسک انقضا را کاهش می‌دهد و هم امنیت را بالا می‌برد چون action نانس هر مرحله یکتاست. اگر از یک نانس برای همه مراحل استفاده کنید، انقضای نانس در مرحله سوم، باعث شکست ارسال می‌شود.

در AJAX، نانس را در فیلد POST بفرستم یا در هدر؟ برای endpointهای admin-ajax.php، فیلد POST مناسب است. برای REST API، هدر X-WP-Nonce رویکرد استاندارد است. انتخاب اشتباه، باعث خطای امنیتی می‌شود حتی اگر نانس معتبر باشد.

چرا در AJAX خطای Security check failed می‌گیرم؟ چهار دلیل رایج: اول، action نانس در دو طرف یکی نیست. دوم، نانس با wp_unslash پاک‌سازی نشده. سوم، نانس منقضی شده چون کاربر مدت زیادی در صفحه مانده. چهارم، پارامتر credentials: same-origin در fetch تنظیم نشده و کوکی ارسال نمی‌شود.

آیا نانس باید یک بار مصرف باشد؟ در وردپرس نه. نانس تا حدود ۲۴ ساعت معتبر است و در این بازه می‌تواند چندین بار استفاده شود. اگر به نانس یک‌بارمصرف نیاز دارید، باید خودتان آن را با ترکیب نانس و رکورد دیتابیس پیاده کنید.

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

آیا نانس در فرم‌های آپلود فایل هم باید بررسی شود؟ بله، همیشه. آپلود فایل، یکی از عملیات حساس است چون می‌تواند مسیر ذخیره‌سازی را تغییر دهد. علاوه بر نانس، حتماً نوع فایل و اندازه آن را با توابع وردپرس بررسی کنید.

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

آیا در فرم‌های خیلی حساس، باید لایه‌های دیگری هم اضافه کنم؟ بله. برای فرم‌های حساس مثل تغییر رمز عبور یا حذف حساب کاربری، لایه‌های توصیه‌شده عبارتند از: تأیید هویت دو مرحله‌ای، ارسال ایمیل تأیید، و ثبت لاگ. نانس، اولین لایه است نه تنها لایه.

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

چطور بفهمم فرم سفارشی من در برابر CSRF امن است؟ سه نشانه ساده: اول، اگر نانس را از فرم حذف کنید و ارسال کنید، باید خطای امنیتی بگیرید. دوم، اگر action نانس را در سمت کلاینت تغییر دهید، بررسی سمت سرور باید شکست بخورد. سوم، در لاگ‌ها باید رکوردهای failed security check برای درخواست‌های مشکوک ثبت شود.

آیا استفاده از نانس در فرم سفارشی روی سرعت سایت اثر دارد؟ نانس، تولید یک هش MD5 است که زمان اجرای آن ناچیز است. اثر آن روی سرعت سایت، در حد میلی‌ثانیه است. مهم‌تر از زمان تولید نانس، زمان بررسی آن است که اگر با منطق ساده انجام شود، همچنان ناچیز می‌ماند.

امنیت فرم سفارشی، یک زنجیره است نه یک قطعه

در پایان این مسیر، حقیقتی که در پروژه‌های واقعی بارها تأیید کرده‌ام: امنیت فرم سفارشی یک قطعه نیست، یک زنجیره است. نانس، یکی از مهم‌ترین حلقه‌های این زنجیره است ولی جایگزین حلقه‌های دیگر نمی‌شود. اعتبارسنجی ورودی، پاک‌سازی خروجی، بررسی سطح دسترسی، Rate Limit و لاگ‌گیری، همه بخشی از این زنجیره‌اند. اگر یکی از این حلقه‌ها نباشد، حمله‌گر از همان حلقه ضعیف، نفوذ می‌کند.

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

اگر در ابتدای مسیر یادگیری هستید، سه تمرین را پیشنهاد می‌کنم. اول، یک فرم سفارشی ساده در افزونه خودتان بسازید و نانس را در آن به‌طور کامل پیاده کنید — از تولید تا بررسی. دوم، یک بار عمداً نانس را از فرم حذف کنید و ببینید در سمت سرور چه اتفاقی می‌افتد. سوم، یک بار action نانس را در فرم تغییر دهید و ببینید بررسی سمت سرور چطور آن را تشخیص می‌دهد. این سه تمرین، در چند ساعت، درک عمیقی از نانس در فرم‌های سفارشی به شما می‌دهد که هیچ مقاله‌ای جایگزینش نمی‌شود.

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