
مختبر Docker لإعادة إنتاج CVE-2025-11262، وهي ثغرة XSS مخزنة عمياء غير مصادق عليها في إضافة Link Whisper Free لـ WordPress. يتضمن أهدافًا ضعيفة ومصححة، وسكربت PoC، وتحليل السبب الجذري.
يحتوي هذا المستودع على مختبر Docker محلي لإعادة إنتاج CVE-2025-11262، وهي ثغرة XSS مخزّنة غير مصادَق عليها تؤثر على إضافة ووردبريس Link Whisper Free.
يقارن هذا المختبر بين نسختين من الإضافة:
| الخدمة | نسخة الإضافة | الغرض | الرابط |
|---|---|---|---|
vuln | 0.9.0 | الهدف المتأثر بالثغرة | http://127.0.0.1:8081 |
patched | 0.9.1 | هدف المقارنة المُصحَّح | http://127.0.0.1:8082 |
سلسلة الثغرة الموضَّحة هي:
طلب REST بدون مصادقة
→ يتم تخزين user_id الذي يتحكم فيه المهاجم بشكل دائم
→ يفتح مستخدم ووردبريس بصلاحيات مرتفعة صفحة اشتراك Link Whisper AI
→ تُعرض القيمة المخزنة في سياق JavaScript إداري
→ يتم تنفيذ alert("CVE-2025-11262-LAB") على النسخة المتأثرة
لا يحتاج المهاجم إلى تسجيل الدخول لزرع الحمولة المخزنة. إذ يتم تنفيذ JavaScript لاحقًا عندما يفتح مستخدم ووردبريس بصلاحيات مرتفعة الصفحة الإدارية المتأثرة.
صُمم هذا المختبر للبحث المحلي المُتحكَّم فيه، وفهم الكود المصدري، والعرض ضمن ملف الأعمال المهني فقط.
| الادعاء | الدليل | طريقة التحقق في هذا المختبر |
|---|---|---|
إضافة Link Whisper Free 0.9.0 متأثرة بالثغرة. | تُصنّف النشرات الأمنية العامة إصدارات Link Whisper Free حتى 0.9.0 ضمن الإصدارات المتأثرة. | شغّل الـ PoC ضد http://127.0.0.1:8081 وافتح رابط الإدارة المطبوع. |
إصدار Link Whisper Free 0.9.1 يحتوي على الإصلاح. | تُحدد النشرات الأمنية العامة وبيانات سجل التغييرات 0.9.1 كإصدار مُصحَّح. | شغّل نفس الـ PoC ضد http://127.0.0.1:8082؛ يجب ألا يظهر أي تنبيه. |
| زرع الحمولة يتم بدون مصادقة. | يرسل الـ PoC طلب POST بدون ملفات تعريف ارتباط ووردبريس أو تسجيل دخول أو nonce. | افحص poc/poc.py؛ فهو لا يتطلب سوى --url. |
| يحدث التأثير المرئي في منطقة إدارة ووردبريس. | تُعرض القيمة المخزنة عند فتح صفحة اشتراك Link Whisper AI بواسطة مستخدم بصلاحيات مرتفعة. | بعد تشغيل الـ PoC، سجّل الدخول كمدير وافتح رابط الإدارة المطبوع. |
قد يستمر الهدف المُصحَّح في إرجاع "ok" على مستوى HTTP. | أظهرت الاختبارات المحلية أن كلا الهدفين قد يُرجع "ok"؛ الفرق الجوهري هو ما إذا كانت الحمولة تُخزَّن وتُنفَّذ. | قارن سلوك المتصفح على 8081 و8082. |
هذا هو قسم Root Cause Summary الجاهز للاستبدال في README مباشرةً. وهو آمن للنشر العام، ولا يذكر الملفات الداخلية مثل vuln_detail.txt، ويستند إلى المختبر/المصدر الذي تستخدمه الآن.
تنتج CVE-2025-11262 عن سلسلة حقن JavaScript مخزّنة في Link Whisper Free 0.9.0.
المشكلة ليست مجرد استدعاء ترميز (escaping) ناقص واحد، بل سلسلة من عدة سلوكيات غير آمنة:
نقطة نهاية REST بدون مصادقة
→ تحقق غير كافٍ من user_id
→ تخزين دائم في wpil_ai_access_user_id
→ عرض غير آمن في سياق JavaScript إداري
→ XSS مخزّن عند فتح مستخدم بصلاحيات مرتفعة لصفحة اشتراك AI
تسجّل Link Whisper Free نقطة نهاية REST لمصادقة AI ضمن مساحة أسماء REST الخاصة بالإضافة:
const REST_SLUG = 'link-whisper';
const AI_AUTH = 'ai-auth';
تُسجَّل نقطة النهاية كمسار POST:
register_rest_route(self::REST_SLUG, self::AI_AUTH, [
'methods' => 'POST',
'callback' => [
$this,
'ai_auth_handler'
],
'permission_callback' => "__return_true",
'show_in_index' => false
]);
ولأن قيمة permission_callback هي __return_true، يمكن الوصول إلى نقطة النهاية دون مصادقة.
في هذا المختبر، نقطة النهاية الفعلية هي:
/wp-json/link-whisper/ai-auth
هذا يعني أن المهاجم غير المصادَق يمكنه إرسال طلب إلى نقطة النهاية دون جلسة ووردبريس أو nonce أو حساب مدير.
في Link Whisper Free 0.9.0، يقرأ المعالج المعاملات التي يتحكم فيها المهاجم من طلب REST:
public function ai_auth_handler( WP_REST_Request $request )
{
if(!empty($request->get_param('access_token'))){
$token = $request->get_param('access_token');
$user_id = $request->get_param('user_id');
$uid = (int)$request->get_param('uid');
$uemail = $request->get_param('uemail');
if(!empty($token) && false !== strpos($token, 'ai-')){
update_option('wpil_ai_access_token', Wpil_Toolbox::encrypt($token));
update_option('wpil_ai_access_user_id', $user_id);
update_option('wpil_ai_access_user_email', $uemail);
update_user_meta($uid, 'wpil_ai_access_user_id', $user_id);
update_user_meta($uid, 'wpil_ai_access_user_email', $uemail);
update_option('wpil_ai_access_authorized', true);
}
return 'ok';
}
return new WP_Error(400, 'Bad request', [ 'status' => 404 ]);
}
السلوك المُعرِّض للثغرة هو شرط التحقق الضعيف:
if(!empty($token) && false !== strpos($token, 'ai-')){
هذا يتحقق فقط مما إذا كان رمز الوصول المقدم يحتوي على السلسلة ai-.
لا يوجد تحقق صارم من user_id قبل تخزينه:
update_option('wpil_ai_access_user_id', $user_id);
ونتيجة لذلك، يمكن تخزين JavaScript يتحكم فيها المهاجم بشكل دائم في جدول خيارات ووردبريس.
تُخزَّن قيمة user_id التي يتحكم فيها المهاجم في خيار ووردبريس:
wpil_ai_access_user_id
في هذا المختبر، يرسل الـ PoC الحمولة المحلية التالية فقط:
</script><script>alert("CVE-2025-11262-LAB")</script>
في الخدمة المتأثرة، تُخزَّن الحمولة كقيمة لـ wpil_ai_access_user_id.
لا يحتاج المهاجم إلى تسجيل الدخول لزرع الحمولة، إذ تُزرع الحمولة عبر نقطة نهاية REST غير المصادَق عليها.
تُسترجَع القيمة المخزنة لاحقًا عبر منطق إعدادات الإضافة:
public static function get_linkwhisper_ai_user_id(){
return get_option('wpil_ai_access_user_id', '');
}
تُسنَد القيمة إلى $ai_id وتُعرض في صفحة اشتراك AI الإدارية.
في Link Whisper Free 0.9.0، تُدرَج القيمة مباشرة داخل سلسلة JavaScript:
body: JSON.stringify({
ai_id: "<?php echo $ai_id;?>",
subscription_id: "<?php echo ((!empty($sub)) && isset($sub->subscription_id)) ? $sub->subscription_id: null;?>"
})
ولأن $ai_id لا يُرمَّز قبل إدراجه في سياق JavaScript، يمكن لحمولة مخزنة الخروج من السلسلة المقصودة وتنفيذ JavaScript عند فتح الصفحة الإدارية.
مع حمولة المختبر، يصبح الناتج المعروض في النسخة المتأثرة مكافئًا لما يلي:
body: JSON.stringify({
ai_id: "</script><script>alert("CVE-2025-11262-LAB")</script>",
subscription_id: ""
})
في المتصفح، تُنهي وسمة الإغلاق </script> المحقونة كتلة البرنامج النصي الأصلية، ويتم تنفيذ كتلة <script> المحقونة.
تُزرع الحمولة بواسطة مهاجم غير مصادَق، لكن التنفيذ يتطلب أن يفتح مستخدم ووردبريس بصلاحيات مرتفعة الصفحة الإدارية المتأثرة:
/wp-admin/admin.php?page=link_whisper_ai_subscription
في هذا المختبر، تُفتح الصفحة المتأثرة بصلاحيات مدير ووردبريس لإظهار نافذة التنبيه.
وهذا يجعل المشكلة XSS مخزّنة بدون مصادقة تستهدف مدراء ووردبريس المصادَق عليهم أو المستخدمين ذوي الصلاحيات المرتفعة الذين يمكنهم الوصول إلى صفحة اشتراك Link Whisper AI الإدارية.
يضيف إصدار Link Whisper Free 0.9.1 تحققًا أكثر صرامة قبل تخزين قيم مصادقة AI.
يتطلب المعالج المُصحَّح تطابق رمز الوصول ومعرّف المستخدم مع تنسيقات صارمة:
if(
!empty($token) &&
false !== strpos($token, 'ai-') &&
(bool) preg_match('/\Aai-[0-9a-f]{64}\z/i', $token) &&
(bool) preg_match('/\A[0-9a-f]{32}\z/i', $user_id)
){
update_option('wpil_ai_access_token', Wpil_Toolbox::encrypt($token));
update_option('wpil_ai_access_user_id', $user_id);
update_option('wpil_ai_access_user_email', sanitize_email($uemail));
update_option('wpil_ai_access_authorized', true);
}
التحقق المهم المضاف لـ user_id هو:
preg_match('/\A[0-9a-f]{32}\z/i', $user_id)
يمنع هذا تخزين أي JavaScript عشوائية كمعرّف مستخدم AI.
كما يرمّز إصدار 0.9.1 القيمة قبل عرضها في سياق JavaScript:
body: JSON.stringify({
ai_id: "<?php echo esc_attr($ai_id);?>",
subscription_id: "<?php echo ((!empty($sub)) && isset($sub->subscription_id)) ? esc_attr($sub->subscription_id): '';?>"
})
وبالتالي يخفف الإصلاح المشكلة عند نقطتين: