
PoC — обход пути через вредоносную синхронизацию устройств в плагине Supernote для Obsidian (GHSA-3gx3-r874-5pp4, CVE-2026-86999, CVSS 5.6).
Статус CVE: запрошен, ожидает присвоения. Данная уязвимость опубликована как GHSA-3gx3-r874-5pp4. После присвоения CVE этот репозиторий будет переименован в
CVE-YYYY-NNNNN-supernote-obsidian-plugin-PoC, а этот баннер заменён ссылкой на CVE.
| Исследователь | Dostxodjayev Abdullox (@squeeze440) |
| Advisory | GHSA-3gx3-r874-5pp4 |
| CVSS 3.1 | 5.6 (Средний) |
| Слабость | CWE-22, CWE-73 |
Path Traversal в функции автоматической синхронизации с устройством плагина Supernote (Unofficial) для Obsidian (philips/supernote-obsidian-plugin) v2.9.1 позволяет вредоносному или скомпрометированному устройству «Supernote» (либо атакующему в той же сети/на пути, выдающему себя за IP-адрес спаренного устройства) заставить клиент Obsidian жертвы записать произвольный новый файл в любое место, куда может писать процесс настольного приложения — в том числе за пределами настроенной папки синхронизации и за пределами самого хранилища (vault) — через специально сформированное поле uri в ответе устройства со списком каталогов.
philips/supernote-obsidian-plugin («Supernote (Unofficial)») — плагин сообщества Obsidian.md, который синхронизирует заметки с физического e-ink устройства Supernote в хранилище через локальный HTTP-сервер устройства «Browse and Access» (http://<device-ip>:8089, без аутентификации, по замыслу самой функции устройства).
48db5bf4dcfb100632c84699c830c1309da1abb9supernote-typescript: коммит 195415b3a1f74147... (сам по себе не причастен — ошибка полностью в собственном коде планирования синхронизации плагина)5.6 Средний — CVSS:3.1/AV:A/AC:H/PR:N/UI:R/S:C/C:N/I:H/A:N
AV:A — HTTP-сервер устройства работает по открытому тексту, без аутентификации и настраивается простым IPv4-адресом (IP_VALIDATION_PATTERN, src/settings.ts:6); чтобы обратиться к нему как «устройство», требуется сетевая позиция в пределах локальной сети (поддельная точка доступа, ARP-спуфинг или присвоение IP-адреса), а не произвольный удалённый доступ.AC:H — эксплуатация зависит от того, что атакующий уже занимает эту сетевую позицию в момент, когда плагин жертвы обращается к directConnectIP:8089; это не одноразовый удалённый триггер.UI:R — требуется, чтобы жертва запустила «Sync supernote notes now» (или уже включила переключатель автосинхронизации), будучи направленной на контролируемую атакующим конечную точку.S:C — уязвимый компонент — это плагин Obsidian с областью действия в пределах хранилища; запись же происходит полностью за пределами хранилища, в базовой файловой системе хоста, что является другой областью безопасности.C:N — это примитив только для записи; ничего не считывается обратно атакующему.I:H — контролируемые атакующим байты попадают по выбранному атакующим пути с выбранным атакующим именем файла. Ограниченная оговорка: writeBinaryAt() (src/syncEngine.ts:40-47) выбирает ветку создания только для путей, ещё не отслеживаемых в собственном манифесте синхронизации плагина, а Vault.createBinary() самого Obsidian отказывается молча перезаписывать файл, который уже физически существует по разрешённому пути (подтверждено эмпирически — см. PoC) — таким образом, это «разместить новый файл где угодно», а не «перезаписать любой существующий файл».A:N — влияние на доступность не продемонстрировано.runDeviceSync() (src/syncEngine.ts:100-196) перечисляет файлы спаренного устройства через scanDeviceSupernoteTree() (src/FileListModal.ts:53-68), который рекурсивно обходит собственный HTTP-листинг каталогов устройства и сохраняет любую запись, чьё name соответствует /\.(note|spd)$/i (src/FileListModal.ts:46,63). Поле uri каждой записи — отдельная, независимо контролируемая строка в том же JSON-объекте, возвращаемом устройством, — никогда не проверяется ни на соответствие name, ни на что-либо ещё.
Этот необработанный uri затем напрямую передаётся в deviceUriToVaultPath() (src/deviceSync.ts:153-161):
const INVALID_FILENAME_CHARS = /[\\:*?"<>|]/g; // deviceSync.ts:144
export function deviceUriToVaultPath(syncFolder: string, deviceUri: string): string {
const segments = deviceUri
.split('/')
.filter((s) => s.length > 0)
.map((s) => s.replace(INVALID_FILENAME_CHARS, '_'));
const cleanRoot = syncFolder.replace(/^\/+|\/+$/g, '');
return cleanRoot ? `${cleanRoot}/${segments.join('/')}` : segments.join('/');
}
INVALID_FILENAME_CHARS удаляет \ : * ? " < > |, но никогда не удаляет и не отклоняет сегменты пути ... uri устройства вида /../../PWNED.txt проходит без изменений и присоединяется к настроенной папке синхронизации (по умолчанию "Supernote sync"), давая vaultPath = "Supernote sync/../../PWNED.txt".
syncEngine.ts:128 вычисляет этот vaultPath из необработанного uri листинга, затем ensureFolder()/writeBinaryAt() (syncEngine.ts:146-148, 21-47) передают его напрямую в app.vault.getAbstractFileByPath() / createBinary() / modifyBinary() без проверки на обход каталогов. Собственное разрешение путей Obsidian затем нормализует сегменты .. относительно реального каталога хранилища на диске, помещая запись выше папки синхронизации — а при достаточном числе сегментов ../ — выше корня хранилища, в файловую систему хоста, в область прав самого пользователя ОС.
Проверка аналогов: все остальные точки записи в хранилище в этой кодовой базе (src/main.ts — захват зеркала экрана, импорт PDF/markdown, «attach to note», DownloadListModal в src/FileListModal.ts:245-246) строят свой путь назначения через собственный app.fileManager.getAvailablePathForAttachment(file.name) Obsidian, который использует только поле name устройства и не эксплуатируется таким образом. deviceUriToVaultPath() в более новой функции автосинхронизации — единственная точка, которая вместо этого вручную собирает путь из поля uri устройства, и именно она пропустила санитизацию .. — чистый случай «проверено во всех аналогах, кроме этого».
Собственный интерфейс настроек плагина утверждает: «Команда синхронизации никогда не пишет никуда за пределами этой папки» (src/settings.ts, описание папки синхронизации) — данный PoC напрямую опровергает эту гарантию.
Динамически подтверждено сквозным образом на реальном, немодифицированном настольном бинарнике Obsidian 1.13.4 (Xvfb + fluxbox + xdotool + scrot), с запуском фактического скомпилированного main.js плагина — без мокирования кода плагина.