
CVE-2026-39154، ثغرة XSS مخزّنة في CometChat JS SDK
يحتوي هذا الدليل على إعادة إنتاج صادقة ومكتفية ذاتيًا لـ مصبّ العرض (render sink)
الكامن وراء CVE-2026-39154: وهو عيب برمجي من نوع XSS مخزّن في CometChat
JavaScript SDK 3.0.12، حيث يُعرَض حقل data.text الخاص برسالة مجموعة داخل
عملاء الأعضاء الآخرين دون ترميز مخرجات سياقي.
يعمل هذا محليًا بالكامل وببيانات اصطناعية. وهو لا يتصل بخدمة CometChat الحية ولا بأي طرف ثالث، ولا يفحصها ولا يصادق عليها. إنه مجرد عرض محلي لمسار العرض في العميل المعرّض للثغرة.
مستودع PoC المطابق (المذكور في التحليل): https://github.com/defineid/Wildfire
التحليل: writeup-cometchat-xss.html في هذه المجموعة.
| الحقل | القيمة |
|---|---|
| CVE | CVE-2026-39154 |
| المورّد / المنتج | CometChat · JavaScript SDK 3.0.12 |
| الفئة | XSS مخزّن (CWE-79) |
| نقطة النهاية | POST /v3.0/messages · حقل الحقن data.text |
| المستقبِل | receiverType: group |
| التفاعل | لا يوجد؛ تُنفَّذ الحمولة المخزّنة تلقائيًا عند فتح أحد الأعضاء للمجموعة |
| الإفصاح | أُبلِغ عنه إلى CometChat في فبراير 2026 (الطلب 42660)؛ ووسم المورّد الحالة بـ Solved؛ وأُسنِد CVE من قِبل MITRE |
| المرجع | https://www.cve.org/CVERecord?id=CVE-2026-39154 |
ملاحظة حول الإصدار المُصلَح: يذكر التحليل المنشور أن المورّد عالج المشكلة ("VENDOR: SOLVED") لكنه لا يسمّي رقم SDK/build مُصلَحًا محددًا، ولا يُقدَّم أي رابط استشارة منفصل من المورّد يتجاوز سجل CVE أعلاه. لا يُؤكَّد أي رقم إصدار مُصلَح هنا لأن التحليل لا يقدّم واحدًا.
تحمل رسالة CometChat من نوع TextMessage نصّها في data.text، وهو مكشوف لكود
واجهة المستخدم بوصفه message.getText(). يمكن لعضو مُصادَق أن يرسل إلى مجموعة
رسالة يحتوي data.text فيها على ترميز HTML، على سبيل المثال:
POST /v3.0/messages
{
"receiverType": "group",
"receiver": "<groupId>",
"category": "message",
"type": "text",
"data": { "text": "" }
}
تُخزّن المنصّة ذلك النص حرفيًا. وعندما يفتح أي عضو في المجموعة المحادثة، يأخذ مسار عرض قائمة الرسائل في العميل data.text ويُدرجه في DOM بصيغة HTML، دون ترميز سياقي للمخرجات (كتابة على نمط innerHTML بدلًا من كتابة textContent). وبالتالي يُحلَّل ترميز ``؛ ويفشل تحميل src=x الخاص به، ما يُشغّل معالج onerror، فيُنفَّذ سكربت المهاجم في جلسة الضحية. ولأن الرسالة مخزّنة، فإن هذا هجوم مستمر ولا يتطلّب أي تفاعل من الضحية سوى عرض المجموعة.
يُطهّر الإصلاح المُفصح عنه هذا الحقل / يرمّزه سياقيًا، بحيث يُعرض المدخل نفسه كنص خامل بدلًا من تنفيذه.
المهاجم هو عضو مصادَق عليه في المجموعة؛ والضحايا هم بقية أعضاء تلك المجموعة. تُسلَّم الحمولة مرة واحدة، وتُخزَّن على الخادم، وتُنفَّذ تلقائيًا في متصفح كل عضو عند عرضه للمحادثة. لا حاجة إلى نقر أو منح إذن أو أي تفاعل إضافي. الأثر في التقرير الأصلي: تنفيذ سكربت عشوائي في جلسة كل عضو (سرقة الجلسة/الرمز، تنفيذ إجراءات نيابةً عن الضحية، الكشف عن بيانات المحادثة).
repro.html — صفحة محلية مجرّدة (خلفية بيضاء بسيطة، CSS أدنى، منطقة قائمة رسائل بسيطة) تقوم مقام مسار العرض الضعيف فقط في SDK 3.0.12. وهي تحتفظ بنسخة محلية أمينة من المصرف غير المُطهَّر:
function getText(message){ return message.data.text; } // mirrors TextMessage#getText()
...
body.innerHTML = getText(message); // *** the SDK 3.0.12 sink, reproduced ***
يعرض كائن رسالة مخزّنة اصطناعيًا عبر ذلك الـ sink بحيث يُنفَّذ السكربت المحقون محليًا. وتذكر ملاحظة صريحة مضمّنة في الصفحة أن هذا إعادة إنتاج محلية لـ sink الخاص بـ SDK 3.0.12، وأن SDK المُصلَّح يُطهّر الحقل، وأنه لا تشارك أي خدمة CometChat حيّة.
payload.json — كائن TextMessage المخزّن الاصطناعي. حقل data.text فيه هو نقطة الحقن (مُعلَّم بمفاتيح _injection_point / _note، لأن JSON لا يدعم التعليقات). إن متجه الحقن (``) مطابق للتقرير المُفصح عنه؛ أما الإجراء فقد غُيِّر إلى علامة داخل الصفحة غير ضارة.
payload.txt — الحقن نفسه موثّقًا بنص عادي، بما في ذلك الطلب المصنوع الأصلي، مع ملاحظة صريحة بأنه لا يُرسَل إلى أي مكان.
إن repro.html هذا بديل مصغّر كُتب انطلاقًا من الآلية الموصوفة في التقرير.
وهو ليس مقتطفًا من الشيفرة المصدرية للبائع؛ فلم يُحصل على SDK الحقيقي
ولم يُفكَّك لأجل هذا الاستنساخ.
الحمولة في هذا الاستنساخ غير ضارة: فبدلًا من alert(document.domain)، يقوم
معالج onerror بتعيين window.__xss_fired = true، ويكتب RESULT: XSS-FIRED في
الصفحة، ويعيّن عنوان المستند. لا يُسرَّب أي شيء.
cd poc/CVE-2026-39154
python3 -m http.server 5599
# then open http://localhost:5599/repro.html in a browser
repro.html يعرض نسخة مضمّنة من الرسالة المخزّنة بشكل متزامن (بحيث يُطلق الـ sink بشكل حتمي حتى في ظل الالتقاط بدون واجهة رسومية) ويجلب بالإضافة إلى ذلك payload.json للتأكد من أن الأثر على القرص يُحمَّل ويحمل الحقل نفسه.
RESULT: XSS-FIRED باللون الأحمر — دليل على أن سكربت onerror المحقون قد نُفّذ عبر sink الـ innerHTML غير المعقّم.data.text نفسه وسيظهر كنص حرفي `` دون تنفيذ أي سكربت.payload.json بصيغة JSON سليمة وتأكد أنه يحمل الحقن في data.text (متجه ). قُدّم الدليل عبر `python3 -m http.server 5599`، وحُمّل `repro.html` في **Firefox 140.11.0esr headless** عبر `--screenshot` (حُفظ هنا باسم `repro-screenshot.png`). تُظهر اللقطة الملتقطة أن قد عُرض كعنصر صورة مكسورة (أي أن data.text قد حُلّل كـ HTML، ولم يُعرض كنص) وأن RESULT: XSS-FIRED قد كُتب في الـ DOM. يؤكد ذلك أن sink الـ innerHTML غير المعقّم ينفّذ سكربتًا محقونًا من حقل الرسالة./v3.0/messages، ولم يُستخدم أي حساب أو مجموعة، ولم يُنزَّل أي بناء حقيقي من الـ SDK أو يُنفَّذ أو يُفكَّك. الـ sink هنا هو بديل مكتوب يدويًا بأمانة استنادًا إلى الآلية الموصوفة في التقرير، وليس الكود الذي يشحنه المورّد. لم يتم تأكيد إصدار الـ SDK المُصلَّح الدقيق لأن التقرير لا يذكر أي إصدار.هذه إعادة إنتاج محلية دون اتصال باستخدام بيانات اصطناعية مولّدة ذاتيًا فقط، بُنيت لتوثيق CVE سبق الإبلاغ عنه وحُلّ من قبل المورّد. وهي تستهدف بديلًا محليًا لمسار العرض المصاب، وليس أي نظام حي أو تابع لطرف ثالث. الحمولة عبارة عن علامة غير ضارة داخل الصفحة؛ لا تحتوي على أي تسريب أو تسليح يتجاوز إثبات أن الحقل يُعرض دون تعقيم.