Insecure Deserialization (نابه‌امنی واسریال‌سازی) یکی از خطرناک‌ترین آسیب‌پذیری‌های وب است که در آن مهاجم با ارسال داده سریال‌شده دستکاری‌شده، اشیاء دلخواه در حافظه سرور می‌سازد و از طریق زنجیره POP (Property-Oriented Programming - برنامه‌نویسی شیءگرا بر پایه ویژگی) به اجرای کد دلخواه می‌رسد. در وردپرس، این آسیب‌پذیری از چند مسیر وارد می‌شود: توابعی مانند unserialize()، maybe_unserialize()، و کاربرد گسترده serialize در جدول wp_options، متادیتا و ترنزینت‌ها. برخلاف XSS که هدفش مرورگر است، Insecure Deserialization می‌تواند مستقیماً به RCE (Remote Code Execution - اجرای کد از راه دور) منجر شود و همین آن را در صدر لیست OWASP Top 10 قرار داده است. دفاع اصلی، جایگزینی serialize با JSON، استفاده از allowed_classes در unserialize و اعتبارسنجی سختگیرانه ورودی است. متأسفانه در وردپرس، بار اصلی دفاع بر دوش توسعه‌دهندگانی است که از این توابع استفاده می‌کنند، چرا که هسته برای سازگاری با داده‌های قدیمی همچنان آن‌ها را نگه داشته است.

در یکی از پروژه‌های امنیتی که بررسی می‌کردم، مشکل از یک فیلد ساده در تنظیمات افزونه بود که مقدار آن با unserialize بازخوانی می‌شد. آنچه در ظاهر یک کد بی‌خطر به‌نظر می‌رسید، در واقع یک درِ پشتی بالقوه به سرور بود. Insecure Deserialization از همین ظرافت‌ها تغذیه می‌کند. در این نوشتار، از ریشه‌های فنی تا دفاع عملی در وردپرس را بررسی می‌کنیم.

Insecure Deserialization چیست؟

سریال‌سازی (Serialization) فرآیند تبدیل یک ساختار داده (مانند شیء یا آرایه) به یک رشته یا بایت‌استریم است تا بتوان آن را ذخیره یا منتقل کرد. معکوس این فرآیند، واسریال‌سازی (Deserialization) نام دارد. وقتی این فرآیند روی داده‌های نامطمئن و بدون اعتبارسنجی انجام شود، مهاجم می‌تواند ساختار داده دلخواه را تزریق کند. این آسیب‌پذیری در دسته A08:2021 در OWASP Top 10 با عنوان «Software and Data Integrity Failures» طبقه‌بندی می‌شود.

تفاوت کلیدی این آسیب‌پذیری با SQL Injection یا XSS در این است که در اینجا مهاجم «کد» تزریق نمی‌کند، بلکه «ساختار داده» تزریق می‌کند. اما این ساختار داده، وقتی بازخوانی می‌شود، می‌تواند متدهای جادویی (Magic Methods) PHP را فعال کند و از همین مسیر به اجرای کد برسد. برای درک بهتر جایگاه این آسیب‌پذیری در دسته‌بندی کلی، انواع آسیب‌پذیری‌های رایج وب را ببینید.

هر بار که unserialize() روی داده‌ای اجرا می‌شود که کاربر می‌تواند آن را کنترل کند، یک درِ پشتی بالقوه گشوده شده است — حتی اگر آن داده از پایگاه داده آمده باشد.

سریال‌سازی در PHP چگونه کار می‌کند؟

PHP تابع serialize() را برای تبدیل اشیاء و آرایه‌ها به رشته فراهم می‌کند. مثال ساده:

$data = ['user' => 'admin', 'role' => 'editor'];
$serialized = serialize($data);
// خروجی: a:2:{s:4:"user";s:5:"admin";s:4:"role";s:6:"editor";}

برای اشیاء، فرمت متفاوت است و شامل نام کلاس می‌شود:

class User {
  public $name = "admin";
  public $role = "editor";
}
$u = new User();
echo serialize($u);
// خروجی: O:4:"User":2:{s:4:"name";s:5:"admin";s:4:"role";s:6:"editor";}

