
مشاركة عامة بدت نظيفة في شجرة الصفحات، لكن نقطة نهاية البحث روت قصة مختلفة. في Docmost، الصفحات الفرعية المقيدة المخفية عن مشاهدي المشاركة العامة ما زالت قادرة على التسرب عبر نتائج البحث للمشاركة العامة.
كانت إحدى المشاركات العامة تبدو نظيفة في شجرة الصفحات، لكن نقطة نهاية البحث روت قصة مختلفة. في Docmost، يمكن للصفحات الفرعية المقيدة المخفية عن مشاهدي المشاركات العامة أن تتسرب عبر نتائج بحث المشاركات العامة.
عثرتُ على هذه المشكلة أثناء مراجعة Docmost، وهي منصة تعاونية مفتوحة المصدر للويكي والتوثيق، مع سؤال بسيط جدًا في ذهني:
إذا كانت الصفحة مخفية عمدًا عن مشاهد المشاركة العامة، فهل تحترم كل ميزة عامة نفس قيود الحظر؟
في هذه الحالة، كانت الإجابة لا.
يمكن لصفحة فرعية مقيدة أن تظل مخفية في شجرة المشاركات العامة بينما تتسرب عبر نقطة نهاية البحث للمشاركات العامة.
تم قبول المشكلة وتعيينها CVE-2026-33146.
Docmost: Docmost على GitHub
CVE: CVE-2026-33146
يقدم الموقع الرسمي لـ Docmost نفسه كويكي داخلي جاهز للمؤسسات مع أكثر من 3 ملايين تنزيل، ويذكر أنه موثوق من قبل فرق في منظمات تشمل مدينة فيلنيوس وBechtle والحكومة الأسترالية والصليب الأحمر وETS Quebec.
مشاركة عامة أصلية مع تمكين الصفحات الفرعية → سليل مقيد محذوف من الشجرة العامة → مهاجم يستعلم عن بحث المشاركة العامة → تسريب عنوان ولقطة من السليل المقيد
Docmost هو منصة تعاونية للويكي والتوثيق.
يوفر:
هذا يعني أن نموذج مشاركته العامة هو حد أمني حقيقي.
لم يكن السؤال المهم هنا هو ما إذا كان Docmost يمكنه مشاركة الصفحات بشكل عام.
السؤال الحقيقي كان:
عندما يقرر Docmost أن الصفحة الفرعية مقيدة ولا ينبغي أن تظهر لزائر المشاركة العامة، هل ينطبق هذا القيد في كل مكان في تدفق المشاركة العامة؟
في هذه الحالة، لم ينطبق.
الكثير من المراجعات الأمنية تتوقف مبكرًا بمجرد رؤية صفحة مخفية في واجهة المستخدم.
هذا ليس كافياً.
السؤال الأقوى هو هذا:
هل يفرض كل مسار خلفي نفس قرار الرؤية؟
هذا مهم لأن الحدود الأمنية لا تُحدد من خلال شكل الواجهة.
بل تُحدد من خلال ما يرجعه الخادم فعليًا.
هنا، تصرفت نقطة نهاية الشجرة العامة بأمان:
لكن مسار بحث المشاركة العامة تصرف بشكل مختلف:
هذا جعلها مشكلة حقيقية في التفويض وإفشاء المعلومات، وليس مجرد تناقض في التقديم.
لم أتعامل مع Docmost بتجربة عشوائية للمسارات على أمل ظهور شيء مثير.
الطريق الأقوى كان اختيار حد الثقة أولاً.
بالنسبة للتطبيقات التي تدعم:
من أفضل الأسئلة التي يمكن طرحها:
هل يفرض طبقة البحث نفس حدود التفويض تمامًا مثل طبقة التصفح؟
يصبح هذا السؤال ذا قيمة خاصة عندما:
هذا بالضبط حيث ظهرت هذه المشكلة.
لم يكن الخلل أن Docmost فشل في إخفاء الصفحة المقيدة في الشجرة العامة العادية.
الخلل كان أن البحث العام لم يحترم نفس منطق التقييد.
من مراجعة المصدر، استخدم تدفق الشجرة العامة اجتيازًا للسلالات مع مراعاة التقييد.
المنطقة ذات الصلة:
apps/server/src/core/share/share.service.tsهذا المسار استبعد عمدًا السلالات المقيدة باستخدام:
getPageAndDescendantsExcludingRestricted(...)لكن تدفق بحث المشاركة العامة اتبع مسارًا مختلفًا.
المناطق ذات الصلة:
apps/server/src/core/search/search.controller.tsapps/server/src/core/search/search.service.tsهناك، قام الكود بجمع السلالات باستخدام:
getPageAndDescendants(...)هذا يعني أن السلالات المقيدة بقيت ضمن نطاق البحث.
في سياق المشاركة العامة، هذا مهم جدًا لأن فرع البحث يعمل بدون سياق صلاحيات مستخدم مصادق عادي. لذلك بمجرد تضمين السلالات المقيدة في مجموعة الصفحات القابلة للبحث، يمكن أن تتسرب بياناتها الوصفية عبر الاستجابة.
لأن المهاجم لا يحتاج إلى حساب مصادق.
يحتاج فقط:
بمجرد وجود هذا الشرط، يمكن للزائر العام استعلام نقطة نهاية بحث المشاركة واسترداد:
هذا يكفي لإنشاء تسرب سرية، حتى لو لم يتم إرجاع كامل جسم الصفحة.
التمييز المهم هو أن التطبيق يشير بالفعل بوضوح إلى النموذج الأمني المقصود.
نقطة نهاية الشجرة العامة تخفي السلالات المقيدة.
لذا السؤال الحقيقي ليس:
"هل يعيد البحث مجموعة نتائج أوسع؟"
السؤال الحقيقي هو:
"هل ينتهك البحث قرار تفويض تم تطبيقه بالفعل في مكان آخر لنفس حد المشاركة العامة؟"
في Docmost، كان الأمر كذلك.
هذا يحول المشكلة من:
إلى:
لهذا السبب هي ثغرة حقيقية.
قمت بالتحقق من المشكلة بمقارنة نقطتي النهاية العامتين ذات الصلة جنبًا إلى جنب.
أولاً، اختبرت نقطة نهاية الشجرة العامة العادية باستخدام مفتاح المشاركة العامة.
مثال على الطلب:
POST /api/shares/tree HTTP/1.1
Host: 127.0.0.1:6752
Content-Type: application/json
{
"shareId": "public-share-key"
}
أعادت الاستجابة صفحة الطفل العام فقط في شجرة الصفحات.
النتيجة التمثيلية:
{
"pageTree": [
{
"id": "public-child",
"title": "Public roadmap"
}
]
}
هذا أسس السلوك المتوقع للمنتج:
ثم قمت باستعلام نقطة نهاية بحث المشاركة العامة باستخدام مصطلح ظهر داخل السليل المقيد.
مثال على الطلب:
POST /api/search/share-search HTTP/1.1
Host: 127.0.0.1:6752
Content-Type: application/json
{
"shareId": "public-share-key",
"query": "salary"
}
الاستجابة تضمنت الطفل المقيد على أي حال:
{
"items": [
{
"id": "public-child",
"title": "Public roadmap",
"highlight": "release plan and milestones"
},
{
"id": "restricted-child",
"title": "Payroll Q4",
"highlight": "salary bands and bonus targets"
}
]
}
هذا أثبت الادعاء الأساسي:
أقوى جزء في هذه المشكلة ليس الطلب الثاني بمفرده.
بل هو التباين بين نقطتي النهاية.
يظهر أن المنتج لديه بالفعل نموذج تقييد مقصود للمشاركات العامة.
لا ينبغي أن يكون السليل المقيد مرئيًا للزائر العام.
يثبت أن مسار البحث يكسر نفس الحد.
هذا يجعل من الصعب رفض المشكلة كسلوك بحث متوقع أو فجوة في التوثيق.
يضع التطبيق القاعدة من خلال استجابة الشجرة، ثم ينتهكها من خلال استجابة البحث.
هذا دليل قوي.
هذه المشكلة لا تكشف محتوى عشوائيًا عبر مساحة العمل بأكملها.
نطاقها أضيق من ذلك.
لكن داخل الشجرة الفرعية للمشاركة العامة المتأثرة، لا يزال يعطي المهاجم معرفة غير مصرح بها مفيدة:
حتى اللقطات القصيرة يمكن أن تكون مهمة.
عنوان مثل:
بالفعل يخلق قيمة أمنية للمهاجم.
لذا، بينما تم تصنيفها في النهاية كـ متوسطة، إلا أنها لا تزال مشكلة سرية صالحة مع اختراق حدودي نظيف وقابل للدفاع.
تم تعيين هذه المشكلة:
كانت شدة التقرير الفني:
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:L/I:N/A:Nيعكس هذا التقييم تسربًا سرّيًا أضيق وليس وصولاً كاملاً غير مصرح به للمستندات.
النقطة المهمة هي أن المشكلة لا تزال صالحة.
الادعاء هنا ليس:
الادعاء هو:
هذا إفشاء معلومات حقيقي يتعلق بالتفويض.
بعض الناس يتجاهلون تسريبات البيانات الوصفية بسرعة كبيرة.
هذا خطأ.
السؤال الحقيقي هو ما إذا كانت البيانات المسربة تتجاوز حدًا مقصودًا.
هنا، تجاوزته.
إذا قال التطبيق:
لكن نقطة نهاية عامة لا تزال تكشف:
فإن نموذج السرية قد فشل، حتى لو كان التأثير محدودًا.
هذا يجعله يستحق الإبلاغ.
الأخطاء النظيفة والمحدودة والقابلة للتكرار مثل هذا هي بالضبط نوع المشكلات التي تساعد في إظهار حكم مراجعة أمنية قوي.
أكثر اتجاهات الإصلاح أمانًا هو جعل البحث العام يستخدم نفس منطق السلالات الواعي بالتقييد مثل تدفق الشجرة العامة.
عمليًا، هذا يعني أن فرع بحث المشاركة لا ينبغي أن يعدد السلالات باستخدام:
getPageAndDescendants(...)
بل يجب أن يتوافق مع اجتياز المشاركة العامة الأكثر أمانًا ويستخدم:
getPageAndDescendantsExcludingRestricted(...)
إصلاح بديل هو الاحتفاظ بالتعداد الأوسع ثم تصفية السلالات المقيدة بشكل صريح قبل أن يعيد استعلام البحث النتائج.
لكن التصميم الأنظف بسيط:
يجب أن يتطابق حد البحث مع حد التصفح
هذه هي خاصية الأمان التي فشلت.
تم الإبلاغ عن هذه المشكلة بشكل خاص من خلال تدفق الإبلاغ الأمني في GitHub.
أظهر التقرير:
/api/shares/tree/api/search/share-searchتم قبول المشكلة وتعيينها:
CVE-2026-33146
كانت شدة التقرير النهائي متوسطة، وهو ما يناسب نطاق التسرب الأضيق بشكل أفضل مما لو كان ادعاءً أوسع بالخطورة.
هذا لا يضعف صحة الاكتشاف.
فقط يحدد تأثيره بشكل أكثر دقة.
الدرس الرئيسي هنا بسيط:
إخفاء شيء في نقطة نهاية عامة واحدة لا يكفي إذا كانت نقطة نهاية عامة أخرى لا تزال تكشفه.
الكثير من المطورين يفكرون في التفويض فقط في مسار العرض الواضح:
لكن الحد الفعلي أوسع.
يجب أيضًا أن تسأل:
في Docmost، كانت الإجابة لا.
هذه هي الخلاصة الحقيقية.
لم تكن هذه الثغرة حول حمولات براقة أو سلاسل استغلال معقدة.
كانت حول طرح سؤال عملي جدًا حول حدود الثقة.
أخفى Docmost الصفحة المقيدة في مكان واحد.
ثم سربها في مكان آخر.
لهذا أصبحت CVE-2026-33146.