
PoC — path traversal via sincronização maliciosa de dispositivo no plugin Supernote Obsidian (GHSA-3gx3-r874-5pp4, CVE-2026-86999, CVSS 5.6).
Status da CVE: solicitada, aguardando atribuição. Esta descoberta é publicada como GHSA-3gx3-r874-5pp4. Após a atribuição da CVE, este repositório é renomeado
CVE-YYYY-NNNNN-supernote-obsidian-plugin-PoCe este banner é substituído pelo link da CVE.
| Pesquisador | Dostxodjayev Abdullox (@squeeze440) |
| Aviso | GHSA-3gx3-r874-5pp4 |
| CVSS 3.1 | 5.6 (Médio) |
| Fraqueza | CWE-22, CWE-73 |
Path Traversal na funcionalidade de sincronização automática do dispositivo do plugin Supernote (Unofficial) para Obsidian (philips/supernote-obsidian-plugin) v2.9.1 permite que um dispositivo "Supernote" malicioso ou comprometido (ou um atacante no caminho/na LAN que se passe pelo IP do dispositivo emparelhado) faça o cliente Obsidian da vítima gravar um novo arquivo arbitrário em qualquer local onde o processo desktop possa gravar — incluindo fora da pasta de sincronização configurada e fora do próprio vault — através de um campo uri manipulado na resposta de listagem de diretórios do dispositivo.
philips/supernote-obsidian-plugin ("Supernote (Unofficial)"), um plugin da comunidade Obsidian.md que sincroniza as notas de um dispositivo físico Supernote e-ink para um vault através do servidor HTTP local "Browse and Access" do dispositivo (http://<device-ip>:8089, sem autenticação, por design da funcionalidade do dispositivo).
48db5bf4dcfb100632c84699c830c1309da1abb9supernote-typescript: commit 195415b3a1f74147... (não implicado em si — o bug está inteiramente no código de planejamento de sincronização do próprio plugin)5.6 Médio — CVSS:3.1/AV:A/AC:H/PR:N/UI:R/S:C/C:N/I:H/A:N
AV:A — o servidor HTTP do dispositivo é em texto simples, não autenticado e configurado por um endereço IPv4 simples (IP_VALIDATION_PATTERN, src/settings.ts:6); alcançá-lo como "o dispositivo" requer posição de rede adjacente à LAN (AP rogue, ARP spoofing ou reivindicar o IP), não acesso remoto arbitrário.AC:H — a exploração depende de o atacante já ocupar essa posição de rede quando o plugin da vítima se comunica com directConnectIP:8089; não é um gatilho remoto de disparo único.UI:R — requer que a vítima execute "Sync supernote notes now" (ou tenha o toggle de sincronização automática já ativado) enquanto aponta para o endpoint controlado pelo atacante.S:C — o componente vulnerável é um plugin do Obsidian com escopo de vault; a gravação ocorre inteiramente fora do vault, no sistema de arquivos do host subjacente, que é um escopo de segurança diferente.C:N — este é um primitivo somente de escrita; nada é lido de volta pelo atacante.I:H — bytes controlados pelo atacante chegam a um caminho escolhido pelo atacante com nome de arquivo escolhido pelo atacante. Ressalva limitada: writeBinaryAt() (src/syncEngine.ts:40-47) só toma o ramo de criação para caminhos ainda não rastreados no próprio manifesto de sincronização do plugin, e o Vault.createBinary() do Obsidian se recusa a sobrescrever silenciosamente um arquivo que já existe fisicamente no caminho resolvido (confirmado empiricamente — ver PoC) — portanto isto é "plantar um novo arquivo em qualquer lugar", não "sobrescrever qualquer arquivo existente".A:N — nenhum impacto de disponibilidade demonstrado.runDeviceSync() (src/syncEngine.ts:100-196) lista os arquivos do dispositivo emparelhado via scanDeviceSupernoteTree() (src/FileListModal.ts:53-68), que percorre recursivamente a listagem de diretórios HTTP do próprio dispositivo e mantém qualquer entrada cujo name corresponda a /\.(note|spd)$/i (src/FileListModal.ts:46,63). O campo uri de cada entrada — uma string separada e controlada independentemente no mesmo objeto JSON retornado pelo dispositivo — nunca é validado em relação ao name ou a qualquer outra coisa.
Esse uri bruto é então alimentado diretamente em 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 remove \ : * ? " < > | mas nunca remove ou rejeita segmentos de caminho ... Um uri de dispositivo /../../PWNED.txt sobrevive intacto e é concatenado à pasta de sincronização configurada (padrão "Supernote sync") para produzir vaultPath = "Supernote sync/../../PWNED.txt".
syncEngine.ts:128 calcula esse vaultPath a partir do uri bruto da listagem, então ensureFolder()/writeBinaryAt() (syncEngine.ts:146-148, 21-47) o passam diretamente para app.vault.getAbstractFileByPath() / createBinary() / modifyBinary() sem qualquer verificação de travessia. A própria resolução de caminho do Obsidian então normaliza os segmentos .. em relação ao diretório real do vault no disco, fazendo a gravação cair acima da pasta de sincronização — e, com segmentos ../ suficientes, acima da raiz do vault inteiramente, no sistema de arquivos do host, no escopo de permissão do próprio usuário do SO.
Verificação de irmãos: todos os outros pontos de gravação no vault neste código (src/main.ts — captura de espelhamento de tela, importação de PDF/markdown, "attach to note", DownloadListModal em src/FileListModal.ts:245-246) constroem seu caminho de destino via o próprio app.fileManager.getAvailablePathForAttachment(file.name) do Obsidian, que só consome o campo name do dispositivo e não é explorável dessa forma. deviceUriToVaultPath() na funcionalidade mais recente de sincronização automática é o único ponto que, em vez disso, monta manualmente um caminho a partir do campo uri do dispositivo, e é o que pulou a sanitização de .. — um caso claro de "verificado em todos os irmãos, exceto neste."
A própria UI de configurações do plugin afirma: "The sync command never writes anywhere outside this folder" (src/settings.ts, descrição da pasta de sincronização) — este PoC falsifica diretamente essa garantia.
Confirmado dinamicamente de ponta a ponta contra um binário real e não modificado do Obsidian 1.13.4 desktop (Xvfb + fluxbox + xdotool + scrot), executando o main.js compilado real do plugin — sem simulação do código do plugin.