در unserialize، اگر رشته حاوی نام کلاسی باشد که در حافظه تعریف شده، PHP آن کلاس را نمونه‌سازی می‌کند. همین نقطه، آغاز آسیب‌پذیری است: مهاجم می‌تواند نام کلاس دلخواه را در رشته قرار دهد. برای درک بهتر PHP و مفاهیم شیءگرایی، برنامه‌نویسی شیءگرا با مثال‌های ساده را ببینید.

مسیرهای ورود در وردپرس

وردپرس در چندین نقطه از serialize و unserialize استفاده می‌کند. این استفاده‌ها هم فرصت‌اند و هم تهدید:

  • جدول wp_options: بسیاری از تنظیمات به‌صورت سریال‌شده ذخیره می‌شوند.
  • Post Meta و User Meta: متادیتا اغلب آرایه‌های سریال‌شده را ذخیره می‌کند.
  • Transients: داده‌های موقت اغلب سریال‌شده ذخیره می‌شوند.
  • Widgets: تنظیمات ویجت‌ها در wp_options به‌صورت سریال‌شده ذخیره می‌شوند.
  • REST API: در برخی نقاط، ورودی JSON به سریال تبدیل می‌شود.
  • افزونه‌ها: بسیاری از افزونه‌ها مستقیم از unserialize استفاده می‌کنند.

تابع پرکاربرد maybe_unserialize() در وردپرس، خود یک لایه اضافی است اما همچنان به unserialize متکی است و در برابر payloadهای پیشرفته آسیب‌پذیر باقی می‌ماند. برای مرور ساختار ذخیره‌سازی داده در وردپرس، مدیریت دیتابیس وردپرس را ببینید.

زنجیره POP و مکانیزم اکسپلویت

POP (Property-Oriented Programming) روشی است که مهاجم با زنجیره‌سازی متدهای جادویی کلاس‌های موجود در برنامه، به اجرای کد می‌رسد. متدهای جادویی کلیدی عبارتند از:

  • __wakeup(): هنگام unserialize فراخوانی می‌شود.
  • __destruct(): هنگام نابودی شیء فراخوانی می‌شود.
  • __toString(): هنگام تبدیل شیء به رشته فراخوانی می‌شود.
  • __call(): هنگام فراخوانی متد ناموجود فراخوانی می‌شود.

یک مثال ساده از کلاس آسیب‌پذیر:

class FileReader {
  public $filename;
  public function __destruct() {
    echo file_get_contents($this->filename);
  }
}

مهاجم با ارسال payload زیر می‌تواند محتوای فایل حساس را بخواند:

O:10:"FileReader":1:{s:8:"filename";s:13:"/etc/passwd";}

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

پیامدهای واقعی در پروژه‌های وردپرسی

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

در چند پروژه‌ای که بررسی کردم، آسیب‌پذیری از یک افزونه ساده فرم‌ساز شروع شده بود که داده‌های فرم را در قالب یک آرایه سریال‌شده در wp_postmeta ذخیره می‌کرد و بعداً با unserialize بازخوانی می‌کرد. اگر مهاجم بتواند آن متادیتا را دستکاری کند (مثلاً از طریق یک آسیب‌پذیری جانبی)، زنجیره POP قابل اجرا می‌شود. برای دیدن نمونه‌های مشابه، آسیب‌پذیری افزونه‌های وردپرس را ببینید.

دفاع مؤثر در برابر Insecure Deserialization

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

لایه ۱: جایگزینی serialize با JSON

JSON امن‌تر است زیرا هیچ مکانیزمی برای نمونه‌سازی کلاس دلخواه ندارد:

// به جای:
$data = serialize( $settings );
update_option( 'myplugin_settings', $data );

// از این استفاده کنید:
$data = wp_json_encode( $settings );
update_option( 'myplugin_settings', $data );

هنگام خواندن:

$raw = get_option( 'myplugin_settings' );
$settings = json_decode( $raw, true );

لایه ۲: استفاده از allowed_classes

اگر مجبور به استفاده از unserialize هستید، از پارامتر دوم آن استفاده کنید:

$data = unserialize( $raw, ['allowed_classes' => false] );

یا صراحتاً کلاس‌های مجاز:

$data = unserialize( $raw, ['allowed_classes' => ['MySafeClass']] );

لایه ۳: HMAC امضا

اگر داده سریال‌شده را در جایی ذخیره می‌کنید که کاربر می‌تواند آن را تغییر دهد (مثلاً کوکی)، آن را با HMAC (Hash-based Message Authentication Code - کد اصالت‌سنجی مبتنی بر هش) امضا کنید:

