PoC — ejecución arbitraria de JavaScript controlada por frontmatter en Note Toolbar para Obsidian (GHSA-q8cw-3m8c-5pf2, CVE-2026-87002, CVSS 7.0).
{{prop_NAME}} → {{js:}}Estado del CVE: solicitado, pendiente de asignación. Este hallazgo se publica como GHSA-q8cw-3m8c-5pf2. Tras la asignación del CVE, este repositorio se renombra a
CVE-YYYY-NNNNN-obsidian-note-toolbar-PoCy este banner se reemplaza con el enlace al CVE.
| Investigador | Dostxodjayev Abdullox (@squeeze440) |
| Aviso | GHSA-q8cw-3m8c-5pf2 |
| CVSS 3.1 | 7.0 (Alto) |
| Debilidad | CWE-94, CWE-1336 |
{{prop_NAME}} → {{js:}}La neutralización de salida inadecuada en el motor de sustitución de variables de Note Toolbar (plugin de Obsidian) permite a un atacante que controla el frontmatter YAML de una nota (por ejemplo, un colaborador en una bóveda sincronizada/compartida) lograr la ejecución de código arbitrario en la máquina de la víctima cuando esta simplemente abre la nota, mediante la inyección de una carga útil {{js: ...}} en una propiedad del frontmatter a la que hace referencia la etiqueta/tooltip/enlace de un elemento de barra de herramientas preexistente y no script mediante {{prop_NAME}}.
Note Toolbar (note-toolbar) — plugin de la comunidad de Obsidian
Repositorio: https://github.com/chrisgurney/obsidian-note-toolbar
v1.34.12 (commit 520271c3027eb6da38bab10a686b21e14c664c13, 2026-07-29), contra Obsidian desktop 1.13.4 en Linux.
7.0 (Alto) — CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H
AV:L — el componente vulnerable (el evaluador JS del plugin, ejecutándose dentro del renderizador Electron de Obsidian) no está expuesto a la red; la carga útil llega como contenido local de bóveda/archivo sincronizado por cualquier medio que use la víctima (Git, Syncthing, Obsidian Sync, carpeta compartida), en consonancia con hallazgos previos de esta familia (por ejemplo, obsidian-syncthing-integration, obsidian-codescript-toolkit).AC:H — la explotación depende de condiciones fuera del control del atacante que ya deben existir en la instalación de la víctima: (1) la configuración de "Scripting" del plugin debe estar ya habilitada (desactivada por defecto), y (2) la víctima debe haber configurado ya un elemento de barra de herramientas cuya etiqueta, tooltip o enlace sea una referencia {{prop_NAME}} simple a alguna clave del frontmatter. Ambas son pautas de uso realistas y documentadas, pero ninguna es la predeterminada.PR:N — el atacante no necesita acceso al sistema de la víctima, solo la capacidad de introducir una nota en la bóveda de la víctima con un frontmatter controlado por el atacante.UI:R — la víctima debe abrir/ver la nota específica; no se requiere hacer clic en el elemento de la barra de herramientas, la barra de herramientas se renderiza automáticamente.S:U, C:H/I:H/A:H — el código se ejecuta con acceso completo a Node.js (require('child_process'), sistema de archivos, etc.) como la cuenta de usuario local que ejecuta Obsidian.Note Toolbar admite una variable documentada {{prop_NAME}} que sustituye el valor del frontmatter de una nota en la etiqueta, tooltip o enlace de un elemento de la barra de herramientas (skills/note-toolbar-variables/SKILL.md, wiki Variables.md). También admite una variable {{js: <expr>}} que evalúa la expresión como JavaScript en vivo cuando la configuración "Scripting" del plugin está habilitada.
El error está en el orden de las operaciones en src/Toolbar/VariableResolver.ts::replaceVars():
{{prop_KEY}} se sustituyen con el valor sin procesar de frontmatter[KEY] de la nota actual — incondicionalmente, independientemente de la configuración scriptingEnabled, y sin importar la fuente de ese valor del frontmatter.if (this.ntb.settings.scriptingEnabled)): la función luego comprueba s.trim().startsWith('{{js:') en la cadena s ya sustituida, y si es verdadero, elimina el envoltorio {{js:/}} y pasa el resto a JavaScriptAdapter.use() para su evaluación.Debido a que el paso 1 se ejecuta antes del paso 2 y reescribe s en su lugar, un valor del frontmatter que a su vez comienza con {{js: y termina con }} pasa de ser "texto mostrado" a "script ejecutado" — aunque el elemento de la barra de herramientas nunca fue creado como un elemento de script. El propietario del elemento de la barra de herramientas solo escribió {{prop_status}}; la carga útil ejecutable proviene enteramente del propio frontmatter de la nota.
JavaScriptAdapter.evaluate() (src/Adapters/JavaScriptAdapter.ts:143-205) ejecuta la expresión con un AsyncFunction real y sin sandbox:
src/Adapters/Adapter.ts:11
protected static readonly AsyncFunction = (Object.getPrototypeOf(async function(){}) as { constructor: typeof Function }).constructor;
src/Adapters/JavaScriptAdapter.ts:164
const func = new JavaScriptAdapter.AsyncFunction("input", expression);
...
result = await Promise.resolve((func as (...args: unknown[]) => unknown)(args));
Este es el constructor AsyncFunction genuino del motor JS, no un intérprete/sandbox — dentro del renderizador Electron de Obsidian esto tiene acceso completo a require()/Node.js, confirmado en el PoC a continuación mediante require('child_process').execSync(...).
De manera crítica, el adaptador JS es integrado y no requiere ningún plugin complementario (src/Adapters/AdapterManager.ts:59: adapter = this.js; // built-in, doesn't rely on plugin), a diferencia de los prefijos de variables de Dataview/Templater/JS-Engine que necesitan esos plugins instalados. Solo lo controla el propio interruptor "Scripting" del plugin (scriptingEnabled, por defecto false, src/Settings/NoteToolbarSettings.ts:300).
Finalmente, la ruta de renderizado que desencadena la sustitución es automática, no está condicionada a un clic: ToolbarRenderer.ts::renderLItems() llama a this.ntb.vars.resolveText(toolbar, file) (src/Toolbar/ToolbarRenderer.ts:418) cada vez que se renderiza una barra de herramientas para una nota — es decir, al abrir/ver la nota — lo que resuelve la etiqueta y el tooltip de cada elemento mediante replaceVars(). No se requiere hacer clic en el elemento de la barra de herramientas afectado.
La asignación de barra de herramientas a nota está a su vez dirigida por frontmatter (configuración toolbarProp, clave por defecto notetoolbar, src/Settings/NoteToolbarSettings.ts:317), por lo que una nota sincronizada puede seleccionar qué barra de herramientas se renderiza en ella — pero esto no es necesario para la explotación si la víctima ya usa una barra de herramientas predeterminada o un mapeo de carpeta que incluya el elemento vulnerable.