شبی که یک فیلد «موضوع» سایت آموزشگاه را هک کرد

زمستان ۱۳۹۶، ساعت یازده شب، تماسی از مدیر یک آموزشگاه زبان گرفتم: «سایتمون پر از لینک‌های شرط‌بندی شده، مشتری‌ها فرار می‌کنند.» سایت هک شده بود، ولی نه از در بسته؛ از یک فرم تماس ساده که فیلد «موضوع» را بدون پاک‌سازی ذخیره می‌کرد و در پنل مدیریت، بدون escape نمایش می‌داد. یک خط <script> در دیتابیس جا خوش کرده بود و در هر بازدید پنل، یک اسکریپت خارجی را اجرا می‌کرد که در پس‌زمینه، لینک شرط‌بندی تزریق می‌کرد. آن شب، بعد از سه ساعت پاک‌سازی، به یک نتیجه رسیدم که از آن روز در هر پروژه‌ای اجرا می‌کنم: امنیت داده، ارزان‌ترین بیمه‌نامه‌ای است که یک توسعه‌دهنده وردپرس می‌تواند برای سایت خودش و مشتری‌اش بخرد — ولی فقط وقتی از روز اول بخرد، نه بعد از فاجعه.

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

چهار قاعده طلایی امنیت داده در وردپرس

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

  1. پاک‌سازی ورودی (Sanitize): هر داده‌ای از کاربر، قبل از ذخیره، با تابع مناسب نوعش پاک‌سازی شود.
  2. Escape خروجی (Escape): هر داده‌ای که نمایش داده می‌شود، با تابع مناسب context، escape شود.
  3. نانس (Nonce): هر فرم و درخواست AJAX، با نانس محافظت شود.
  4. بررسی دسترسی (Capability): هر عملیات حساس، با current_user_can محافظت شود.

این چهار قاعده، در پروژه‌های واقعی، معیار من برای قبول یا رد کد هستند. یک قاعده پنجم که مکمل است: استفاده از توابع آماده وردپرس به‌جای توابع خام PHP. برای کوئری، $wpdb->prepare؛ برای URL، esc_url_raw؛ برای HTML، wp_kses_post.

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

توابع پاک‌سازی ورودی (Sanitize)

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

نوع دادهتابع پاک‌سازینکته
متن کوتاهsanitize_text_fieldحذف تگ‌ها، کاراکترهای کنترلی و فضای اضافی
متن چندخطیsanitize_textarea_fieldمانند متن ساده، خط جدید حفظ می‌شود
ایمیلsanitize_emailحذف کاراکترهای غیرمجاز
URL (برای ذخیره)esc_url_rawحذف پروتکل‌های خطرناک
کلید / slugsanitize_keyحروف کوچک، اعداد، خط تیره، زیرخط
نام فایلsanitize_file_nameحذف کاراکترهای خطرناک در نام فایل
عدد صحیحabsintتبدیل به عدد صحیح مثبت
HTML مجازwp_kses_postفقط تگ‌های امن استاندارد پست
ایمیل چندتاییsanitize_email + آرایههر ایمیل جداگانه پاک‌سازی شود

الگوی صحیح در پردازش فرم:

$name  = sanitize_text_field( wp_unslash( $_POST['name'] ) );
$email = sanitize_email( wp_unslash( $_POST['email'] ) );
$age   = absint( $_POST['age'] );
$url   = esc_url_raw( wp_unslash( $_POST['url'] ) );
$html  = wp_kses_post( wp_unslash( $_POST['content'] ) );

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

یک تجربه میدانی: در پروژه‌ای، فیلد «شماره تماس» بدون sanitize_text_field ذخیره می‌شد. یک کاربر، شماره را با کاراکترهای کنترلی وارد کرده بود و در گزارش‌های CSV، ستون‌ها جابه‌جا می‌شدند. این باگ ظاهراً کوچک، در گزارش‌های مالی، چند ساعت کار اضافه ایجاد کرد. رفع آن، یک خط sanitize_text_field بود؛ پیدا کردنش، یک روز.

توابع escape خروجی

