
PoC — ZotLit में attachment import अनुमोदित न किए गए स्थानीय पथों से फ़ाइलें कॉपी करता है (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 में attachment-import सुविधा में फ़ाइल नाम या पथ का बाहरी नियंत्रण एक ऐसे हमलावर को, जो साझा/सिंक किए गए Zotero लाइब्रेरी को नियंत्रित करता है, एक तैयार किए गए linked_file attachment पथ के माध्यम से पीड़ित के फ़ाइलसिस्टम से मनमानी स्थानीय फ़ाइलों को पीड़ित के Obsidian vault में प्रकट करने की अनुमति देता है।
उत्पाद
ZotLit — Zotero एकीकरण के लिए Obsidian प्लगइन (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 में आयात/सिंक करना होगा और तैयार किए गए attachment का संदर्भ देने वाले नोट पर ZotLit की note-import (या annotation/citation embed) सुविधा चलानी होगी; यह प्लगइन की मुख्य, डिफ़ॉल्ट-सक्षम कार्यक्षमता का नियमित उपयोग है (attachment.import डिफ़ॉल्ट रूप से true है), कोई असामान्य क्रिया नहीं। I:N/A:N — यह निष्कर्ष केवल एक read/copy प्रिमिटिव है; कोई गंतव्य-पक्ष पथ ट्रैवर्सल का दावा नहीं किया गया है (संबंधित लेकिन असत्यापित अवलोकन के लिए विवरण देखें)।
विवरण
ZotLit attachment मेटाडेटा को सीधे Zotero के SQLite डेटाबेस (या सिंक किए गए/साझा लाइब्रेरी) से पढ़ता है, जिसमें फ्री-टेक्स्ट itemAttachments.path कॉलम भी शामिल है, और "linked" attachment को कहाँ से पढ़ना है यह निर्धारित करते समय उस पर पूरी तरह भरोसा करता है:
packages/db/src/lib/zt-path.ts:62-63 — attachmentAbsPath(), केस "linked-absolute":
case "linked-absolute":
return parsed.path;
linkMode: 2 (linked_file) पंक्तियों के लिए जिनके path में attachments: बेस-डिर प्लेसहोल्डर नहीं है, parsed.path वह कच्चा DB स्ट्रिंग है जो पढ़ने के लिए पूर्ण फ़ाइलसिस्टम पथ के रूप में शब्दशः लौटाया जाता है — कोई allowlist नहीं, Zotero डेटा डिरेक्टरी या किसी उपयोगकर्ता-अनुमोदित स्थान तक कोई सीमा नहीं।
packages/db/src/lib/context/zt-template-attach.ts:101-106 — attachmentFilename(), केस "linked-absolute":
case "linked-absolute":
return basename(path.path);
in-vault प्रतिलिपि बनाने के लिए उपयोग किया जाने वाला फ़ाइलनाम उसी हमलावर-नियंत्रित पथ से basename() के माध्यम से प्राप्त किया जाता है, इसलिए गंतव्य फ़ाइलनाम हमलावर-प्रभावित है लेकिन पथ-विभाजक-रहित है (इस शाखा पर ट्रैवर्सल-सुरक्षित)।
apps/obsidian/src/services/note-import/note-parser.ts:403-430 — resolveEmbeddedImage() को Zotero साहित्य-नोट के एम्बेडेड इमेज एनोटेशन को Obsidian Markdown में परिवर्तित करते समय लागू किया जाता है (ZotLit की "Import Note" सुविधा की एक नियमित, डिफ़ॉल्ट क्रिया)। यह sourcePath = attachmentAbsPath(attachment, ...) को हल करता है और deps.resolveLink({ sourcePath, vaultName: \${attachment.key}-${filename}` })` को कॉल करता है — जो किसी भी हमलावर-चयनित पूर्ण पथ की एक प्रतिलिपि को vault में कतारबद्ध करता है।
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() वास्तविक read+write करता है बिना किसी पथ सत्यापन के: stat(source) फिर copyFile(source, dest)।
शुद्ध प्रभाव: एक हमलावर जो पीड़ित की लाइब्रेरी में एक linked_file (linkMode 2) Zotero आइटम डाल सकता है — उदाहरण के लिए एक साझा Zotero समूह लाइब्रेरी, एक .rdf/.json/Better BibTeX निर्यात जिसे पीड़ित आयात करता है, या एक सिंक किया गया लाइब्रेरी जिस पर हमलावर का लेखन अधिकार है — वह उस आइटम के attachment पथ को पीड़ित की डिस्क पर किसी भी फ़ाइल पर सेट कर सकता है (~/.ssh/id_rsa, ब्राउज़र क्रेडेंशियल स्टोर, अन्य vaults, .env फ़ाइलें, आदि)। जिस क्षण पीड़ित डिफ़ॉल्ट attachment.import: true सेटिंग के साथ ZotLit की note-import चलाता है (या उस एनोटेशन को रेंडर/एम्बेड करता है), ZotLit चुपचाप उस फ़ाइल की सामग्री को एक अनुमानित नाम (<attachmentKey>-<basename>) के अंतर्गत पीड़ित के Obsidian vault में प्रतिलिपि कर देता है। चूँकि vaults नियमित रूप से सिंक किए जाते हैं, git-कमिट किए जाते हैं, या प्रकाशित किए जाते हैं, यह उस डेटा को, जिस तक हमलावर अन्यथा कभी नहीं पहुँच सकता, ऐसे स्थान पर ले जाता है जिसे हमलावर (या सिंक लक्ष्य तक पहुँच रखने वाला कोई भी) पढ़ सकता है।
संबंधित, असत्यापित अवलोकन (इस निष्कर्ष के PoC का हिस्सा नहीं): attachmentFilename() की सहोदर "storage" शाखा (zt-template-attach.ts:103-104) कच्चा पथ सफ़िक्स लौटाती है बिना अन्य दो शाखाओं पर लागू basename() कॉल के। यह स्वतंत्र रूप से शोषणीय है या नहीं, यह इस पर निर्भर करता है कि Zotero की स्वयं की storage-sync प्रक्रिया स्थानीय रूप से डाउनलोड की गई फ़ाइलों का नाम कैसे रखती है, जो इस रिपॉज़िटरी के बाहर है और सत्यापित नहीं किया गया — यहाँ केवल एक हार्डनिंग अंतराल के रूप में चिह्नित किया गया है जिसे निरंतरता के लिए बंद करना उचित है।
प्रमाण अवधारणा
गतिशील सत्यापन: सटीक कमज़ोर फ़ंक्शन (parseAttachmentPath, attachmentAbsPath, attachmentFilename, copyAttachments/copyAttachment/writeCopy/destMatches, isErrno, reflink) शिप किए गए स्रोत से शब्दशः निकाले गए (ऊपर file:line-उद्धृत, तर्क अपरिवर्तित) और सीधे 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 के DB से लौटाता है।attachmentAbsPath() चलाया — स्रोत को पीड़ित की फ़ाइल के रूप में शब्दशः हल किया, कोई सत्यापन नहीं।attachmentFilename() चलाया — id_rsa को सुरक्षित-दिखने वाले गंतव्य basename के रूप में हल किया।note-parser.ts:427 (${key}-${filename}) से वास्तविक vaultName निर्माण को पुनरुत्पादित किया।copyAttachments() चलाया — परिणाम { copied: 1, skipped: 0, missing: 0 }।fake-vault/attachments/EVILKEY1-id_rsa को वापस पढ़ा — सामग्री बाइट-दर-बाइट पीड़ित की मूल फ़ाइल से मेल खाती है।वास्तविक xterm में पूर्ण रन (कमांड + आउटपुट) का स्क्रीनशॉट: ../evidence/poc-run.png
प्रभाव