
PoC — path traversal tramite sincronizzazione malevola del dispositivo nel plugin Supernote Obsidian (GHSA-3gx3-r874-5pp4, CVE-2026-86999, CVSS 5.6).
Stato CVE: richiesto, in attesa di assegnazione. Questa segnalazione è pubblicata come GHSA-3gx3-r874-5pp4. All'assegnazione del CVE questo repository viene rinominato
CVE-YYYY-NNNNN-supernote-obsidian-plugin-PoCe questo banner viene sostituito con il link al CVE.
| Ricercatore | Dostxodjayev Abdullox (@squeeze440) |
| Advisory | GHSA-3gx3-r874-5pp4 |
| CVSS 3.1 | 5.6 (Medio) |
| Debolezza | CWE-22, CWE-73 |
Il Path Traversal nella funzionalità di sincronizzazione automatica del dispositivo del plugin Obsidian Supernote (Unofficial) (philips/supernote-obsidian-plugin) v2.9.1 consente a un dispositivo "Supernote" malevolo o compromesso (o a un attaccante on-path/on-LAN che si spaccia per l'IP del dispositivo associato) di far scrivere al client Obsidian della vittima un file nuovo arbitrario ovunque il processo desktop possa scrivere — anche al di fuori della cartella di sincronizzazione configurata e al di fuori del vault stesso — tramite un campo uri appositamente craftato nella risposta di elenco directory del dispositivo.
philips/supernote-obsidian-plugin ("Supernote (Unofficial)"), un plugin della community di Obsidian.md che sincronizza le note di un dispositivo fisico e-ink Supernote in un vault tramite il server HTTP locale "Browse and Access" del dispositivo (http://<device-ip>:8089, senza autenticazione, per design della funzionalità del dispositivo).
48db5bf4dcfb100632c84699c830c1309da1abb9supernote-typescript: commit 195415b3a1f74147... (non direttamente coinvolto — il bug è interamente nel codice di pianificazione della sincronizzazione del plugin stesso)5.6 Medio — CVSS:3.1/AV:A/AC:H/PR:N/UI:R/S:C/C:N/I:H/A:N
AV:A — il server HTTP del dispositivo è in chiaro, non autenticato e configurato tramite un semplice indirizzo IPv4 (IP_VALIDATION_PATTERN, src/settings.ts:6); raggiungerlo come "il dispositivo" richiede una posizione di rete adiacente alla LAN (rogue AP, ARP spoofing o rivendicazione dell'IP), non un accesso remoto arbitrario.AC:H — lo sfruttamento dipende dal fatto che l'attaccante occupi già quella posizione di rete quando il plugin della vittima comunica con directConnectIP:8089; non è un trigger remoto one-shot.UI:R — richiede che la vittima esegua "Sync supernote notes now" (o abbia già abilitato l'interruttore di sincronizzazione automatica) mentre è puntata all'endpoint controllato dall'attaccante.S:C — il componente vulnerabile è un plugin Obsidian con scope sul vault; la scrittura finisce completamente al di fuori del vault, sul filesystem host sottostante, che è uno scope di sicurezza diverso.C:N — questa è una primitiva di sola scrittura; nulla viene letto dall'attaccante.I:H — byte controllati dall'attaccante finiscono in un percorso scelto dall'attaccante con un nome file scelto dall'attaccante. Avvertenza limitante: writeBinaryAt() (src/syncEngine.ts:40-47) prende il ramo create solo per percorsi non già tracciati nel manifest di sincronizzazione del plugin stesso, e Vault.createBinary() di Obsidian rifiuta di sovrascrivere silenziosamente un file che esiste già fisicamente nel percorso risolto (confermato empiricamente — vedi PoC) — quindi si tratta di "piantare un nuovo file ovunque", non di "sovrascrivere qualsiasi file esistente".A:N — nessun impatto sulla disponibilità dimostrato.runDeviceSync() (src/syncEngine.ts:100-196) elenca i file del dispositivo associato tramite scanDeviceSupernoteTree() (src/FileListModal.ts:53-68), che ricorre l'elenco directory HTTP del dispositivo stesso e mantiene qualsiasi voce il cui name corrisponda a /\.(note|spd)$/i (src/FileListModal.ts:46,63). Il campo uri di ciascuna voce — una stringa separata e controllata indipendentemente nello stesso oggetto JSON restituito dal dispositivo — non viene mai validato rispetto a name o a qualsiasi altra cosa.
Quel uri grezzo viene poi passato direttamente a 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 rimuove \ : * ? " < > | ma non rimuove né rifiuta mai i segmenti di percorso ... Un uri del dispositivo pari a /../../PWNED.txt sopravvive intatto e viene unito alla cartella di sincronizzazione configurata (predefinita "Supernote sync") per produrre vaultPath = "Supernote sync/../../PWNED.txt".
syncEngine.ts:128 calcola questo vaultPath dall'uri grezzo dell'elenco, poi ensureFolder()/writeBinaryAt() (syncEngine.ts:146-148, 21-47) lo passano direttamente a app.vault.getAbstractFileByPath() / createBinary() / modifyBinary() senza alcun controllo di traversal. La risoluzione dei percorsi di Obsidian normalizza poi i segmenti .. rispetto alla directory reale del vault su disco, facendo atterrare la scrittura sopra la cartella di sincronizzazione — e, con abbastanza segmenti ../, sopra la radice del vault, sul filesystem host, nello scope di permessi dell'utente OS stesso.
Controllo sui sibling: ogni altro sink di scrittura sul vault in questo codebase (src/main.ts — cattura screen-mirror, importazione PDF/markdown, "attach to note", DownloadListModal in src/FileListModal.ts:245-246) costruisce il proprio percorso di destinazione tramite app.fileManager.getAvailablePathForAttachment(file.name) di Obsidian, che consuma sempre e solo il campo name del dispositivo e non è sfruttabile in questo modo. deviceUriToVaultPath() nella funzionalità di sincronizzazione automatica più recente è l'unico sink che invece costruisce a mano un percorso dal campo uri del dispositivo, ed è quello che ha saltato la sanificazione di .. — un caso limpido di "controllato su ogni sibling tranne questo".
L'interfaccia delle impostazioni del plugin stesso afferma: "The sync command never writes anywhere outside this folder" (src/settings.ts, descrizione della cartella di sincronizzazione) — questo PoC falsifica direttamente tale garanzia.
Confermato dinamicamente end-to-end contro un binario desktop Obsidian 1.13.4 reale e non modificato (Xvfb + fluxbox + xdotool + scrot), eseguendo il main.js effettivamente compilato del plugin — nessun mocking del codice del plugin.
./scripts/build) e caricato in un vault di test pulito (Supernote (Unofficial) v2.9.1, abilitato tramite "Trust author and enable plugins").Supernote IP address a 127.0.0.1, lasciato Sync folder al valore predefinito Supernote sync.127.0.0.1:8089, simulando un dispositivo malevolo/compromesso. Il suo elenco directory restituisce:
{"name":"Quick notes.note","size":61,"date":"2026-07-31 00:00:00",
"uri":"/../../PWNED_BY_DEVICE_SYNC.txt","extension":"note","isDirectory":false}
name supera il filtro sull'estensione .note; uri porta il traversal.) Qualsiasi GET riceve risposta 200 con i byte del file — i client HTTP reali normalizzano i .. fuori dal percorso della richiesta in uscita prima che raggiunga il filo (RFC 3986), il che è irrilevante qui poiché il codice vulnerabile calcola il percorso di scrittura dalla stringa JSON originale, non dall'URL della richiesta normalizzato.PWNED_BY_DEVICE_SYNC.txt, contenente i byte dell'attaccante, è stato creato una directory sopra la radice del vault (sibling di testvault/, completamente fuori dal vault). Il data.json del plugin stesso ha registrato il percorso calcolato alla lettera: "vaultPath": "Supernote sync/../../PWNED_BY_DEVICE_SYNC.txt".Evidenze (catture autentiche, ~/engagements/supernote-obsidian-plugin/evidence/):
01-vault-escape-file-write.png — terminale reale: ls -la testvault/ PWNED_BY_DEVICE_SYNC.txt che mostra il file come sibling della directory del vault, più il suo contenuto controllato dall'attaccante tramite cat.02-plugin-data-json-vaultpath.png — il file di stato di sincronizzazione persistito dal plugin stesso che registra "vaultPath": "Supernote sync/../../PWNED_BY_DEVICE_SYNC.txt".03-plugin-settings-security-claim.png — l'interfaccia delle impostazioni del plugin che afferma che il comando di sincronizzazione "never writes anywhere outside this folder."Un attaccante in grado di rispondere come il dispositivo Supernote configurato della vittima (adiacente alla LAN: rogue AP, ARP spoofing o rivendicazione dell'IP del dispositivo) può far creare al client Obsidian della vittima un nuovo file controllato dall'attaccante in qualsiasi percorso del filesystem in cui il processo desktop possa scrivere — dentro il vault (ad es. un file nuovo di zecca sotto .obsidian/plugins/<new-id>/, ponendo le basi per ulteriori abusi della fiducia nei plugin) o completamente fuori di esso (ad es. ~/.config/autostart/*.desktop, un nuovo drop-in di crontab, o qualsiasi altra posizione in cui un file nuovo — non una sovrascrittura — sia sufficiente per ottenere esecuzione o persistenza). Non può sovrascrivere silenziosamente un file che esiste già nel percorso risolto (il createBinary di Obsidian lancia "File already exists" in quel caso, confermato empiricamente), il che limita la primitiva al planting di file nuovi anziché alla sovrascrittura universale.
uri fornito dal dispositivo determina direttamente la destinazione su disco)In deviceUriToVaultPath() (src/deviceSync.ts:153-161), rifiutare o rimuovere i segmenti di percorso .. (e quelli vuoti/solo .) dopo aver diviso deviceUri, ad es.:
const segments = deviceUri
.split('/')
.filter((s) => s.length > 0 && s !== '.' && s !== '..')
.map((s) => s.replace(INVALID_FILENAME_CHARS, '_'));
Inoltre, scanDeviceSupernoteTree() (src/FileListModal.ts:63) dovrebbe validare che l'uri di una voce dell'elenco sia coerente con il suo name (ad es. uri termina con lo stesso nome file), anziché fidarsi dei due campi indipendentemente — la stessa classe di difesa su cui si fa già implicitamente affidamento ovunque nel codebase, che costruisce percorsi solo da name/basename tramite getAvailablePathForAttachment().
Dostxodjayev Abdullox
Nessun SECURITY.md è presente in questo repository. La segnalazione privata di vulnerabilità di GitHub è confermata abilitata per philips/supernote-obsidian-plugin (gh api repos/philips/supernote-obsidian-plugin/private-vulnerability-reporting --jq .enabled → true; 0 advisory di sicurezza pubblicati in precedenza). Si applica il flusso GHSA standard: https://github.com/philips/supernote-obsidian-plugin/security/advisories/new.