$secret = wp_salt( 'auth' );
$data = serialize( $payload );
$signature = hash_hmac( 'sha256', $data, $secret );
$stored = base64_encode( $signature . '|' . $data );

// هنگام خواندن:
list( $sig, $data ) = explode( '|', base64_decode( $stored ), 2 );
if ( ! hash_equals( hash_hmac( 'sha256', $data, $secret ), $sig ) ) {
  die( 'Tampered data' );
}
$payload = unserialize( $data, ['allowed_classes' => false] );

این ترکیب، هم یکپارچگی و هم اصالت داده را تضمین می‌کند.

لایه ۴: اعتبارسنجی سختگیرانه ورودی

هر داده‌ای که از کاربر می‌آید، قبل از هر پردازشی باید اعتبارسنجی و پاک‌سازی شود. در وردپرس، از توابع sanitize_* و validate_* استفاده کنید. برای درک عمیق‌تر، نوشتن کد PHP امن برای وردپرس را ببینید.

کاربرد allowed_classes در unserialize

پارامتر allowed_classes در PHP 7 به بالا اضافه شد و کنترل دقیقی بر کلاس‌هایی که می‌توانند نمونه‌سازی شوند فراهم می‌کند:

// هیچ کلاسی مجاز نیست:
$data = unserialize( $raw, ['allowed_classes' => false] );

// فقط کلاس‌های مشخص:
$data = unserialize( $raw, ['allowed_classes' => ['WP_User', 'WP_Post']] );

// همه کلاس‌ها (ناامن):
$data = unserialize( $raw, ['allowed_classes' => true] );

نکته مهم: حتی با false، متدهای __wakeup و __destruct در برخی نسخه‌های PHP ممکن است فراخوانی شوند. بنابراین allowed_classes یک لایه دفاعی است، نه یک راه‌حل کامل.

اشتباهات رایج در دفع این آسیب‌پذیری

  • اتکای صرف به maybe_unserialize: این تابع همچنان به unserialize متکی است.
  • استفاده از unserialize روی داده کاربر بدون بررسی: حتی اگر داده از پایگاه داده آمده باشد، ممکن است قبلاً آلوده شده باشد.
  • فراموش کردن HMAC در کوکی‌ها: کوکی‌های حاوی داده سریال‌شده باید امضا شوند.
  • نادیده گرفتن متدهای جادویی: هر کلاس با __wakeup یا __destruct یک نقطه بالقوه است.
  • اعتماد به افزونه‌های محبوب: بسیاری از افزونه‌های پرکاربرد نیز این آسیب‌پذیری را داشته‌اند.
  • عدم به‌روزرسانی PHP: نسخه‌های قدیمی PHP محافظت‌های کمتری دارند.

برای مرور نمونه‌های خطاهای مرتبط، خطای Trying to access array offset on null در PHP را ببینید.

پرسش‌های پرتکرار درباره Insecure Deserialization

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

آیا استفاده از JSON به‌طور کامل امن است؟ JSON در برابر نمونه‌سازی کلاس دلخواه امن است، اما در برابر XSS و prototype pollution آسیب‌پذیر باقی می‌ماند. برای مقایسه، کار با JSON در پروژه‌های واقعی را ببینید.

چطور بفهمم افزونه‌ای از unserialize استفاده می‌کند؟ کد افزونه را با ابزارهای مانند grep جست‌وجو کنید. هر موردی که unserialize یا maybe_unserialize دارد، نیازمند بررسی است.

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

برای مطالعه بیشتر درباره این آسیب‌پذیری، صفحه Insecure deserialization در ویکی‌پدیا مفید است.

خط پایان

Insecure Deserialization یکی از آن آسیب‌پذیری‌هایی است که در سکوت کامل عمل می‌کند و پیامدهایش می‌تواند از نشت داده تا RCE متغیر باشد. در بستر وردپرس، استفاده گسترده از serialize در تنظیمات، متادیتا و ترنزینت‌ها، سطح حمله را بزرگ می‌کند. دفاع مؤثر نیازمند جایگزینی serialize با JSON، استفاده از allowed_classes، امضای HMAC و اعتبارسنجی سختگیرانه ورودی است. اگر پروژه‌ای با داده‌های سریال‌شده دارید، همین امروز بازبینی کنید.

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