
Docmost قبلت رابط javascript: داخل عقدة مرفق، وحافظت عليه خلال التخزين والعرض، وأعادته كرابط قابل للنقر في نطاق Docmost.
قام Docmost بقبول رابط javascript: داخل عقدة مرفق، وحافظ عليه من خلال التخزين والعرض، ثم حوله مرة أخرى إلى رابط قابل للنقر في نطاق Docmost.
لقد حددت، وكشفت بطريقة مسؤولة، وأعدت إنتاج مشكلة XSS مخزنة عالية الخطورة في Docmost، منصة التوثيق التعاونية مفتوحة المصدر.
يقدم الموقع الرسمي لـ Docmost المنصة كويكي جاهز للمؤسسات مع أكثر من 3 ملايين تحميل، ويذكر أنها موثوقة من قبل فرق في مؤسسات تشمل مدينة فيلنيوس، Bechtle، الحكومة الأسترالية، الصليب الأحمر، و ETS Quebec.
كانت الثغرة موجودة في مكان يسهل التغاضي عنه في أنظمة النص الغني:
ليس في ملحق الرابط العادي، بل في نوع عقدة مخصص منفصل يُستخدم للمرفقات.
كنت أراجع خط المحرر مع سؤال محدد جدًا في ذهني:
إذا كانت الروابط العادية تمنع روابط javascript:، فهل تفرض عُقد المرفقات نفس القاعدة قبل أن تصل إلى مصرف رابط؟
في الإصدارات المعرضة للخطر، لم تكن تفرضها.
قبل Docmost عقدة مرفق خبيثة في JSON الصفحة، وخزن سمة url الخاصة بها كما هي، ثم عرض تلك القيمة لاحقًا كعنصر <a href="javascript:..."> قابل للنقر.
أصبحت هذه المشكلة CVE-2026-34212.
Docmost: docmost/docmost
التوصية: GHSA-cf68-cff9-hq4w
CVE: CVE-2026-34212
تم التصحيح في: v0.71.0
رابط عقدة مرفق يتحكم به المهاجم -> يتم قبول JSON الصفحة وتخزينها دون تغيير -> عرض HTML/React يحول ذلك الرابط إلى href للرابط -> يضحي المستخدم على إجراء المرفق -> يتم تنفيذ JavaScript يتحكم بها المهاجم في نطاق Docmost
يخزن Docmost محتوى الصفحة بتنسيق JSON متوافق مع ProseMirror/Tiptap.
يتضمن نموذج المحتوى هذا عُقد كتلة مخصصة لأشياء مثل:
تخزن عقدة المرفق حقولاً مثل:
urlnamemimesizeattachmentIdيقبل الخادم محتوى الصفحة بعدة تنسيقات:
jsonmarkdownhtmlويقوم بتطبيعه إلى JSON ProseMirror قبل تخزينه.
هذا يعني أن أي نوع عقدة يمكن أن يحمل رابطًا هو جزء من حدود الثقة المباشرة.
إذا قام أحد أنواع العُقد هذه في النهاية بعرض رابط <a href>، فإن معالجة مخطط الرابط ليست اختيارية.
إنها جزء من النموذج الأمني.
ملحقات المحرر المخصصة هي مصدر متكرر للانحراف الأمني.
قد يعرف النظام الأساسي بالفعل كيفية التعامل مع الروابط الخطرة بشكل صحيح، لكن كل عقدة مخصصة لا تزال بحاجة إلى إعادة تطبيق نفس القواعد على مصارفها الخاصة.
يخلق ذلك استراتيجية مراجعة يمكن التنبؤ بها:
هذا هو بالضبط ما كشف هذه الثغرة.
ملحق الرابط العادي في Docmost كان يعالج بالفعل javascript: على أنه خطر.
عقدة المرفق الخاصة به لم تفعل ذلك.
بمجرد أن ترى هذا التباين، يصبح السؤال الأمني واضحًا:
هل يمكنني الاحتفاظ بعقدة مرفق يكون عنوان url الخاص بها هو javascript: والحصول عليها معروضة مرة أخرى في رابط مباشر؟
كان الجواب نعم.
كان السبب الجذري هو عدم تناسق تعقيم الرابط عبر أنواع عُقد المحتوى.
مسار المحتوى في الخادم قبل روابط مرفق عشوائية طالما أن المحتوى الكلي يتطابق مع مخطط ProseMirror.
في الإصدار المعرض للخطر:
CreatePageDto قبل content?: string | objectPageService.parseProsemirrorContent() قام بتطبيع markdown أو html أو jsonjsonToNode(prosemirrorJson)تلك الخطوة التحقق من الصلاحية الهيكلية، وليس سلامة الرابط.
الجزء الحرج من منطق الخادم المعرض للخطر كان بشكل فعال:
prosemirrorJson = content;
jsonToNode(prosemirrorJson);
return prosemirrorJson;
لم يحدث أي تطبيع لمخطط رابط المرفق هناك.
لاحقًا، عرض ملحق المرفق القيمة التي يتحكم بها المهاجم مباشرة.
قامت عقدة المرفق المعرضة للخطر بذلك:
url: {
default: "",
parseHTML: (element) => element.getAttribute("data-attachment-url"),
renderHTML: (attributes) => ({
"data-attachment-url": attributes.url,
}),
},
ثم:
[
"a",
{
href: HTMLAttributes["data-attachment-url"],
class: "attachment",
target: "blank",
},
`${HTMLAttributes["data-attachment-name"]}`,
]
على جانب العميل، قام عرض عقدة React بتغليف ذلك مرة أخرى في:
<a href={getFileUrl(url)} target="_blank">
لكن getFileUrl() قامت فقط بمعالجة حالات خاصة:
http المطلقة/api/.../files/...أي شيء آخر تم إرجاعه دون تغيير.
لذا فإن حمولة مثل:
javascript:alert(document.domain)
نجت من:
هذا وحده كان كافياً لـ XSS مخزنة.
ما يوضح السبب الجذري بشكل خاص هو نقطة المقارنة.
ملحق الرابط العادي في Docmost منع javascript: بشكل صريح:
javascript: في parseHTML()javascript: في renderHTML()لذا كان المنتج يعرف بالفعل أن هذا المخطط خطر.
عقدة المرفق فشلت ببساطة في تطبيق نفس السياسة.
لهذا السبب لم يكن هذا "XSS عام في المحرر."
لقد كانت فجوة حدود ثقة خاصة بالعقدة.
لم تكن هذه الثغرة مجرد مسألة جماليات HTML غير آمنة.
لقد سمحت للمهاجم الذي يمكنه تحرير صفحة بالاحتفاظ بحمولة خبيثة سيتم تنفيذها لاحقًا في نطاق Docmost عندما يتفاعل مستخدم آخر مع المرفق المعروض.
هذا مهم لأن البرنامج النصي داخل النطاق يمكنه:
لا يقلل شرط النقر من هذه المشكلة إلى مسألة تافهة.
النقر جزء من سلوك المنتج العادي: تقدم واجهة المستخدم المرفق عمداً كرابط/أيقونة قابلة للتنفيذ.
لذا فإن السؤال الأمني ليس "هل يمكن للمهاجم إجبار JS عشوائي دون أي تفاعل؟"
السؤال الحقيقي هو:
هل يقوم التطبيق بتخزين محتوى يحمل برنامجًا نصيًا يتحكم به المهاجم ثم يقدمه لاحقًا لمستخدمين آخرين كمسار تفاعل موثوق؟
في الإصدارات المعرضة للخطر، فعل ذلك.
هذا هو XSS مخزنة.
كان مسار الاستغلال مباشرًا:
هذا جعل أيضًا المستخدمين ذوي الامتيازات الأعلى أهدافًا واقعية.
إذا كان مالك مساحة العمل أو مديرها أو محرر موثوق على نطاق واسع قد شاهد محتوى يتحكم به المهاجم ونقر على إجراء المرفق، فسيتم تشغيل البرنامج النصي للمهاجم في سياق تلك الجلسة ذات الامتيازات الأعلى.
هذه هي النقطة العملية المهمة:
متطلبات امتياز المهاجم كانت منخفضة فقط. مستوى امتياز الضحية هو الذي يحدد مقدار القيمة التي تحملها جلسة XSS.
لقد تحققت من المشكلة مباشرة ضد Docmost v0.70.3.
استخدم إثبات المفهوم طلبات HTTP عادية وواجهات برمجة التطبيقات الخاصة بالصفحة في التطبيق.
كان التدفق:
POST /api/pages/update مع format: "json" وعقدة مرفق يكون عنوان url الخاص بها هو حمولة javascript:.POST /api/pages/info.javascript:....كان المحتوى الخبيث الأدنى:
{
"pageId": "<pageId>",
"content": {
"type": "doc",
"content": [
{
"type": "attachment",
"attrs": {
"url": "javascript:alert(document.domain)",
"name": "policy.pdf",
"mime": "application/pdf",
"size": 1
}
}
]
},
"operation": "replace",
"format": "json"
}
النتيجة المباشرة التي لوحظت من اختباري كانت:
019d18cf-4212-70b0-894a-fe20080fb0f1POST /api/pages/info أعادت JSON المخزن مع: