
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 المخزن مع:"url": "javascript:alert(document.domain)"
POST /api/pages/info مع format: "html" أعادت HTML تحتوي على:<div data-type="attachment" data-attachment-url="javascript:alert(document.domain)" data-attachment-name="policy.pdf" data-attachment-mime="application/pdf" data-attachment-size="1"><a href="javascript:alert(document.domain)" class="attachment" target="blank">policy.pdf</a></div>
استجابة HTML هذه هي الدليل الحاسم.
لم أحتاج إلى الاعتماد على ادعاء غير محدد بأن "المتصفح قد يفعل شيئًا مثيرًا للاهتمام."
التطبيق نفسه عرض مصرف التنفيذ الدقيق.
بمجرد أن ينقر المستخدم على رابط/أيقونة المرفق ذلك، ينفذ المتصفح رابط javascript: في نطاق الصفحة التي أنشأته.
بالنسبة لـ XSS القائم على المحرر، فإن لقطات الشاشة وحدها دليل ضعيف.
تظهر الأعراض، وليس فشل الحدود.
لهذا السبب بنيت إثبات المفهوم حول نقطتي تفتيش صريحتين:
أظهر إثبات التخزين أن الخادم قبل وحافظ على المخطط الخطر.
أظهر إثبات مصرف العرض أن التطبيق حول تلك القيمة المخزنة مرة أخرى إلى:
<a href="javascript:...">
هذا التقسيم مهم.
إذا قام منتج بتخزين إدخال خطر لكنه يحيده قبل كل مصرف، فقد يكون لديك فجوة في التعزيز ولكن ليس بالضرورة XSS حي.
إذا قام المنتج بتخزين إدخال خطر ثم يعرضه لاحقًا في مصرف تنفيذ حقيقي، فلديك سلسلة الثغرة الكاملة.
هذا ما حدث هنا.
تم شحن الإصلاح في v0.71.0 وعالج مسار الاستغلال المعروض من خلال تطبيق تعقيم الرابط على روابط المرفقات.
يقوم ملحق المرفق الآن باستيراد واستخدام sanitizeUrl، بما في ذلك:
data-attachment-url أثناء التحليلdata-attachment-url أثناء العرضhref للرابطمن الناحية المفاهيمية، غير التصحيح عقدة المرفق من:
إلى:
تم أيضًا تحديث المساعد من جانب العميل getFileUrl() بحيث لا تمر المخططات غير المعروفة دون تغيير.
في الإصدار المُصحح، يعيد المسار الاحتياطي sanitizeUrl(src) بدلاً من إعادة src كما هي.
هذا جزء مهم من الإصلاح لأن التصميم المعرض للخطر كان به مشكلتان معززتان:
أزال التصحيح كلا الافتراضين.
كان هذا إصلاحًا جيدًا لمسار XSS الحي لأنه أعاد معالجة رابط المرفق إلى التوافق مع بقية النموذج الأمني للمحرر.
ومع ذلك، لا يزال هناك درس أوسع في التعزيز:
التعقيم من جانب العميل أو وقت العرض ضروري هنا، لكن رفض المخططات الخطرة من جانب الخادم أثناء إنشاء/تحديث الصفحة سيكون ثابتًا أقوى.
النموذج الأكثر أمانًا على المدى الطويل هو:
الدفاع في العمق مهم في أنظمة المحتوى الغني.
للتغطية طويلة المدى، هذه هي الحالات الأكثر أهمية:
attachment.attrs.url = "javascript:..."data-attachment-url="javascript:..."href="javascript:..."/api/files/... و /files/... يجب أن تستمر في العمل بشكل طبيعيالنقطة الرئيسية هي الاتساق.
إذا كانت الروابط العادية معقمة ولكن عُقد الرابط المخصصة الحاملة للرابط ليست كذلك، فليس لدى المحرر حقًا سياسة رابط واحدة.
لديه أجزاء، والأجزاء هي المكان الذي تعيش فيه ثغرات XSS.
صنفت التوصية المنشورة هذه المشكلة على أنها:
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:L/A:N
يقع هذا عند 7.6 / عالية.
هذا تصنيف يمكن الدفاع عنه.
الخصائص المهمة هي:
يبقى التفاعل مطلوبًا لأن الضحية يجب أن تنشط رابط/أيقونة المرفق.
لهذا السبب UI:R صحيحة.
لكن بمجرد حدوث هذا التفاعل، فإن الحدود الأمنية قد فشلت بالفعل في وقت أبكر بكثير: التطبيق خزن مخططًا خطرًا وأعاده عرضه في مصرف تنفيذ.
أبلغت عن المشكلة بشكل خاص عبر توصيات GitHub الأمنية مع:
تم قبول المشكلة، وتم تخصيص CVE-2026-34212 لها، ونشرت في 14 أبريل 2026.
تسرد التوصية العامة حاليًا:
0.70.30.71.0تم إجراء التحقق المباشر على v0.70.3، والذي يطابق الإصدار المعرض للخطر المنشور.
الدرس الرئيسي هنا ليس فقط "قم بتعقيم الروابط."
الجميع يعرف ذلك بالفعل.
الدرس الأكثر إثارة للاهتمام هو:
إذا كان التطبيق يحتوي على نوع عقدة رابط آمنة ونوع عقدة رابط غير آمنة، فإن النوع غير الآمن هو السياسة الحقيقية.
غالبًا ما تتراكم أنظمة النص الغني امتدادات مخصصة أسرع من تراكم المراجعة الأمنية.
يخلق ذلك بالضبط هذا النوع من التباين:
تظهر هذه الثغرة أيضًا لماذا التحقق من المخطط ليس كافيًا.
jsonToNode() تحقق من أن المحتوى كان صحيحًا من الناحية الهيكلية لبيانات ProseMirror.
لم يثبت أن المحتوى آمن للعرض.
تلك أسئلة مختلفة.
تصبح المراجعة الأمنية أكثر حدة عندما تبقى هذه الأسئلة منفصلة:
عقدة المرفق اجتازت السؤال الأول وفشلت في الثالث.
هكذا تبقى ثغرات المحتوى المخزن داخل خطوط المحرر المنظمة جيدًا.
data-attachment-url و href للرابط مباشرة من إدخال يتحكم به المهاجم.getFileUrl() المخططات غير المعروفة دون تغيير.javascript:، لكن عُقد المرفقات لم تفعل.v0.71.0 معالجة sanitizeUrl إلى عقدة المرفق ومسار الاحتياط من جانب العميل.لم تكن هذه الثغرة حول خلل في المتصفح.
لقد كانت حول عقدة محتوى مخصصة تجاوزت افتراضات التطبيق الخاصة بسلامة الرابط.
قبل Docmost رابط مرفق يتحكم به المهاجم، وحافظ عليه خلال التخزين، ثم أعاد عرضه في رابط مباشر في نطاق التطبيق.
لهذا السبب أصبحت CVE-2026-34212.
أغلق التصحيح في v0.71.0 مسار XSS النشط بشكل نظيف، لكن الدرس الأوسع هو الذي يستحق الاحتفاظ به:
في التطبيقات كثيفة المحرر، كل عقدة مخصصة يمكنها حمل رابط هي حدود أمنية خاصة بها، ويجب مراجعتها على هذا الأساس.