
PoC — path traversal mediante sincronización maliciosa de dispositivos en el plugin Supernote Obsidian (GHSA-3gx3-r874-5pp4, CVE-2026-86999, CVSS 5.6).
Estado del CVE: solicitado, pendiente de asignación. Este hallazgo se publica como GHSA-3gx3-r874-5pp4. Al asignarse el CVE, este repositorio se renombra
CVE-YYYY-NNNNN-supernote-obsidian-plugin-PoCy este banner se reemplaza con el enlace del CVE.
| Investigador | Dostxodjayev Abdullox (@squeeze440) |
| Aviso | GHSA-3gx3-r874-5pp4 |
| CVSS 3.1 | 5.6 (Medio) |
| Debilidad | CWE-22, CWE-73 |
El Path Traversal en la función de sincronización automática del dispositivo del plugin de Obsidian Supernote (Unofficial) (philips/supernote-obsidian-plugin) v2.9.1 permite que un dispositivo "Supernote" malicioso o no autorizado (o un atacante en la ruta/en la LAN que suplante la IP del dispositivo emparejado) haga que el cliente Obsidian de la víctima escriba un archivo nuevo arbitrario en cualquier lugar donde el proceso de escritorio pueda escribir —incluyendo fuera de la carpeta de sincronización configurada y fuera del propio vault— mediante un campo uri manipulado en la respuesta de listado de directorios del dispositivo.
philips/supernote-obsidian-plugin ("Supernote (Unofficial)"), un plugin de la comunidad de Obsidian.md que sincroniza las notas de un dispositivo físico de tinta electrónica Supernote dentro de un vault a través del servidor HTTP local "Browse and Access" del dispositivo (http://<device-ip>:8089, sin autenticación, por diseño de la función del dispositivo).
48db5bf4dcfb100632c84699c830c1309da1abb9supernote-typescript: commit 195415b3a1f74147... (no implicado en sí mismo — el fallo está enteramente en el propio código de planificación de sincronización del plugin)5.6 Medio — CVSS:3.1/AV:A/AC:H/PR:N/UI:R/S:C/C:N/I:H/A:N
AV:A — el servidor HTTP del dispositivo es texto plano, sin autenticación, y se configura mediante una dirección IPv4 simple (IP_VALIDATION_PATTERN, src/settings.ts:6); alcanzarlo como "el dispositivo" requiere una posición de red adyacente a la LAN (AP rogue, ARP spoofing, o reclamar la IP), no acceso remoto arbitrario.AC:H — la explotación depende de que el atacante ya ocupe esa posición de red cuando el plugin de la víctima se comunica con directConnectIP:8089; no es un disparador remoto de un solo intento.UI:R — requiere que la víctima ejecute "Sync supernote notes now" (o que tenga ya habilitado el interruptor de sincronización automática) mientras apunta al endpoint controlado por el atacante.S:C — el componente vulnerable es un plugin de Obsidian con alcance de vault; la escritura aterriza completamente fuera del vault, en el sistema de archivos del host subyacente, que es un ámbito de seguridad diferente.C:N — esto es una primitiva de solo escritura; nada se lee de vuelta hacia el atacante.I:H — bytes controlados por el atacante aterrizan en una ruta elegida por el atacante con un nombre de archivo elegido por el atacante. Advertencia acotada: writeBinaryAt() (src/syncEngine.ts:40-47) solo toma la rama de creación para rutas que aún no están registradas en el propio manifiesto de sincronización del plugin, y el propio Vault.createBinary() de Obsidian se niega a sobrescribir silenciosamente un archivo que ya existe físicamente en la ruta resuelta (confirmado empíricamente — véase PoC) — por lo que esto es "plantar un archivo nuevo en cualquier lugar", no "sobrescribir cualquier archivo existente".A:N — no se demostró impacto en la disponibilidad.runDeviceSync() (src/syncEngine.ts:100-196) lista los archivos del dispositivo emparejado mediante scanDeviceSupernoteTree() (src/FileListModal.ts:53-68), que recorre recursivamente el propio listado de directorios HTTP del dispositivo y conserva cualquier entrada cuyo name coincida con /\.(note|spd)$/i (src/FileListModal.ts:46,63). El campo uri de cada entrada —una cadena separada e independientemente controlada en el mismo objeto JSON devuelto por el dispositivo— nunca se valida contra name ni contra nada más.
Ese uri sin procesar se introduce directamente en 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 elimina \ : * ? " < > | pero nunca elimina ni rechaza segmentos de ruta ... Un uri de dispositivo de /../../PWNED.txt sobrevive intacto y se une a la carpeta de sincronización configurada (por defecto "Supernote sync") para producir vaultPath = "Supernote sync/../../PWNED.txt".
syncEngine.ts:128 calcula este vaultPath a partir del uri sin procesar del listado, y luego ensureFolder()/writeBinaryAt() (syncEngine.ts:146-148, 21-47) lo pasan directamente a app.vault.getAbstractFileByPath() / createBinary() / modifyBinary() sin ninguna comprobación de traversal. La propia resolución de rutas de Obsidian normaliza entonces los segmentos .. contra el directorio real del vault en disco, haciendo que la escritura aterrice por encima de la carpeta de sincronización —y, con suficientes segmentos ../, por encima de la raíz del vault por completo, en el sistema de archivos del host, dentro del propio ámbito de permisos del usuario del SO.
Comprobación de hermanos: cualquier otro sumidero de escritura en el vault en este código base (src/main.ts — captura de espejo de pantalla, importación de PDF/markdown, "attach to note", DownloadListModal en src/FileListModal.ts:245-246) construye su ruta de destino mediante el propio app.fileManager.getAvailablePathForAttachment(file.name) de Obsidian, que solo consume el campo name del dispositivo y no es explotable de esta manera. deviceUriToVaultPath() en la función de sincronización automática más reciente es el único sumidero que, en su lugar, construye a mano una ruta a partir del campo uri del dispositivo, y es el que omitió el saneamiento de .. — un caso claro de "comprobado en todos los hermanos menos en este".
La propia interfaz de ajustes del plugin afirma: "El comando de sincronización nunca escribe en ningún lugar fuera de esta carpeta" (src/settings.ts, descripción de Sync folder) — este PoC falsifica directamente esa garantía.
Confirmado dinámicamente de extremo a extremo contra un binario de escritorio real y sin modificar de Obsidian 1.13.4 (Xvfb + fluxbox + xdotool + scrot), ejecutando el main.js compilado real del plugin — sin simulación del código del plugin.