Resumen

Los endpoints /vault/{path} de la API REST local (métodos GET, PUT, PATCH, POST y DELETE) decodifican el path de la petición dentro del propio handler, después de que Express ya ha enrutado y normalizado la URL. A continuación, el path decodificado se entrega al adaptador del vault de Obsidian sin ninguna verificación de confinamiento. Una secuencia literal ../ es resuelta o rechazada en la capa de enrutamiento con un 404, pero %2F no es interpretado como separador en esa capa, por lo que ..%2F..%2F sobrevive el enrutamiento y solo se convierte en / real cuando el handler ejecuta decodeURIComponent, reconstituyendo un path traversal ../ que escapa del vault. Un cliente autenticado puede leer, escribir o eliminar archivos arbitrarios del host con los privilegios del proceso Obsidian.

Detalles técnicos

Framework: Express (import express from "express"; rutas registradas mediante this.api.route("/vault/*")).

Línea vulnerable: en src/requestHandler.ts, todos los handlers del vault (vaultGet, vaultPut, vaultPatch, vaultPost, vaultDelete) derivan el path de la siguiente forma:

typescript
const rawPath = decodeURIComponent(
  req.path.slice(req.path.indexOf("/", 1) + 1),
);

El path se decodifica con decodeURIComponent después del enrutamiento de Express. Una secuencia literal ../ es colapsada o rechazada en la capa de enrutamiento, pero %2F no actúa como separador en esa capa, por lo que ..%2F..%2F llega intacta al handler y esta llamada a decodeURIComponent la convierte en ../../. La cadena que vio el enrutador (...%2F...) no es la cadena que utiliza el handler (.../...), y %2e%2e se comporta de la misma manera.

Sin confinamiento sobre el path decodificado. Los handlers pasan rawPath directamente al adaptador del vault, por ejemplo this.app.vault.adapter.readBinary(filePath) o this.app.vault.getAbstractFileByPath(filePath), sin ninguna verificación mediante path.resolve más prefijo de la raíz del vault. Esto permite que el ../../ reconstituido escape del directorio del vault.

La corrección ya existe en el código, pero solo para MOVE. El handler vaultMove confina correctamente:

typescript
const syntheticRoot = "/vault";
const resolved = posix.resolve(syntheticRoot, normalized);
if (resolved !== syntheticRoot && !resolved.startsWith(syntheticRoot + "/")) {
  this.returnCannedResponse(res, { errorCode: ErrorCode.PathTraversalNotAllowed });
  return;
}

Los métodos GET, PUT, PATCH, POST y DELETE carecen de esta protección. La solución consiste en aplicar la misma verificación posix.resolve(syntheticRoot, ...) más startsWith al path decodificado en cada handler del vault, y rechazar cualquier segmento que se decodifique como ...

Prueba de concepto

Requisito previo: Obsidian en ejecución con el plugin Local REST API habilitado y su clave de API configurada ($API_KEY). Los objetivos siguientes son para Unix; ajustar según el sistema operativo (por ejemplo, ..%2F..%2FWindows%2Fwin.ini en Windows).

bash
# LECTURA fuera del vault (devuelve 200 con los bytes reales del archivo objetivo):
curl --path-as-is -k -H "Authorization: Bearer $API_KEY" \
  "https://127.0.0.1:27124/vault/..%2F..%2F..%2F..%2Fetc%2Fpasswd"

# ESCRITURA fuera del vault (crea un archivo en disco fuera de la raíz del vault):
curl --path-as-is -k -X PUT -H "Authorization: Bearer $API_KEY" --data "pwned" \
  "https://127.0.0.1:27124/vault/..%2F..%2F..%2Ftmp%2Fcanary.txt"

El flag --path-as-is impide que curl colapse .. en el lado del cliente. Una petición con ../ literal sin codificar devuelve 404; solo la forma codificada con %2F o %2e%2e elude el control, lo que confirma la brecha de decodificación posterior al enrutamiento.

Impacto

Lectura, escritura y eliminación arbitraria de archivos fuera del vault de Obsidian, con los privilegios del sistema operativo del proceso Obsidian, que típicamente corresponden al directorio home del usuario (claves SSH, perfiles de navegador, dotfiles, credenciales almacenadas).

El impacto se amplifica en despliegues con agentes MCP o LLM: esta API se utiliza ampliamente como servidor MCP, por lo que una inyección de prompt en el contenido del vault, o un cliente MCP malicioso, puede hacer que un agente emita un path con %2F, convirtiendo la capacidad de "el agente puede editar mis notas" en "el agente puede leer y escribir cualquier archivo del host", sin que el usuario haya tenido intención de conceder acceso al sistema de archivos más allá del vault.

Esta vulnerabilidad fue reportada por Caleb Brisbin mediante divulgación responsable.