پاک‌سازی ورودی، نیمی از کار است. نیمه دیگر، escape خروجی بر اساس context است. جدول توابع escape:

Context خروجیتابع escapeمثال
متن بین تگ‌های HTMLesc_html<h2><?php echo esc_html( $title ); ?></h2>
مقدار attributeesc_attrclass="<?php echo esc_attr( $class ); ?>"
URL در href/srcesc_urlhref="<?php echo esc_url( $link ); ?>"
درون جاوااسکریپتesc_jsvar name = '<?php echo esc_js( $name ); ?>';
داخل textareaesc_textarea<textarea><?php echo esc_textarea( $content ); ?></textarea>
HTML مجازwp_kses_postecho wp_kses_post( $content );

الگوی کامل در قالب:

// نمایش متادیتای کاربر
$phone = get_user_meta( $user_id, '_phone', true );
if ( ! empty( $phone ) ) {
    printf( '<p>تلفن: %s</p>', esc_html( $phone ) );
}

// نمایش لینک
printf(
    '<a href="%s" title="%s">%s</a>',
    esc_url( $url ),
    esc_attr( $title ),
    esc_html( $text )
);

یک آسیب‌پذیری شایع که در بازبینی‌ها دیده‌ام: نمایش متادیتا در قالب بدون escape. حتی اگر داده در ورودی پاک‌سازی شده باشد، لایه دوم escape، شما را در برابر تغییر مسیرهای داده (افزونه‌های دیگر، ایمپورت، مهاجرت) محافظت می‌کند. راهنما در PHP امن در وردپرس و امنیت وردپرس برای مبتدیان.

پاک‌سازی، درِ ورودیِ خانه است؛ escape، پنجره‌ها. بازگذاشتن یکی از این دو، به‌اندازه سرقت از خانه، دردناک است.

نانس و محافظت از فرم

نانس (Nonce)، یک مقدار یکتا و موقت است که برای هر فرم یا درخواست AJAX تولید می‌شود و به سرور می‌گوید این درخواست از منبع معتبر آمده است. سه کاربرد اصلی: محافظت از فرم‌ها در برابر CSRF، محافظت از درخواست‌های AJAX، و محافظت از لینک‌های حساس.

// در فرم
<form method="post">
    <?php wp_nonce_field( 'my_action', 'my_nonce' ); ?>
    <input type="text" name="title" />
    <button type="submit">ذخیره</button>
</form>

// در پردازش
if ( ! isset( $_POST['my_nonce'] ) || ! wp_verify_nonce( $_POST['my_nonce'], 'my_action' ) ) {
    wp_die( 'درخواست نامعتبر' );
}
// ادامه پردازش

سه نکته حیاتی: یک — wp_nonce_field در فرم: خودکار فیلد مخفی می‌سازد. دو — wp_verify_nonce در پردازش: قبل از هر عملیات حساس. سه — check_ajax_referer در AJAX: برای endpointهای AJAX. راهنمای کامل در نانس وردپرس و پیاده‌سازی نانس در فرم‌ها. یک تجربه میدانی: در پروژه‌ای، فرم حذف نوشته بدون نانس بود. یک محقق امنیتی توانست با ارسال یک درخواست ساختگی از سایت دیگر، نوشته‌ها را حذف کند. اضافه کردن نانس در سه خط کد، این حفره را بست.

بررسی دسترسی (Capability)

پس از نانس، باید بررسی شود کاربر جاری دسترسی لازم برای عملیات را دارد:

if ( ! current_user_can( 'manage_options' ) ) {
    wp_die( esc_html__( 'شما دسترسی به این عملیات ندارید.', 'my-plugin' ) );
}

// برای عملیات روی نوشته
if ( ! current_user_can( 'edit_post', $post_id ) ) {
    wp_die( 'دسترسی غیرمجاز' );
}

// برای عملیات روی کاربر
if ( ! current_user_can( 'edit_user', $user_id ) ) {
    wp_die( 'دسترسی غیرمجاز' );
}

