
يمكن لمستخدم Docmost ذو صلاحية منخفضة تقديم attachmentId ضحية إلى نقطة نهاية التحميل العامة واستبدال المرفق المخزن لصفحة أخرى داخل نفس مساحة العمل.
يمكن لمستخدم Docmost ذي صلاحيات منخفضة توفير attachmentId خاص بالضحية إلى نقطة نهاية التحميل العامة والكتابة فوق مرفق مخزّن في صفحة أخرى داخل نفس مساحة العمل.
لقد حددت وكشفت بشكل مسؤول وأعدت إنتاج ثغرة في التفويض عالية الخطورة في Docmost، منصة التوثيق التعاوني مفتوحة المصدر.
يقدم الموقع الرسمي لـ Docmost نفسه كويكي محلي جاهز للمؤسسات مع أكثر من 3 ملايين تحميل، ويذكر أنه موثوق من قبل فرق في مؤسسات تشمل مدينة فيلنيوس، Bechtle، الحكومة الأسترالية، الصليب الأحمر، وETS Quebec.
كان الخلل موجودًا في مسار تحميل الملفات العام الذي يستخدمه Docmost أيضًا لتدفقات حفظ/تحديث المخططات.
كنت أراجع ذلك الكود مع سؤال محدد جدًا في ذهني:
ماذا يحدث إذا أثبتت نقطة نهاية التحميل صلاحية التحرير في صفحة واحدة، ولكن الهدف المراد الكتابة فوقه يتم تحديده باستخدام معرف مرفق منفصل يتحكم به المستخدم؟
في هذه الحالة، أدى هذا السؤال مباشرة إلى فشل حقيقي في ربط الكائنات.
سمح Docmost للمتصل بإرسال:
pageId لصفحة يُسمح له بتحريرها، وattachmentId ينتمي إلى صفحة مختلفة في نفس مساحة العملقام الخادم بالفعل بإجراء فحص تناسق للكتابة فوق، لكن الحارس استخدم منطقًا منطقيًا خاطئًا.
هذا يعني أن الطلب يمكن أن يمر عبر التفويض ويكتب فوق مرفق الضحية على أي حال.
أصبحت هذه المشكلة CVE-2026-34213.
Docmost: docmost/docmost
الاستشارة: GHSA-89fp-2hch-j9gp
CVE: CVE-2026-34213
تم التصحيح في: v0.71.0
---
pageId يتحكم به المهاجم مع صلاحية تحرير -> attachmentId ضحية يتحكم به المهاجم -> حارس كتابة فوق معيب يعالج الكتابة فوق عبر الصفحات على أنها صالحة -> إعادة بناء مسار التخزين من attachmentId الضحية -> بايتات المهاجم تحل محل ملف الضحية -> تستمر صفحة الضحية في خدمة المرفق المعدل
يقوم Docmost بتخزين المرفقات المرفوعة للصفحات كسجلات قاعدة بيانات بالإضافة إلى ملفات داعمة في التخزين.
بالنسبة للتحميلات العادية، يقوم الخادم بإنشاء معرف مرفق جديد وكتابة ملف جديد.
بالنسبة لتدفقات حفظ/تحديث المخططات، يعيد العميل استخدام attachmentId موجود عن قصد بحيث يمكن تحديث نفس ملف المخطط في مكانه بدلاً من إنشاء سجل مرفق جديد في كل مرة.
هذا السلوك مشروع في حد ذاته.
المشكلة هي أنه يُنشئ مسارًا عالي المخاطرة:
عندما تخلط نقطة نهاية بين هاتين المسؤوليتين، يجب على التنفيذ ربطهما معًا بالضبط.
لم يفعل Docmost ذلك.
نقاط النهاية المختلطة للإنشاء/التحديث هي أماكن شائعة لأخطاء التفويض.
السبب بسيط:
هذا هو النمط بالضبط هنا.
قام POST /api/files/upload بالتحقق من أن المتصل يمكنه تحرير الصفحة المسماة بـ pageId.
ولكن إذا تم توفير attachmentId أيضًا، تحول الخادم إلى مسار الكتابة فوق وحدد سجل مرفق موجود بشكل منفصل.
هذا جعل السؤال الأمني الحرج هو:
هل يُثبت مسار الكتابة فوق أن المرفق المحدد ينتمي بالفعل إلى الصفحة المفوّضة؟
كانت الإجابة في الإصدارات الضعيفة: لا.
كان السبب الجذري تجاوز التفويض من خلال مفتاح يتحكم به المستخدم، بالإضافة إلى خطأ في المنطق المنطقي في حارس الكتابة فوق.
بدا التدفق الضعيف كالتالي:
AttachmentController.uploadFile() قرأ pageId من بيانات النموذج متعدد الأجزاء.validateCanEdit(page, user).attachmentId اختياريًا من نفس الطلب.AttachmentService.uploadFile() قام بتحميل المرفق الموجود عبر ذلك المعرف الذي قدمه المهاجم.&& بدلاً من الرفض عند أي عدم تطابق.كان الحارس الضعيف:
if (
existingAttachment.pageId !== pageId &&
existingAttachment.fileExt !== preparedFile.fileExtension &&
existingAttachment.workspaceId !== workspaceId
) {
throw new BadRequestException("File attachment does not match");
}
هذا الشرط يرفض الطلب فقط إذا:
كل ذلك في نفس الوقت.
هذا هو عكس ما يجب أن يفعله حارس الكتابة فوق.
بالنسبة لحالة الهجوم الحقيقية، بقي المهاجم عن قصد داخل نفس مساحة العمل.
لذا:
existingAttachment.workspaceId !== workspaceId كانت falseبمجرد أن يصبح هذا المعامل false، تم تقييم شرط && بالكامل إلى false، حتى لو كان المرفق ينتمي إلى صفحة مختلفة.
لذا تعامل الخادم مع الكتابة فوق عبر الصفحات على أنها صالحة.
كان هذا هو النصف الأول من الخلل.
النصف الثاني هو ما جعل التأثير حقيقيًا.
بعد الفحص، أعادت الخدمة بناء مسار التخزين الوجهة باستخدام attachmentId واسم الملف اللذين قدمهما المهاجم:
const filePath =
`${getAttachmentFolderPath(AttachmentType.File, workspaceId)}/` +
`${attachmentId}/${preparedFile.fileName}`;
ثم، في مسار التحديث، قام Docmost فقط بتحديث البيانات الوصفية القابلة للتغيير مثل:
fileSizeupdatedAtلم يقم بإعادة ربط الملكية بصفحة المهاجم.
لذا استمرت صفحة الضحية في الإشارة إلى نفس سجل المرفق ونفس معرف المرفق. فقط بايتات الملف الأساسية تغيرت.
لهذا لم يكن هذا مجرد عدم تطابق غير ضار.
كانت بدائية كتابة فوق غير مصرح بها ودائمة.
لم يكن هذا خطأ تجميليًا ولم يكن مشكلة تضارب في اسم الملف.
لم يحتاج المهاجم إلى سباق. لم يحتاج المهاجم إلى تخمين مسار عشوائي. لم يحتاج المهاجم إلى صلاحية كتابة على صفحة الضحية.
كل ما احتاجه هو:
من هناك، يمكنه استبدال بايتات الملف المخزنة لمرفق صفحة أخرى بينما تستمر صفحة الضحية في الإشارة إلى ذلك المرفق وخدمته كما لو لم يتغير شيء.
هذا فشل مباشر في التكامل.
من الناحية العملية، يمكن للمهاجم:
النقطة المهمة هي هذه:
قبل الخادم هدفًا للكتابة فوق اختاره المهاجم، دون ربطه بالصفحة التي تم التحقق من صلاحية تحريرها بالفعل.
هذا فشل في التحكم بالوصول، وليس مجرد سوء النظافة المنطقية.
كان الاستغلال عمليًا بشكل خاص لمرفقات المخططات.
يعيد عميل Docmost استخدام attachmentId عن قصد لحفظ المخططات ويستخدم أسماء ملفات حتمية:
diagram.excalidraw.svgdiagram.drawio.svgهذا مهم لأنه يقلل من متطلبات المهاجم.
بالنسبة للمرفقات العامة، يحتاج المهاجم إلى كليهما:
بالنسبة للمخططات، اسم الملف متوقع بالفعل.
لذا إذا كان بإمكان المهاجم قراءة محتوى صفحة الضحية، فغالبًا ما يمكنه استرداد القطعة المفقودة الوحيدة التي يحتاجها:
attachmentId الضحيةفي إعداد التحقق الخاص بي، استخدمت هذا المسار بالضبط:
كان ذلك كافياً.
عبر الاستغلال حدود الصفحات وحدود المساحات داخل نفس مساحة العمل، مع استمرار تلبية فحص مساحة العمل المعيب.
لقد تحققت من المشكلة مباشرة ضد Docmost v0.70.3 باستخدام مختبر قابل للتصميم مبني من docmost/docmost:0.70.3 و Postgres و Redis.
كان تدفق PoC كالتالي:
attachmentId الضحية.POST /api/files/upload مع:
pageId = معرف صفحة المهاجمattachmentId = معرف مرفق الضحيةfile = ملف بديل يتحكم به المهاجم باستخدام اسم ملف الضحيةكان شكل الطلب الأدنى:
POST /api/files/upload
Content-Type: multipart/form-data