Nonce در وردپرس چیست و چرا امنیت فرمها بدون آن یک توهم است؟
راهنمای Nonce؛ بررسی ساخت، بررسی و عمر. برای فرم و AJAX کاربرد دارد. اشتباه رایج، نبود nonce، نبود بررسی و نبود تست است. تسلط بر آن برای امنیت ضروری است.
Nonce یا Number used ONCE در وردپرس یک توکن امنیتی یکبارمصرف است که برای محافظت از فرمها، درخواستهای AJAX و عملیات مدیریتی در برابر حمله CSRF (Cross-Site Request Forgery) طراحی شده است. برخلاف نامش که به «یکبار» اشاره دارد، Nonce وردپرس در بازهای محدود — بهطور پیشفرض ۱۲ یا ۲۴ ساعت — معتبر میماند و سپس منقضی میشود. امنیت فرمهای وردپرس بدون Nonce یک توهم است؛ چون مرورگر کاربر بهطور خودکار کوکی نشست را در همه درخواستها ارسال میکند و یک صفحه مخرب میتواند بهجای کاربر، عملیات حساس را اجرا کند. این متن مسیر عملی پیادهسازی Nonce را از مفاهیم پایه تا الگوهای پیشرفته در AJAX، REST API و فرمهای سفارشی، با تمرکز بر دامهای امنیتی و کد قابل اجرا بررسی میکند.
بارها دیدهام توسعهدهندهای که یک فرم سفارشی پیچیده ساخته، همه لایههای اعتبارسنجی را رعایت کرده، اما یک خط Nonce را فراموش کرده است. نتیجه، فرمی بود که در ظاهر امن به نظر میرسید اما هر صفحه مخربی میتوانست با یک درخواست پنهان، دادههای کاربر را تغییر دهد. تجربه نشان داده که Nonce، همان لایهای است که کمترین توجه به آن میشود و بیشترین آسیب از نبودش میآید.
Nonce در وردپرس دقیقاً چیست؟
Nonce در وردپرس، مخفف عبارت Number used ONCE است و به یک توکن امنیتی یکبارمصرف گفته میشود که در قالب یک رشته هششده تولید و در فرم یا درخواست AJAX جاسازی میشود. این توکن، به هویت کاربر جاری، یک رشته Action مشخص و یک پنجره زمانی محدود گره خورده است. وقتی درخواست به سرور میرسد، وردپرس همان محاسبه را تکرار میکند و اگر توکن ارسالی با مقدار محاسبهشده مطابقت داشته باشد، درخواست معتبر تلقی میشود.
مفهوم Nonce در ادبیات امنیتی ریشهای طولانی دارد و در پروتکلهای رمزنگاری مختلف برای جلوگیری از حملات Replay و جعل درخواست استفاده میشود. توضیحات پایهای این مفهوم در دانشنامه آزاد ویکیپدیا با عنوان Cryptographic nonce مستندسازی شده است. اما Nonce وردپرس تفاوت مهمی با Nonceهای رمزنگاری کلاسیک دارد: در رمزنگاری، Nonce واقعاً یکبارمصرف است و پس از استفاده باطل میشود؛ در وردپرس، Nonce برای یک بازه زمانی مشخص معتبر میماند و میتواند چندین بار در همان بازه استفاده شود.
این تفاوت طراحی، یک تصمیم آگاهانه از تیم هسته وردپرس است. دلیل آن، ماهیت بدون حالت (Stateless) وردپرس و نبود یک مخزن سرور برای توکنهای مصرفشده است. اگر Nonce پس از هر استفاده باطل میشد، وردپرس برای هر کاربر و هر Action باید یک رکورد جداگانه نگه میداشت که بار دیتابیس را بهطور چشمگیری افزایش میداد. بهجای آن، وردپرس یک راهحل میانراه انتخاب کرده که توازن بین امنیت و کارایی را حفظ میکند.
نکته حیاتی در درک Nonce این است که هدف آن، اثبات احراز هویت کاربر نیست. Nonce جایگزین بررسی current_user_can نمیشود و نباید بهعنوان مکانیزم مجوزدهی تلقی شود. هدف دقیق Nonce، اثبات این است که درخواست از یک صفحه معتبر سایت آمده و نه از یک صفحه مخرب خارجی. این تمایز، نقطه شروع درست برای پیادهسازی امنیتی محسوب میشود. برای درک دقیقتر تفاوت احراز هویت و مجوزدهی، مطلب تفاوت احراز هویت و مجوزدهی چیست و چرا مرز این دو در معماری سیستمها گم میشود؟ پیشنیاز ضروری این بحث است.
Nonce اثبات نمیکند که کاربر شما هستید؛ اثبات میکند که درخواست از یک صفحه معتبر سایت شما آمده است.
چرا فرمهای وردپرس بدون Nonce آسیبپذیرند؟
برای درک اهمیت Nonce، باید ابتدا مکانیزم حمله CSRF را بشناسید. در این حمله، مهاجم کاربر را به یک صفحه مخرب هدایت میکند که در آن یک درخواست پنهان به سایت شما ارسال میشود. مرورگر کاربر بهطور خودکار کوکی نشست را همراه درخواست ارسال میکند و سایت شما نمیتواند تشخیص دهد که این درخواست از خود کاربر آمده یا از یک صفحه خارجی.
سناریوی عملی یک حمله CSRF
فرض کنید یک کاربر وارد پنل مدیریت وردپرس خود شده است. در همان زمان، یک صفحه HTML مخرب را در یک تب دیگر باز میکند. این صفحه شامل یک فرم پنهان به آدرس زیر است: /wp-admin/user-new.php با پارامترهایی که یک کاربر ادمین جدید میسازد. وقتی صفحه مخرب بارگذاری میشود، فرم بهطور خودکار ارسال میشود. مرورگر کوکی نشست کاربر را همراه درخواست میفرستد. اگر وردپرس Nonce را بررسی نکند، کاربر جدید با نقش ادمین ساخته میشود و مهاجم از این طریق به پنل دسترسی پیدا میکند.
این حمله بدون Nonce تقریباً همیشه موفق است. اما وقتی Nonce در فرم وجود دارد، درخواست ارسالی از صفحه مخرب هیچ Nonce معتبری ندارد و سرور آن را رد میکند. نتیجه، یک خطای «پیوند منقضی شده» است و کاربر میتواند با اطمینان از ادامه کار مطلع شود.
چرا SameSite کافی نیست
مرورگرهای مدرن از ویژگی SameSite در کوکیها پشتیبانی میکنند که بهطور پیشفرض در برخی مرورگرها روی Lax تنظیم شده است. این ویژگی، بخش بزرگی از حملات CSRF را دفع میکند چون کوکی در درخواستهای Cross-Site ارسال نمیشود. اما SameSite تنها یک لایه دفاعی است و در سه سناریو ناقص میماند.
سناریو اول، مرورگرهای قدیمی که از SameSite پشتیبانی نمیکنند. سناریو دوم، درخواستهایی که از همان دامنه اما از طریق نقاط پایانی باز (Open Redirect) اجرا میشوند. سناریو سوم، درخواستهای Cross-Site که توسط کاربر خود آغاز میشوند — مثل کلیک روی یک لینک از یک سایت دیگر — که در مرورگرهای جدید با SameSite=Lax کوکی ارسال میشود. به همین دلیل، استانداردهای امنیتی مدرن توصیه میکنند که Nonce بهعنوان لایه دوم دفاعی همیشه فعال باشد.
تفاوت CSRF با XSS
CSRF با XSS (Cross-Site Scripting) تفاوت بنیادین دارد. در XSS، مهاجم کد JavaScript را در صفحه سایت شما اجرا میکند و از همان دامنه، درخواستهای مخرب ارسال میکند. در این شرایط، Nonce نیز در دسترس مهاجم قرار دارد چون در همان صفحه تولید شده است. یعنی Nonce در برابر XSS محافظت نمیکند و اگر سایت شما در برابر XSS آسیبپذیر باشد، Nonce نیز بیاثر میشود. برای درک عمیقتر این تفاوت، مطلب چرا جلوگیری از XSS در برنامههای وب اینقدر حیاتی است؟ راهنمای کاملی ارائه میدهد.
| ویژگی | CSRF | XSS |
|---|---|---|
| هدف | اجرای عملیات بهجای کاربر | اجرای کد در مرورگر کاربر |
| نیاز به تعامل کاربر | بله (کلیک یا بارگذاری صفحه) | بله (بارگذاری صفحه آلوده) |
| دفاع اصلی | Nonce، SameSite | پاکسازی خروجی، CSP |
| آیا Nonce مؤثر است؟ | بله | خیر |
مکانیزم کار Nonce در هسته وردپرس
Nonce وردپرس از ترکیب چند ورودی محاسبه میشود که بهطور مشترک یک هش تولید میکنند. این ورودیها عبارتاند از: یک رشته Action مشخص، شناسه کاربر جاری، شناسه نشست کاربر و یک بازه زمانی. هر یک از این ورودیها نقش مشخصی در امنیت Nonce دارد.
رشته Action
رشته Action یک شناسه دلخواه است که توسط توسعهدهنده تعیین میشود و به Nonce گره میخورد. اگر یک Nonce برای Action مربوط به حذف نوشته تولید شود، همان Nonce برای Actionهای دیگر کار نمیکند. این تفکیک، جلوگیری میکند که یک مهاجم با سرقت یک Nonce، از آن در Actionهای دیگر استفاده کند.
شناسه کاربر
Nonce به شناسه کاربر جاری گره خورده است. اگر مهاجم یک Nonce را از مرورگر خودش ذخیره کند و آن را در مرورگر کاربر دیگری بهکار گیرد، اعتبارسنجی شکست میخورد چون شناسه کاربر متفاوت است. این ویژگی، Nonce وردپرس را از Nonceهای عمومی جدا میکند.
شناسه نشست
Nonce به توکن نشست کاربر گره خورده است. وقتی کاربر وارد میشود، وردپرس یک توکن نشست تولید میکند که در کوکی ذخیره میشود. Nonce بر پایه این توکن محاسبه میشود. نتیجه، باطل شدن خودکار Nonceها هنگام خروج کاربر یا انقضای نشست است.
بازه زمانی
Nonce به یک بازه زمانی گره خورده است که بهطور پیشفرض با ثابت NONCE_LIFE تعیین میشود و مقدار آن ۲۴ ساعت است. در واقع، وردپرس دو بازه را همزمان بررسی میکند: بازه جاری و بازه قبلی. این یعنی Nonce در برخی شرایط میتواند تا ۴۸ ساعت معتبر بماند؛ چون پس از پایان بازه اصلی، هنوز در بازه قبلی قابل تأیید است.
// محاسبه Nonce در هسته وردپرس (سادهسازی شده)
function wp_create_nonce( $action = -1 ) {
$user = wp_get_current_user();
$uid = (int) $user->ID;
$token = wp_get_session_token();
$tick = wp_nonce_tick();
return substr(
wp_hash( $tick . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ),
-12,
10
);
}
function wp_nonce_tick() {
$nonce_life = apply_filters( 'nonce_life', DAY_IN_SECONDS );
return ceil( time() / ( $nonce_life / 2 ) );
}
نکته مهم در این کد، مقدار $nonce_life / 2 است. وردپرس بازه زمانی را نصف میکند تا بتواند دو بازه مجاور را همزمان بررسی کند. این طراحی، امکان استفاده از Nonce را در شرایطی که صفحه کاربر مدت طولانی باز مانده باشد، فراهم میآورد.
ساخت Nonce با wp_create_nonce و wp_nonce_field
وردپرس دو تابع اصلی برای ساخت Nonce ارائه میدهد که هر یک برای سناریو متفاوتی مناسب است.
wp_create_nonce برای درخواستهای برنامهنویسی
تابع wp_create_nonce یک رشته Nonce تولید میکند که میتوان آن را در URL، هدر یا هر مکان دیگری جاسازی کرد. این تابع برای درخواستهای AJAX و REST API مناسب است.
$nonce = wp_create_nonce( 'wk_delete_post_' . $post_id );
$url = add_query_arg( '_wpnonce', $nonce, admin_url( 'post.php?action=delete&post=' . $post_id ) );
نکته کلیدی در استفاده از wp_create_nonce، افزودن شناسه منبع به Action است. اگر Action فقط delete_post باشد، یک Nonce میتواند برای حذف همه نوشتهها استفاده شود. اما اگر Action شامل شناسه نوشته باشد، Nonce فقط برای همان نوشته معتبر است.
wp_nonce_field برای فرمهای HTML
تابع wp_nonce_field یک فیلد پنهان HTML تولید میکند که بهطور خودکار در فرم قرار میگیرد. این تابع برای فرمهای استاندارد HTML مناسب است.
<form method="post" action="">
<?php wp_nonce_field( 'wk_save_settings', 'wk_nonce' ); ?>
<label>عنوان سایت</label>
<input type="text" name="site_title" value="">
<button type="submit">ذخیره</button>
</form>
پارامتر اول، رشته Action است و پارامتر دوم، نام فیلد HTML. اگر پارامتر دوم نادیده گرفته شود، بهطور پیشفرض _wpnonce استفاده میشود. برای جلوگیری از تضاد در فرمهایی که چند Nonce دارند، توصیه میشود نام فیلد بهصورت صریح تعیین شود.
wp_nonce_url برای لینکهای حساس
برای عملیاتهایی که از طریق لینک (GET) انجام میشوند، مانند حذف یا تأیید، وردپرس تابع wp_nonce_url را ارائه میدهد.
$delete_url = wp_nonce_url(
admin_url( 'admin.php?page=wk-items&action=delete&id=' . $item_id ),
'wk_delete_item_' . $item_id
);
echo '<a href="' . esc_url( $delete_url ) . '">حذف</a>';
استفاده از Nonce در لینکهای GET یک نکته ظریف دارد: Nonce در URL قرار میگیرد و میتواند در لاگ سرور، تاریخچه مرورگر یا Referer Header ذخیره شود. به همین دلیل، توصیه میشود عملیات حساس ترجیحاً از طریق POST انجام شوند.
اعتبارسنجی Nonce با wp_verify_nonce
پس از دریافت درخواست، باید Nonce ارسالی را با تابع wp_verify_nonce اعتبارسنجی کرد. این تابع سه مقدار ممکن را برمیگرداند: 1 اگر Nonce در بازه جاری معتبر باشد، 2 اگر در بازه قبلی معتبر باشد، و false اگر نامعتبر باشد.
$nonce = sanitize_text_field( $_POST['wk_nonce'] ?? '' );
if ( ! wp_verify_nonce( $nonce, 'wk_save_settings' ) ) {
wp_die(
'پیوند منقضی شده است. لطفاً به صفحه قبل بازگردید و دوباره تلاش کنید.',
'خطای امنیتی',
[ 'response' => 403 ]
);
}
// ادامه پردازش فرم
$site_title = sanitize_text_field( $_POST['site_title'] ?? '' );
update_option( 'blogname', $site_title );
سه دام رایج در اعتبارسنجی Nonce وجود دارد. دام اول، مقایسه مستقیم Nonce با === بهجای استفاده از wp_verify_nonce است. تابع هسته وردپرس از hash_equals بهصورت داخلی استفاده میکند که در برابر حملات Timing Attack مقاوم است؛ مقایسه دستی این محافظت را ندارد. دام دوم، نادیده گرفتن مقدار بازگشتی ۲ است. برخی توسعهدهندگان فقط 1 را معتبر میدانند و درخواستهایی که Nonce آنها در بازه قبلی معتبر است را رد میکنند که منجر به خطاهای بیدلیل در فرمهای طولانی میشود. دام سوم، اعتبارسنجی Nonce بعد از اجرای عملیات است که بهطور کامل بیاثر میشود.
اگر میخواهید با اصول نوشتن کد امن PHP در وردپرس آشنا شوید، مطلب نوشتن کد PHP امن برای وردپرس پیشنیاز ضروری این بحث است.
check_admin_referer و check_ajax_referer
وردپرس دو تابع سطحبالا برای اعتبارسنجی Nonce ارائه میدهد که علاوه بر بررسی Nonce، بررسیهای اضافی دیگری نیز انجام میدهند.
check_admin_referer برای فرمهای مدیریتی
تابع check_admin_referer یک Nonce را اعتبارسنجی میکند و اگر نامعتبر باشد، اجرای اسکریپت را با یک پیام خطا متوقف میکند. این تابع بهطور خودکار Nonce را از $_REQUEST میخواند و در صورتی که فیلد موجود نباشد، خطا میدهد.
add_action( 'admin_post_wk_save_settings', function() {
// اعتبارسنجی Nonce با تابع سطحبالا
check_admin_referer( 'wk_save_settings' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( 'دسترسی غیرمجاز', 403 );
}
$data = [
'api_key' => sanitize_text_field( $_POST['api_key'] ?? '' ),
'endpoint' => esc_url_raw( $_POST['endpoint'] ?? '' ),
];
update_option( 'wk_settings', $data );
wp_safe_redirect( add_query_arg( 'updated', '1', wp_get_referer() ) );
exit;
} );
مزیت check_admin_referer نسبت به wp_verify_nonce این است که فرآیند اعتبارسنجی و مدیریت خطا را یکپارچه میکند و امکان فراموش کردن بررسی را کاهش میدهد. اما یک محدودیت دارد: این تابع همیشه با پیام خطای پیشفرض اجرای اسکریپت را متوقف میکند، که در برخی پروژهها نیاز به سفارشیسازی دارد.
check_ajax_referer برای درخواستهای AJAX
تابع check_ajax_referer برای درخواستهای AJAX طراحی شده است و بهطور پیشفرض Nonce را از پارامتر _ajax_nonce یا _wpnonce میخواند. اگر Nonce نامعتبر باشد، بهطور خودکار یک پاسخ JSON با کد وضعیت ۴۰۳ برمیگرداند و اجرای اسکریپت را متوقف میکند.
add_action( 'wp_ajax_wk_load_more', function() {
check_ajax_referer( 'wk_load_more', 'nonce' );
$page = absint( $_POST['page'] ?? 1 );
$query = new WP_Query( [
'post_type' => 'post',
'posts_per_page' => 10,
'paged' => $page,
] );
$items = [];
while ( $query->have_posts() ) {
$query->the_post();
$items[] = [
'id' => get_the_ID(),
'title' => get_the_title(),
'url' => get_permalink(),
];
}
wp_reset_postdata();
wp_send_json_success( [ 'items' => $items ] );
} );
نکته مهم در استفاده از check_ajax_referer این است که پارامتر دوم، نام فیلد Nonce در درخواست را مشخص میکند. اگر این پارامتر نادیده گرفته شود، تابع بهطور پیشفرض _ajax_nonce و _wpnonce را بررسی میکند. برای وضوح و جلوگیری از تضاد، توصیه میشود نام فیلد بهصورت صریح تعیین شود.
Nonce در درخواستهای AJAX
پیادهسازی Nonce در AJAX نیازمند هماهنگی دقیق بین سمت کلاینت و سمت سرور است. در سمت کلاینت، Nonce باید از یک متغیر JavaScript که توسط wp_localize_script تزریق شده، خوانده شود. در سمت سرور، تابع check_ajax_referer آن را اعتبارسنجی میکند.
تزریق Nonce به JavaScript
add_action( 'wp_enqueue_scripts', function() {
wp_enqueue_script(
'wk-ajax',
get_template_directory_uri() . '/js/ajax.js',
[ 'jquery' ],
'1.0.0',
true
);
wp_localize_script( 'wk-ajax', 'wkAjax', [
'url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'wk_load_more' ),
] );
} );
ارسال Nonce از سمت کلاینت
jQuery( function( $ ) {
$( '#load-more' ).on( 'click', function( e ) {
e.preventDefault();
const page = $( this ).data( 'page' ) || 1;
$.ajax( {
url: wkAjax.url,
type: 'POST',
data: {
action: 'wk_load_more',
nonce: wkAjax.nonce,
page: page,
},
success: function( response ) {
if ( response.success ) {
$( '#items' ).append( response.data.items );
}
},
} );
} );
} );
دامهای امنیتی در AJAX
سه دام اصلی در پیادهسازی Nonce در AJAX وجود دارد. دام اول، ذخیره Nonce در یک متغیر JavaScript سراسری و استفاده از آن در همه درخواستها. این کار درست است، اما اگر Nonce منقضی شود، همه درخواستها شکست میخورند. راهحل، تازهسازی Nonce در صورت دریافت خطای ۴۰۳ است. دام دوم، فراموش کردن افزودن پارامتر Action به درخواست است که منجر به فراخوانی هوک نادرست میشود. دام سوم، اعتبارسنجی Nonce بعد از پردازش داده است که بهطور کامل آن را بیاثر میکند.
jQuery( function( $ ) {
const refreshNonce = function() {
return $.post( wkAjax.url, { action: 'wk_refresh_nonce' } )
.then( function( response ) {
if ( response.success ) {
wkAjax.nonce = response.data.nonce;
}
} );
};
$( document ).ajaxError( function( event, jqxhr ) {
if ( 403 === jqxhr.status ) {
refreshNonce().then( function() {
// تکرار درخواست اصلی
} );
}
} );
} );
Nonce در REST API وردپرس
REST API وردپرس از Nonce بهعنوان یکی از مکانیزمهای احراز هویت پشتیبانی میکند. وقتی درخواست از یک صفحه معتبر سایت ارسال شود، وردپرس بهطور خودکار Nonce را از هدر X-WP-Nonce میخواند و در صورت معتبر بودن، کاربر جاری را در بافت درخواست تنظیم میکند.
تزریق Nonce به REST API
add_action( 'wp_enqueue_scripts', function() {
wp_enqueue_script(
'wk-rest',
get_template_directory_uri() . '/js/rest.js',
[ 'wp-api-fetch' ],
'1.0.0',
true
);
wp_localize_script( 'wk-rest', 'wkRest', [
'root' => esc_url_raw( rest_url() ),
'nonce' => wp_create_nonce( 'wp_rest' ),
] );
} );
ارسال درخواست با Nonce
fetch( wkRest.root + 'wp/v2/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': wkRest.nonce,
},
body: JSON.stringify( {
title: 'نوشته جدید',
content: 'محتوای نوشته',
status: 'draft',
} ),
} )
.then( response => response.json() )
.then( data => console.log( 'Created:', data.id ) )
.catch( error => console.error( 'Error:', error ) );
نکات خاص REST API
در REST API، Nonce از طریق هدر X-WP-Nonce ارسال میشود و نه در بدنه درخواست. این طراحی، امکان حفظ سازگاری با استاندارد RESTful را فراهم میکند. همچنین، Nonce در REST API بهطور خودکار با Action ویژه wp_rest تولید میشود، نه Actionهای سفارشی.
نکته ظریف اینجاست که Nonce در REST API تنها یک مکانیزم احراز هویت است و نه مکانیزم مجوزدهی. بعد از اعتبارسنجی Nonce، همچنان باید با current_user_can بررسی شود که کاربر جاری مجاز به انجام عملیات است. برای درک عمیقتر معماری REST API در وردپرس، مطلب REST API در وردپرس راهنمای کامل راهنمای جامعی ارائه میدهد.
عمر Nonce و مدیریت انقضا
Nonce وردپرس عمر محدودی دارد که بهطور پیشفرض ۲۴ ساعت است. اما این عمر در عمل میتواند تا ۴۸ ساعت افزایش یابد، چون وردپرس دو بازه زمانی را همزمان بررسی میکند. این طراحی، امکان استفاده از Nonce را در شرایطی که صفحه کاربر برای مدت طولانی باز مانده، فراهم میآورد.
تغییر عمر Nonce با فیلتر nonce_life
در برخی پروژههای حساس، ممکن است نیاز به کاهش عمر Nonce باشد. فیلتر nonce_life این امکان را فراهم میکند.
add_filter( 'nonce_life', function( $lifetime ) {
// کاهش عمر Nonce به ۲ ساعت برای عملیات حساس
return 2 * HOUR_IN_SECONDS;
} );
کاهش عمر Nonce، امنیت را افزایش میدهد اما تجربه کاربری را تحت تأثیر قرار میدهد. اگر کاربر صفحهای را بیش از عمر Nonce باز نگه دارد، درخواست بعدی او با خطای «پیوند منقضی شده» مواجه میشود. در پروژههای واقعی، مقدار مناسب باید بر پایه ماهیت عملیات تعیین شود: عملیات حساس مانند تغییر رمز عبور، عمر کوتاهتر؛ عملیات عمومی مانند بارگذاری محتوای بیشتر، عمر بلندتر.
مدیریت Nonce منقضی در تجربه کاربری
وقتی Nonce منقضی میشود، پیام خطای پیشفرض وردپرس «The link you followed has expired» است که برای کاربران غیرفنی گیجکننده است. توصیه میشود این پیام با یک پیام واضحتر و پیشنهاد عملی جایگزین شود.
add_filter( 'wp_die_handler', function( $handler ) {
return function( $message, $title = '', $args = [] ) {
if ( is_string( $message )
&& false !== strpos( $message, 'expired' ) ) {
$message = sprintf(
'نشست شما منقضی شده است. لطفاً صفحه را دوباره بارگذاری کنید و فرم را از نو ارسال نمایید.
' .
'',
esc_url( home_url() )
);
}
_default_wp_die_handler( $message, $title, $args );
};
} );
اتصال Nonce به کاربر و Action
یکی از رایجترین اشتباهات در پیادهسازی Nonce، استفاده از یک Action سراسری برای همه عملیات است. این الگو، Nonce را از یک مکانیزم دقیق به یک توکن عمومی تبدیل میکند که در صورت سرقت، برای همه عملیات قابل استفاده است.
الگوی نادرست Action عمومی
// نادرست: یک Action برای همه عملیات
$nonce = wp_create_nonce( 'my_plugin_action' );
// اگر مهاجم این Nonce را بدزدد، میتواند
// برای حذف نوشته، حذف کاربر، تغییر تنظیمات استفاده کند
الگوی صحیح Action اختصاصی
// صحیح: Action اختصاصی برای هر عملیات و هر منبع
$nonce_delete = wp_create_nonce( 'wk_delete_post_' . $post_id );
$nonce_update = wp_create_nonce( 'wk_update_post_' . $post_id );
$nonce_user = wp_create_nonce( 'wk_delete_user_' . $user_id );
این الگو، دامنه اعتبار هر Nonce را به یک عملیات مشخص روی یک منبع مشخص محدود میکند. اگر مهاجم یک Nonce را سرقت کند، فقط میتواند همان عملیات را روی همان منبع اجرا کند که بهدلیل محدودیتهای دیگر — مانند نیاز به مجوز کاربر — معمولاً بیاثر است.
الگوی Action مبتنی بر کاربر
در برخی سناریوها، نیاز است که Nonce به کاربر مشخصی گره بخورد. مثلاً در سیستمهای چندکاربره که هر کاربر فقط به دادههای خودش دسترسی دارد.
$user_id = get_current_user_id();
$nonce = wp_create_nonce( 'wk_edit_profile_' . $user_id );
// اعتبارسنجی
$submitted = sanitize_text_field( $_POST['nonce'] ?? '' );
if ( ! wp_verify_nonce( $submitted, 'wk_edit_profile_' . $user_id ) ) {
wp_die( 'درخواست نامعتبر', 403 );
}
ترکیب Action با شناسه کاربر، سطح امنیت را چند برابر میکند. اما توجه داشته باشید که Nonce وردپرس بهطور داخلی به کاربر جاری گره خورده است، بنابراین این افزودن صریح، یک لایه تأیید مضاعف محسوب میشود.
پیادهسازی Nonce در فرمهای سفارشی
فرمهای سفارشی، یکی از پرتکرارترین محلهای نبود Nonce در پروژههای وردپرسی هستند. سه الگوی رایج برای پیادهسازی Nonce در این فرمها وجود دارد.
الگوی اول: فرمهای ارسال به admin-post.php
الگوی توصیهشده برای فرمهای مدیریتی، ارسال به admin-post.php است که امنیت و ساختار را یکپارچه میکند.
add_action( 'admin_post_wk_save_form', function() {
check_admin_referer( 'wk_save_form' );
if ( ! current_user_can( 'edit_posts' ) ) {
wp_die( 'دسترسی غیرمجاز', 403 );
}
// پردازش دادهها
$title = sanitize_text_field( $_POST['title'] ?? '' );
$content = wp_kses_post( $_POST['content'] ?? '' );
wp_insert_post( [
'post_title' => $title,
'post_content' => $content,
'post_status' => 'draft',
'post_type' => 'post',
] );
wp_safe_redirect( admin_url( 'admin.php?page=wk-form&saved=1' ) );
exit;
} );
// نمایش فرم
function wk_render_form() {
?>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
<input type="hidden" name="action" value="wk_save_form">
<?php wp_nonce_field( 'wk_save_form', 'wk_nonce' ); ?>
<label>عنوان</label>
<input type="text" name="title">
<label>محتوا</label>
<textarea name="content"></textarea>
<button type="submit">ذخیره</button>
</form>
<?php
}
این الگو، هم Nonce را بهطور استاندارد اعمال میکند و هم از یک نقطه پایانی امن در هسته وردپرس استفاده میکند. برای بررسی عمیقتر نحوه استفاده از این الگو، مطلب چگونه نانس را در فرمهای سفارشی وردپرس درست پیادهسازی کنیم؟ راهنمای گامبهگام ارائه میدهد.
الگوی دوم: فرمهای عمومی در front-end
برای فرمهای عمومی مانند فرمهای تماس، پیادهسازی Nonce نیازمند دقت بیشتر است چون کاربر ممکن است ساعتها در صفحه بماند. راهحل توصیهشده، استفاده از یک Nonce با عمر کوتاهتر و مدیریت انقضا در سمت کلاینت است.
add_shortcode( 'wk_contact_form', function() {
ob_start();
?>
<form id="wk-contact-form" method="post">
<?php wp_nonce_field( 'wk_contact_submit', 'wk_contact_nonce' ); ?>
<input type="email" name="email" required>
<textarea name="message" required></textarea>
<button type="submit">ارسال</button>
</form>
<?php
return ob_get_clean();
} );
add_action( 'wp', function() {
if ( empty( $_POST['wk_contact_nonce'] ) ) {
return;
}
$nonce = sanitize_text_field( $_POST['wk_contact_nonce'] );
if ( ! wp_verify_nonce( $nonce, 'wk_contact_submit' ) ) {
wp_die( 'درخواست نامعتبر است', 403 );
}
// پردازش فرم
$email = sanitize_email( $_POST['email'] ?? '' );
$message = sanitize_textarea_field( $_POST['message'] ?? '' );
wp_mail( get_option( 'admin_email' ), 'پیام جدید', $message );
wp_safe_redirect( add_query_arg( 'sent', '1', wp_get_referer() ) );
exit;
} );
الگوی سوم: فرمهای چندمرحلهای
در فرمهای چندمرحلهای، Nonce باید در هر مرحله تولید و اعتبارسنجی شود. اگر یک Nonce واحد برای همه مراحل استفاده شود، در صورتی که کاربر مرحله اول را در بازهای طولانی تکمیل کند، ممکن است Nonce در مراحل بعدی منقضی شود.
// مرحله اول
$step_nonce_1 = wp_create_nonce( 'wk_step_1' );
update_user_meta( get_current_user_id(), 'wk_step_nonce', $step_nonce_1 );
// مرحله دوم
$step_nonce_2 = wp_create_nonce( 'wk_step_2' );
update_user_meta( get_current_user_id(), 'wk_step_nonce', $step_nonce_2 );
Nonce در متاباکس و برگه تنظیمات
در متاباکسها و برگههای تنظیمات، وردپرس مکانیزمهای استانداردی برای Nonce ارائه میدهد که استفاده از آنها، هم امنیت را تضمین میکند و هم از بازنویسی کد جلوگیری مینماید.
Nonce در متاباکس
add_action( 'add_meta_boxes', function() {
add_meta_box(
'wk_product_extra',
'اطلاعات تکمیلی',
'wk_render_product_meta',
'product',
'normal',
'default'
);
} );
function wk_render_product_meta( $post ) {
wp_nonce_field( 'wk_product_meta', 'wk_product_meta_nonce' );
$sku = get_post_meta( $post->ID, '_wk_sku', true );
?>
<label>کد محصول</label>
<input type="text" name="wk_sku" value="<?php echo esc_attr( $sku ); ?>">
<?php
}
add_action( 'save_post_product', function( $post_id, $post, $update ) {
if ( ! isset( $_POST['wk_product_meta_nonce'] ) ) {
return;
}
$nonce = sanitize_text_field( $_POST['wk_product_meta_nonce'] );
if ( ! wp_verify_nonce( $nonce, 'wk_product_meta' ) ) {
return;
}
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
$sku = sanitize_text_field( $_POST['wk_sku'] ?? '' );
update_post_meta( $post_id, '_wk_sku', $sku );
}, 10, 3 );
نکته مهم در متاباکس، بررسی سه شرط قبل از پردازش است: وجود Nonce، معتبر بودن Nonce و وجود مجوز کاربر. اگر یکی از این سه شرط نادیده گرفته شود، خطر امنیتی ایجاد میشود. اگر با مفهوم متاباکس و هوک save_post آشنا نیستید، مطلب کار با متاباکسها در کدنویسی وردپرس راهنمای مفیدی است.
Nonce در برگه تنظیمات
در برگههای تنظیمات، وردپرس تابع settings_fields را ارائه میدهد که بهطور خودکار Nonce تولید و تزریق میکند.
add_action( 'admin_init', function() {
register_setting( 'wk_settings_group', 'wk_options', [
'type' => 'array',
'sanitize_callback' => 'wk_sanitize_options',
] );
add_settings_section(
'wk_main_section',
'تنظیمات اصلی',
'__return_null',
'wk_settings'
);
add_settings_field(
'wk_api_key',
'کلید API',
'wk_render_api_key_field',
'wk_settings',
'wk_main_section'
);
} );
function wk_render_settings_page() {
?>
<div class="wrap">
<h1>تنظیمات افزونه</h1>
<form method="post" action="options.php">
<?php
settings_fields( 'wk_settings_group' );
do_settings_sections( 'wk_settings' );
submit_button();
?>
</form>
</div>
<?php
}
تابع settings_fields بهطور خودکار Nonce را با Action wk_settings_group تولید و بهصورت یک فیلد پنهان در فرم تزریق میکند. همچنین، هنگام ذخیره توسط options.php، وردپرس بهطور خودکار Nonce را اعتبارسنجی میکند.
Nonce در وردپرس چندسایته
در نصبهای Multisite، Nonce رفتار خاصی دارد که باید از آن آگاه باشید. Nonce در Multisite به سایت جاری گره خورده است، نه به شبکه. یعنی اگر کاربری در سایت A وارد شود، Nonce تولیدشده در سایت A در سایت B معتبر نیست.
سناریوی چالشبرانگیز
فرض کنید یک افزونه شبکهای دارید که در همه سایتهای شبکه اجرا میشود. اگر از یک Nonce سراسری استفاده کنید، درخواستهای ارسالی از سایت A به سایت B با شکست اعتبارسنجی مواجه میشوند. راهحل، تولید Nonce در بافت همان سایتی است که درخواست از آن ارسال میشود.
// سناریوی نادرست در Multisite
$nonce = wp_create_nonce( 'wk_network_action' );
// این Nonce در همه سایتهای شبکه یکسان نیست
// راهحل درست: تولید Nonce در بافت سایت جاری
$current_blog_id = get_current_blog_id();
$nonce = wp_create_nonce( 'wk_action_blog_' . $current_blog_id );
مدیریت Nonce در Super Admin
Super Admin در Multisite به همه سایتهای شبکه دسترسی دارد. وقتی Super Admin وارد یک سایت میشود، Nonce در بافت همان سایت تولید میشود. اگر Super Admin به یک سایت دیگر منتقل شود، Nonce متفاوتی تولید میشود. این رفتار، امنیت را افزایش میدهد اما در طراحی افزونههای شبکهای باید در نظر گرفته شود.
اشتباهات رایج در استفاده از Nonce
در بررسی پروژههای متعدد، الگوهای زیر پرتکرارترین خطاها در استفاده از Nonce بودهاند. هر یک از این خطاها، امنیت سایت را بهطور جدی تضعیف میکند.
- استفاده از یک Nonce عمومی برای همه عملیات، بهجای Nonce اختصاصی برای هر Action و منبع.
- اعتبارسنجی Nonce بعد از اجرای عملیات حساس، که آن را کاملاً بیاثر میکند.
- مقایسه مستقیم Nonce با
===بهجای استفاده ازwp_verify_nonce. - ذخیره Nonce در فایل یا دیتابیس بهعنوان یک مقدار ثابت، بهجای تولید پویا.
- نادیده گرفتن مقدار بازگشتی ۲ در
wp_verify_nonceکه منجر به خطاهای بیدلیل میشود. - نبود Nonce در فرمهای سفارشی که مستقیماً به
$_POSTدسترسی دارند. - استفاده از Nonce بهعنوان مکانیزم احراز هویت بهجای مکانیزم CSRF.
- فراموش کردن بررسی
current_user_canپس از اعتبارسنجی Nonce. - عدم مدیریت Nonce در درخواستهای AJAX طولانی که منجر به خطای انقضا میشود.
- تزریق Nonce از طریق URL بهجای POST در عملیات حساس.
- نادیده گرفتن تفاوت Nonce در Multisite و استفاده از Nonce سراسری.
- نبود اطلاعرسانی به کاربر در زمان انقضای Nonce که تجربه کاربری را خراب میکند.
هر یک از این خطاها میتواند یک سایت امن را به یک هدف آسان تبدیل کند. برای مرور جامع اصول امنیت فرمها، مطلب نانس وردپرس چیست و چگونه امنیت فرم و درخواست AJAX را تقویت کنیم؟ راهنمای عملی خوبی است. همچنین، برای شناخت بهتر حملات CSRF، مطلب CSRF و راههای دفاع عملی دیدگاه تکمیلی ارائه میدهد.
Nonce، بهتنهایی امنیت نمیسازد؛ فقط یک حلقه از زنجیرهای است که در ترکیب با مجوزدهی، پاکسازی داده و کنترل نشست معنا پیدا میکند.
پرسشهای پرتکرار درباره Nonce در وردپرس
در ادامه به پرسشهایی پاسخ میدهیم که بیشترین جستجو و بازخورد کاربران در ارتباط با Nonce و امنیت فرمهای وردپرس داشتهاند.
Nonce در وردپرس چند ساعت معتبر است؟
عمر Nonce در وردپرس بهطور پیشفرض ۲۴ ساعت است که با ثابت NONCE_LIFE تعیین میشود. اما بهدلیل بررسی همزمان دو بازه، Nonce میتواند تا ۴۸ ساعت معتبر بماند. این عمر با فیلتر nonce_life قابل تغییر است.
آیا Nonce جایگزین احراز هویت است؟
خیر و هرگز. Nonce یک مکانیزم ضد CSRF است، نه یک مکانیزم احراز هویت. Nonce اثبات میکند درخواست از یک صفحه معتبر سایت آمده، اما نمیگوید کاربر چه کسی است یا چه اختیاراتی دارد. پس از اعتبارسنجی Nonce، همیشه باید با current_user_can مجوز کاربر بررسی شود.
چرا پیام «The link you followed has expired» نمایش داده میشود؟
این پیام زمانی ظاهر میشود که Nonce ارسالی نامعتبر یا منقضی شده باشد. سه علت اصلی: نخست، صفحه کاربر بیش از عمر Nonce باز مانده است. دوم، افزونه کش یا CDN در حال کش کردن صفحه فرم است و کاربر نسخه قدیمی را میبیند. سوم، Nonce بهدرستی در فرم قرار نگرفته است.
آیا Nonce در برابر XSS محافظت میکند؟
خیر. Nonce فقط در برابر CSRF محافظت میکند. اگر سایت شما در برابر XSS آسیبپذیر باشد، مهاجم میتواند کد JavaScript را در صفحه اجرا کند و Nonce را مستقیماً از صفحه استخراج نماید. در این شرایط، Nonce بیاثر میشود. دفاع در برابر XSS نیازمند پاکسازی خروجی و اعمال CSP است.
آیا SameSite جایگزین Nonce است؟
خیر. SameSite یک لایه دفاعی اضافی است، اما بهتنهایی کافی نیست. SameSite در مرورگرهای قدیمی پشتیبانی نمیشود و در برخی سناریوها — مانند درخواستهای از همان دامنه — کوکی ارسال میشود. Nonce در ترکیب با SameSite یک دفاع دوگانه میسازد که هیچکدام بهتنهایی قادر به تأمین آن نیستند.
آیا میتوان Nonce را در URL قرار داد؟
بله، اما توصیه نمیشود. Nonce در URL میتواند در لاگ سرور، تاریخچه مرورگر یا Referer Header ذخیره شود و در معرض افشا قرار گیرد. برای عملیات حساس، Nonce باید در بدنه درخواست POST ارسال شود. تابع wp_nonce_url وجود دارد اما برای لینکهای غیرحساس یا عملیات قابل تکرار طراحی شده است.
چگونه Nonce منقضی را در AJAX مدیریت کنیم؟
سه راهکار وجود دارد. نخست، بررسی خطای ۴۰۳ در پاسخ سرور و تازهسازی Nonce با یک درخواست جداگانه. دوم، استفاده از یک مکانیزم Heartbeat که Nonce را بهطور دورهای در سمت کلاینت تازهسازی میکند. سوم، کاهش عمر Nonce به مقدار کمتر و اطمینان از اینکه عملیات در آن بازه تکمیل میشوند.
آیا Nonce در Multisite رفتار متفاوتی دارد؟
بله. Nonce در Multisite به سایت جاری گره خورده است و در سایتهای دیگر شبکه معتبر نیست. این رفتار برای امنیت مفید است اما در طراحی افزونههای شبکهای باید در نظر گرفته شود. برای درخواستهایی که بین سایتهای شبکه ارسال میشوند، باید Nonce در بافت همان سایتی تولید شود که درخواست از آن ارسال میگردد.
آیا Nonce در REST API با Nonce در فرمها متفاوت است؟
از نظر مکانیزم پایه، یکسان است اما در نحوه انتقال تفاوت دارد. در REST API، Nonce از طریق هدر X-WP-Nonce ارسال میشود، در حالی که در فرمها از طریق فیلد پنهان _wpnonce منتقل میشود. همچنین، Action استاندارد برای Nonce در REST API wp_rest است، نه یک Action سفارشی.
چگونه کد Nonce را برای جلوگیری از Timing Attack بنویسیم؟
همیشه از تابع wp_verify_nonce استفاده کنید که بهطور داخلی از hash_equals بهره میبرد. هرگز Nonce را با === یا == مقایسه نکنید. اگر به هر دلیل نیاز به مقایسه دستی داشتید، از hash_equals استفاده کنید که زمان اجرای آن مستقل از محتوای ورودی است.
نگاهی در سطح معماری هسته
در سطح مهندسی ارشد، Nonce را باید بهعنوان یکی از اجزای یک معماری امنیتی چندلایه در نظر گرفت، نه یک قابلیت مستقل. این معماری سه محدودیت ساختاری در بافت وردپرس دارد که باید صادقانه پذیرفته شوند.
محدودیت اول، نبود ذخیرهسازی سرور برای Nonceهای مصرفشده است. برخلاف Nonceهای رمزنگاری کلاسیک که پس از یکبار استفاده باطل میشوند، Nonce وردپرس در بازه عمر خود قابل استفاده مجدد است. این تصمیم، بار دیتابیس را کاهش میدهد اما یک پنجره حمله کوچک باقی میگذارد: اگر مهاجم یک Nonce را در همان بازه عمر سرقت کند، میتواند آن را چندین بار بهکار گیرد. راهحل توصیهشده در پروژههای حساس، ترکیب Nonce با بررسیهای اضافی مانند محدودیت IP و زمان اجرای درخواست است.
محدودیت دوم، وابستگی Nonce به توکن نشست است. Nonce بر پایه توکن نشست کاربر محاسبه میشود و اگر این توکن لو برود، Nonce نیز قابل بازسازی است. این محدودیت در سناریوهایی که نشست کاربر برای مدت طولانی معتبر میماند، خطرناکتر است. راهحل، ترکیب Nonce با کوتاه کردن عمر نشست و ابطال نشست پس از تغییر نقش یا رمز عبور است. برای آشنایی با اصول امنیت نشست، مطلب چگونه نشستهای کاربری را امن کنیم؟ راهنمای جامعی ارائه میدهد.
محدودیت سوم، نبود تفکیک دقیق سطح اعتماد در Nonce است. Nonce وردپرس در همه Actionها از یک مکانیزم مشترک استفاده میکند. اگر یک مهاجم بتواند Nonce یک Action کمحساسیت را سرقت کند، بهطور فنی امکان استفاده از آن در Actionهای دیگر را ندارد چون Action بخشی از محاسبه Nonce است. اما اگر Action عمومی باشد — مثل delete_post بدون شناسه منبع — Nonce میتواند برای حذف هر نوشته استفاده شود. راهحل، افزودن شناسه منبع به Action است که در بخشهای قبلی بهطور کامل بررسی شد.
در سطح پیادهسازی پیشرفته، توصیه میشود Nonce را بهعنوان بخشی از یک ماژول امنیتی سهلایه طراحی کنید. لایه اول، تولید و توزیع Nonce با Action اختصاصی برای هر عملیات و منبع. لایه دوم، اعتبارسنجی در سمت سرور با wp_verify_nonce و بررسی current_user_can. لایه سوم، مدیریت انقضا در سمت کلاینت با Heartbeat یا تازهسازی خودکار. این تفکیک، آزمونپذیری را بالا میبرد و امکان جایگزینی هر لایه را بدون بازنویسی کل سیستم فراهم میکند.
در نهایت، باید پذیرفت که Nonce یک لایه دفاعی مستقل نیست. Nonce در ترکیب با SameSite، بررسی منبع درخواست، پاکسازی داده و کنترل نشست معنا پیدا میکند. اگر یکی از این لایهها ضعیف باشد، Nonce نمیتواند بهتنهایی امنیت را تضمین کند. در پروژههای سازمانی، توصیه میشود از اصول Zero Trust نیز بهره گرفته شود که در آن هر درخواست بهصورت مستقل ارزیابی میشود. برای درک این رویکرد، مطلب Zero Trust برای وردپرس چرا آینده امنیت است؟ چارچوب کامل ارائه میدهد.
Nonce یک حلقه کوچک اما حیاتی از زنجیره امنیتی است؛ حلقهای که نبودش، همه لایههای دیگر را بیاثر میکند.
بستن این مسیر
Nonce وردپرس یک مکانیزم ساده در ظاهر و پیچیده در کاربرد است. برای توسعهدهندهای که بهتازگی با وردپرس آشنا میشود، افزودن یک خط wp_nonce_field در فرم کافی به نظر میرسد. اما برای توسعهدهندهای که پروژههای حساس را میسازد، Nonce بخشی از یک معماری چندلایه است که در آن هر جزء نقش مشخصی در دفاع ایفا میکند. مهمترین درس این مسیر ساده است: Nonce را بهعنوان یک Action اختصاصی برای هر عملیات و منبع تولید کنید، در همان نقطهای که داده را دریافت میکنید اعتبارسنجی کنید و همیشه آن را با بررسی مجوز کاربر همراه کنید. اگر امروز تنها یک گام بردارید، بگذارید آن گام بازبینی همه فرمهای سایت و اطمینان از حضور Nonce در همه آنها باشد؛ چون این کوچکترین گام، بزرگترین اثر را در برابر حملات CSRF دارد. 🔐
اگر در یک پروژه واقعی با مشکل Nonce روبهرو شدهاید، برایم جالب است بدانم کدام بخش بیشترین چالش را ایجاد کرد: مدیریت انقضا در AJAX، پیادهسازی صحیح در Multisite، یا اتصال Action به شناسه منبع. تجربه خودتان را در دیدگاهها بنویسید؛ بهخصوص اگر راهکار جایگزینی پیدا کردهاید که میتواند برای خواننده بعدی مفید باشد.