
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 (Medium) |
| Слабость | CWE-73, CWE-200 |
Краткое описание
Внешний контроль имени файла или пути в функции импорта вложений в AidenLx ZotLit (aidenlx/zotlit) 1.1.12 позволяет злоумышленнику, контролирующему общую/синхронизируемую библиотеку Zotero, раскрыть произвольные локальные файлы из файловой системы жертвы в хранилище Obsidian жертвы через специально сформированный путь вложения linked_file.
Продукт
ZotLit — плагин Obsidian для интеграции с Zotero (aidenlx/zotlit, id плагина 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 (Medium)
Неочевидные метрики: 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), чей path не содержит плейсхолдер базовой директории 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: <папка вложений хранилища>/<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: "<путь к 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) в репозитории, а не через публичный issue.