
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.
./scripts/build) e carregou-se em um vault de teste novo (Supernote (Unofficial) v2.9.1, ativado via "Trust author and enable plugins").Supernote IP address como 127.0.0.1, deixou-se Sync folder no padrão Supernote sync.127.0.0.1:8089, simulando um dispositivo malicioso/comprometido. Sua listagem de diretórios retorna:
{"name":"Quick notes.note","size":61,"date":"2026-07-31 00:00:00",
"uri":"/../../PWNED_BY_DEVICE_SYNC.txt","extension":"note","isDirectory":false}
name passa pelo filtro de extensão .note; uri carrega a travessia.) Qualquer GET é respondido com 200 e os bytes do arquivo — clientes HTTP reais normalizam .. do caminho da requisição de saída antes de chegar ao fio (RFC 3986), o que é irrelevante aqui, já que o código vulnerável calcula o caminho de escrita a partir da string JSON original, não da URL de requisição normalizada.PWNED_BY_DEVICE_SYNC.txt, contendo os bytes do atacante, foi criado um diretório acima da raiz do vault (irmão de testvault/, inteiramente fora do vault). O próprio data.json do plugin registrou o caminho calculado literalmente: "vaultPath": "Supernote sync/../../PWNED_BY_DEVICE_SYNC.txt".Evidências (capturas genuínas, ~/engagements/supernote-obsidian-plugin/evidence/):
01-vault-escape-file-write.png — terminal real: ls -la testvault/ PWNED_BY_DEVICE_SYNC.txt mostrando o arquivo como irmão do diretório do vault, além de seu conteúdo controlado pelo atacante via cat.02-plugin-data-json-vaultpath.png — o próprio arquivo de estado de sincronização persistido do plugin registrando "vaultPath": "Supernote sync/../../PWNED_BY_DEVICE_SYNC.txt".03-plugin-settings-security-claim.png — a UI de configurações do plugin afirmando que o comando de sincronização "never writes anywhere outside this folder."Um atacante que possa responder como o dispositivo Supernote configurado da vítima (adjacente à LAN: AP rogue, ARP spoofing ou reivindicar o IP do dispositivo) pode fazer o cliente Obsidian da vítima criar um novo arquivo controlado pelo atacante em qualquer caminho do sistema de arquivos onde o processo desktop possa gravar — dentro do vault (por exemplo, um arquivo totalmente novo em .obsidian/plugins/<new-id>/, preparando o terreno para abuso adicional de confiança de plugins) ou inteiramente fora dele (por exemplo, ~/.config/autostart/*.desktop, um novo drop-in de crontab, ou qualquer outro local onde um novo arquivo — não uma sobrescrita — seja suficiente para obter execução ou persistência). Não pode sobrescrever silenciosamente um arquivo que já existe no caminho resolvido (o createBinary do Obsidian lança "File already exists" nesse caso, confirmado empiricamente), o que limita o primitivo a plantio de arquivos totalmente novos em vez de sobrescrita universal.
uri fornecido pelo dispositivo determina diretamente o destino no disco)Em deviceUriToVaultPath() (src/deviceSync.ts:153-161), rejeitar ou remover segmentos de caminho .. (e vazios/apenas .) após dividir deviceUri, por exemplo:
const segments = deviceUri
.split('/')
.filter((s) => s.length > 0 && s !== '.' && s !== '..')
.map((s) => s.replace(INVALID_FILENAME_CHARS, '_'));
Além disso, scanDeviceSupernoteTree() (src/FileListModal.ts:63) deve validar que o uri de uma entrada de listagem é consistente com seu name (por exemplo, uri termina com o mesmo nome de arquivo), em vez de confiar nos dois campos independentemente — a mesma classe de defesa já implicitamente utilizada em todos os outros lugares do código, que constrói caminhos apenas a partir de name/basename via getAvailablePathForAttachment().
Dostxodjayev Abdullox
Não há SECURITY.md presente neste repositório. O relato privado de vulnerabilidades do GitHub está confirmado como habilitado para philips/supernote-obsidian-plugin (gh api repos/philips/supernote-obsidian-plugin/private-vulnerability-reporting --jq .enabled → true; 0 avisos de segurança publicados anteriormente). Aplica-se o fluxo padrão do GHSA: https://github.com/philips/supernote-obsidian-plugin/security/advisories/new.