
PoC — استيراد المرفقات ينسخ الملفات من مسارات محلية غير معتمدة في ZotLit (GHSA-4qh7-66xv-h329، CVE-2026-87000، CVSS 5.5).
حالة CVE: مطلوب، في انتظار التعيين. تم نشر هذا الاكتشاف باسم GHSA-4qh7-66xv-h329. عند تعيين CVE، سيُعاد تسمية هذا المستودع إلى
CVE-YYYY-NNNNN-zotlit-PoCوسيُستبدل هذا الشعار برابط CVE.
| الباحث | Dostxodjayev Abdullox (@squeeze440) |
| التنبيه | GHSA-4qh7-66xv-h329 |
| CVSS 3.1 | 5.5 (متوسط) |
| نقطة الضعف | CWE-73, CWE-200 |
الملخص
التحكم الخارجي في اسم الملف أو المسار في ميزة استيراد المرفقات في AidenLx ZotLit (aidenlx/zotlit) 1.1.12 يسمح للمهاجم الذي يتحكم في مكتبة Zotero مشتركة/متزامنة بالكشف عن ملفات محلية عشوائية من نظام ملفات الضحية إلى خزنة Obsidian الخاصة بالضحية عبر مسار مرفق linked_file مُصمَّم بعناية.
المنتج
ZotLit — إضافة Obsidian لتكامل Zotero (aidenlx/zotlit، معرّف الإضافة zotlit، إصدار البيان 1.1.12)
الإصدار المُختبَر
التزام Git 41e60aaa6178629b34f8a42d104e757594b72435 (2026-07-31)
تقدير CVSS v3.1
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N — 5.5 (متوسط)
مقاييس غير بديهية: AV:L — يحدث الاستغلال عندما تعالج عملية Obsidian/zotlit المحلية للضحية بيانات وصفية لعنصر Zotero مُقدَّمة من المهاجم (تُسلَّم عبر مكتبة مشتركة/متزامنة)، وهو نفس الاصطلاح المستخدم في أخطاء "ملف ضار تعالجه تطبيق محلي"، حتى وإن كان التسليم نفسه يمكن أن يحدث عبر الشبكة (مكتبة جماعية مشتركة، ملف تصدير مُرسَل بالبريد). UI:R — يجب على الضحية استيراد/مزامنة المكتبة الضارة في Zotero وتشغيل ميزة استيراد الملاحظات في ZotLit (أو تضمين التعليقات/الاقتباسات) على ملاحظة تشير إلى المرفق المُصمَّم؛ وهذا استخدام روتيني لوظيفة الإضافة الأساسية والمفعّلة افتراضيًا (attachment.import افتراضيًا true)، وليس إجراءً غير معتاد. I:N/A:N — هذا الاكتشاف هو أساس قراءة/نسخ فقط؛ لا يُدَّعى أي اجتياز مسار على جانب الوجهة (انظر التفاصيل لملاحظة ذات صلة لكن غير مُتحقَّق منها).
التفاصيل
يقرأ ZotLit البيانات الوصفية للمرفقات مباشرة من قاعدة بيانات SQLite الخاصة بـ Zotero (أو مكتبة متزامنة/مشتركة)، بما في ذلك عمود itemAttachments.path النصي الحر، ويثق به تمامًا عند تحديد مكان قراءة مرفق "مرتبط" منه:
packages/db/src/lib/zt-path.ts:62-63 — attachmentAbsPath()، الحالة "linked-absolute":
case "linked-absolute":
return parsed.path;
بالنسبة لصفوف linkMode: 2 (linked_file) التي لا يحمل مسارها العنصر النائب attachments: للمجلد الأساسي، فإن parsed.path هو السلسلة الخام من قاعدة البيانات التي تُعاد حرفيًا كمسار نظام ملفات مطلق للقراءة — لا قائمة سماح، ولا حصر في مجلد بيانات Zotero أو أي موقع معتمد من المستخدم.
packages/db/src/lib/context/zt-template-attach.ts:101-106 — attachmentFilename()، الحالة "linked-absolute":
case "linked-absolute":
return basename(path.path);
اسم الملف المستخدم لبناء النسخة داخل الخزنة مشتق من نفس المسار الذي يتحكم فيه المهاجم عبر basename()، لذا فإن اسم ملف الوجهة متأثر بالمهاجم لكنه خالٍ من فواصل المسار (آمن ضد الاجتياز في هذا الفرع).
apps/obsidian/src/services/note-import/note-parser.ts:403-430 — يُستدعى resolveEmbeddedImage() أثناء تحويل تعليق صورة مضمّنة في ملاحظة أدبية من Zotero إلى Markdown الخاص بـ Obsidian (إجراء روتيني وافتراضي لميزة "Import Note" في ZotLit). يحل sourcePath = attachmentAbsPath(attachment, ...) ويستدعي deps.resolveLink({ sourcePath, vaultName: \${attachment.key}-${filename}` })` — مما يضع في قائمة الانتظار نسخة من أي مسار مطلق يختاره المهاجم إلى الخزنة.
apps/obsidian/src/services/attachment-import/service.ts:125-155 — يضع AttachmentImportBatch.resolveLink() في قائمة الانتظار { source: sourcePath, dest: <vault attachment folder>/<key>-<filename> }، مقيّدًا فقط بإعداد attachment.import، الذي قيمته الافتراضية true (apps/obsidian/src/services/settings/schema.ts:123).
apps/obsidian/src/lib/copy-attachments.ts:43-60 — ينفّذ copyAttachment() القراءة والكتابة الفعليتين دون أي تحقق من المسار: stat(source) ثم copyFile(source, dest).
النتيجة الصافية: يمكن للمهاجم القادر على إدخال عنصر Zotero من نوع linked_file (linkMode 2) إلى مكتبة الضحية — مثل مكتبة Zotero جماعية مشتركة، أو تصدير .rdf/.json/Better BibTeX يستورده الضحية، أو مكتبة متزامنة يملك المهاجم حق الكتابة فيها — أن يضبط مسار مرفق ذلك العنصر إلى أي ملف على قرص الضحية (~/.ssh/id_rsa، مخازن بيانات اعتماد المتصفح، خزائن أخرى، ملفات .env، إلخ). في اللحظة التي يشغّل فيها الضحية استيراد الملاحظات في ZotLit (أو يعرض/يضمّن ذلك التعليق) مع الإعداد الافتراضي attachment.import: true، ينسخ ZotLit بصمت محتوى ذلك الملف إلى خزنة Obsidian الخاصة بالضحية تحت اسم متوقع (<attachmentKey>-<basename>). ولأن الخزائن تُزامَن أو تُرفع إلى git أو تُنشر بشكل روتيني، فإن هذا ينقل بيانات لم يكن بمقدور المهاجم الوصول إليها بأي طريقة أخرى إلى موقع يمكن للمهاجم (أو أي شخص آخر لديه وصول إلى هدف المزامنة) قراءته.
ملاحظة ذات صلة، غير مُتحقَّق منها (ليست جزءًا من PoC هذا الاكتشاف): الفرع الشقيق "storage" في attachmentFilename() (zt-template-attach.ts:103-104) يعيد لاحقة المسار الخام دون استدعاء basename() المطبَّق على الفرعين الآخرين. ما إذا كان هذا قابلًا للاستغلال بشكل مستقل يعتمد على كيفية تسمية آلية مزامنة التخزين الخاصة بـ Zotero للملفات المُنزَّلة محليًا، وهو أمر خارج هذا المستودع ولم يُتحقَّق منه — يُشار إليه هنا فقط كفجوة تقوية تستحق الإغلاق للاتساق.
إثبات المفهوم
التحقق الديناميكي: تم استخراج الدوال المعرّضة للخطر بالضبط (parseAttachmentPath، attachmentAbsPath، attachmentFilename، copyAttachments/copyAttachment/writeCopy/destMatches، isErrno، reflink) حرفيًا (مع الاستشهاد بالملف:السطر أعلاه، والمنطق دون تغيير) من المصدر المُصدَّر وتنفيذها مباشرة في Node، لأن تثبيت مساحة عمل pnpm كاملًا دون اتصال لم يكن متاحًا في هذه البيئة المعزولة (سلسلة أدوات corepack/pnpm معطلة دون اتصال). كان الاستبدال الوحيد هو تبديل استدعاء مسجّل LogTape بـ console.warn — تجميلي فقط، دون أي تأثير على تدفق التحكم. السكربت: ~/engagements/zotlit/evidence/poc-verify.mjs.
الخطوات:
~/engagements/zotlit/evidence/victim-disk/id_rsa (محتوى وهمي، مُعلَّم بوضوح كمحاكى).{ key: "EVILKEY1", path: "<path to victim's id_rsa>", linkMode: 2 } — بالضبط الشكل الذي يعيده getAttachmentByKey من قاعدة بيانات Zotero.attachmentAbsPath() الحقيقية — حلّ المصدر كملف الضحية، حرفيًا، دون تحقق.attachmentFilename() الحقيقية — حلّ id_rsa كاسم ملف وجهة يبدو آمنًا.vaultName الحقيقي من note-parser.ts:427 (${key}-${filename}).copyAttachments() الحقيقية — النتيجة { copied: 1, skipped: 0, missing: 0 }.fake-vault/attachments/EVILKEY1-id_rsa مرة أخرى — المحتوى مطابق بايت ببايت للملف الأصلي للضحية.لقطة شاشة للتشغيل الكامل (الأمر + المخرجات) في xterm حقيقي: ../evidence/poc-run.png
التأثير
يمكن للمهاجم القادر على التأثير في محتويات مكتبة Zotero الخاصة بالضحية (مكتبة مشتركة/جماعية، تصدير ببليوغرافي مستورد، أو مكتبة متزامنة) أن يسحب بشكل غير مصرح به ملفات محلية عشوائية يمكن لمستخدم نظام الضحية قراءتها — مفاتيح SSH، مخازن بيانات الاعتماد، محتويات خزائن أخرى، أسرار .env/الإعدادات — إلى خزنة Obsidian الخاصة بالضحية، دون أي مطالبة أو تأكيد، في اللحظة التي يستخدم فيها الضحية وظيفة استيراد الملاحظات الأساسية في ZotLit بالإعدادات الافتراضية. غالبًا ما تُزامَن الخزائن (Obsidian Sync، git، مجلدات سحابية) أو تُنشر، لذا يحوّل هذا أساس قراءة ملف محلي إلى تعرّض عن بُعد واقعي.
نقاط الضعف
المعالجة
يُقترح حصر مصادر مرفقات linked_file (linkMode 2) في مواقع نظام الملفات التي أعدّها الضحية أو وافق عليها صراحةً (مثل baseAttachmentPath المُعدّ في Zotero نفسه، أو مجلد بيانات Zotero)، ومطالبة المستخدم قبل النسخ التلقائي لمرفق مرتبط يقع مساره خارج أي جذر متوقع. وكدفاع في العمق، يُنظر في تطبيق نفس التعقيم القائم على basename() فقط والمستخدم بالفعل في فرعي linked-absolute/linked-base من attachmentFilename() على الفرع "storage" أيضًا، حتى لا يعيد أي مسار في الكود جزءًا من اسم ملف غير مُعقَّم.
الشكر
Dostxodjayev Abdullox (GitHub: squeeze440)
قناة الإبلاغ
الإبلاغ الخاص عن الثغرات مُفعَّل على aidenlx/zotlit — أبلغ عبر مسودة GitHub Security Advisory (GHSA) على المستودع بدلًا من مشكلة عامة.