راهنمای کامل در توابع نقش و دسترسی، توابع بررسی وضعیت ورود، و امن‌سازی ورود ادمین. نکته مهم: در ۹۰٪ پرونده‌های امنیتی که بررسی کرده‌ام، ریشه در نبود یکی از این چهار قاعده بوده، نه در حمله پیچیده ناشناخته. این جمله را در امنیت پروژه وردپرس هم تکرار کرده‌ام.

کوئری امن با wpdb

کوئری خام با مقادیر ورودی، خطر SQL Injection دارد. همیشه از $wpdb->prepare استفاده کنید:

global $wpdb;

$author_id = absint( $_GET['author'] );
$status    = sanitize_key( $_GET['status'] );

$sql = $wpdb->prepare(
    "SELECT * FROM {$wpdb->posts}
     WHERE post_author = %d AND post_status = %s",
    $author_id,
    $status
);

$results = $wpdb->get_results( $sql );

سه placeholder مهم: %d برای عدد صحیح، %s برای رشته، %f برای اعشار. نام جدول همیشه با {$wpdb->prefix} ساخته شود. راهنمای کامل در توابع کوئری سفارشی و بهینه‌سازی کوئری‌های MySQL. یک اشتباه شایع که در پرونده‌های واقعی دیده‌ام: استفاده از $wpdb->prepare با آرایه‌های چندمقداری بدون placeholder مناسب. برای آرایه‌ها، باید تعداد placeholderها را با تعداد مقادیر هماهنگ کنید — الگوی دقیق در همان مقاله کوئری سفارشی.

امنیت در AJAX و REST API

در endpointهای AJAX و REST API، سه لایه محافظت لازم است:

// AJAX
add_action( 'wp_ajax_my_action', 'my_ajax_handler' );

function my_ajax_handler() {
    check_ajax_referer( 'my_nonce', 'nonce' );
    
    if ( ! current_user_can( 'edit_posts' ) ) {
        wp_send_json_error( array( 'message' => 'دسترسی غیرمجاز' ), 403 );
    }
    
    $data = sanitize_text_field( wp_unslash( $_POST['data'] ) );
    
    wp_send_json_success( array( 'result' => $data ) );
}

// REST API
register_rest_route( 'myplugin/v1', '/items', array(
    'methods'             => 'POST',
    'callback'            => 'my_rest_handler',
    'permission_callback' => function() {
        return current_user_can( 'edit_posts' );
    },
    'args' => array(
        'title' => array(
            'required'          => true,
            'type'              => 'string',
            'sanitize_callback' => 'sanitize_text_field',
        ),
    ),
) );

راهنمای کامل در ساخت API اختصاصی، REST API وردپرس، و احراز هویت REST API. یک آسیب‌پذیری شایع که در پرونده‌های امنیتی دیده‌ام: endpointهای REST API که permission_callback نداشتند یا __return_true داشتند بدون دلیل. نتیجه: هر کاربری می‌توانست داده حساس را بخواند یا تغییر دهد.

امنیت در ذخیره متادیتا و گزینه‌ها

ذخیره متادیتا و گزینه‌ها، نقاط ورود بالقوه هستند. الگوی صحیح:

// ذخیره در متادیتای نوشته
if ( isset( $_POST['my_field'] ) ) {
    update_post_meta(
        $post_id,
        '_my_field',
        sanitize_text_field( wp_unslash( $_POST['my_field'] ) )
    );
}

// ثبت تنظیمات با Settings API
register_setting(
    'my_plugin_group',
    'my_plugin_options',
    array(
        'type'              => 'array',
        'sanitize_callback' => 'my_plugin_sanitize',
    )
);

// تابع sanitize
function my_plugin_sanitize( $input ) {
    $clean = array();
    if ( isset( $input['api_key'] ) ) {
        $clean['api_key'] = sanitize_text_field( $input['api_key'] );
    }
    if ( isset( $input['email'] ) ) {
        $clean['email'] = sanitize_email( $input['email'] );
    }
    return $clean;
}

راهنمای کامل در کار با متاباکس‌ها، کار با User Meta، کار با Options API، و ساخت صفحه تنظیمات اختصاصی. یک نکته عملی: در ذخیره متادیتا، همیشه کلیدها را با پیشوند یکتا ذخیره کنید تا با افزونه‌های دیگر تعارض نکنند؛ همین یک قاعده، در پروژه‌های چندساله، تفاوت بین پاک‌سازی ساده و پاک‌سازی دقیق را می‌سازد.

پاک‌سازی نام فایل و آپلود

آپلود فایل، یکی از پرخطرترین نقاط سایت‌های وردپرسی است. مهاجم می‌تواند فایل PHP با پسوند دوگانه (مثل image.php.jpg) آپلود کند. الگوی امن:

$allowed = array(
    'jpg|jpeg' => 'image/jpeg',
    'png'      => 'image/png',
    'webp'     => 'image/webp',
    'pdf'      => 'application/pdf',
);

$check = wp_check_filetype( $_FILES['file']['name'], $allowed );
if ( ! $check['ext'] ) {
    wp_die( 'نوع فایل مجاز نیست' );
}

$upload = wp_handle_upload( $_FILES['file'], array(
    'test_form' => false,
    'mimes'     => $allowed,
) );

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

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

فهرست کوتاه اما گران‌قیمت از اشتباهاتی که در کدهای بازبینی‌شده دیده‌ام:

  • استفاده از یک تابع پاک‌سازی برای همه نوع داده: sanitize_text_field برای همه چیز، داده URL و HTML را نابود می‌کند.
  • فراموش کردن wp_unslash: باعث از دست رفتن بک‌اسلش‌ها و به‌هم‌ریختن متون فارسی می‌شود.
  • Escape با تابع اشتباه: استفاده از esc_url برای متن، حروف خاص را حذف می‌کند. استفاده از esc_html در attribute، attribute را می‌شکند.
  • پاک‌سازی بعد از ذخیره: اگر داده را در دیتابیس آلوده ذخیره کنید، پاک‌سازی بعدی همیشه کامل نیست.
  • نبود current_user_can در فرم‌های حساس: خطر دسترسی غیرمجاز. راهنما در نقش و دسترسی.
  • نبود نانس در فرم‌ها: خطر CSRF. راهنما در حمله CSRF و نانس وردپرس.
  • کوئری خام بدون $wpdb->prepare: خطر SQL Injection. راهنما در PHP امن.
  • نبود escape در نمایش نتایج کوئری: خطر XSS. راهنما در پاک‌سازی داده‌ها.
  • نبود بررسی is_wp_error: خطا در صورت برگشت خطا. راهنما در اعتبارسنجی داده‌ها.
  • ذخیره داده حساس در متادیتای عمومی: خطر افشا. راهنما در امنیت وردپرس.
  • نصب افزونه از منابع ناشناس: خطر آلودگی. راهنما در دانلود افزونه مطمئن.
  • نبود لاگ و پایش: در بحران، اطلاعات کافی برای دیباگ وجود ندارد. راهنما در تست و دیباگ.

جمع‌بندی

توابع امنیت و پاک‌سازی در وردپرس، در پنج گروه خلاصه می‌شوند: پاک‌سازی ورودی (sanitize_*)، escape خروجی (esc_*، wp_kses_post)، نانس (wp_nonce_field، wp_verify_nonce)، بررسی دسترسی (current_user_can)، و کوئری امن ($wpdb->prepare). سه اصل را در پایان تاکید می‌کنم: اول، امنیت را از روز اول رعایت کنید، نه بعداً. دوم، دفاع در عمق؛ هیچ لایه‌ای به‌تنهایی کافی نیست. سوم، ابزارهای بررسی امنیت را در جریان کاری خود بگنجانید.

اگر امروز یک کار در این مسیر انجام می‌دهید: به آخرین فرم یا endpoint AJAX پروژه خود نگاه کنید و ببینید آیا چهار قاعده طلایی در آن رعایت شده است. همان یک بازبینی کوچک، در پروژه‌های بعدی تبدیل به الگوی ذهنی می‌شود. اگر تجربه‌ای از یک حادثه امنیتی دارید که با یکی از این توابع حل شد، در دیدگاه‌ها بنویسید — همان گزارش‌های واقعی، این راهنما را برای توسعه‌دهنده بعدی دقیق‌تر می‌کند. 🔐