PoC — exécution arbitraire de JavaScript pilotée par le frontmatter dans Note Toolbar pour Obsidian (GHSA-q8cw-3m8c-5pf2, CVE-2026-87002, CVSS 7.0).
{{prop_NAME}} → {{js:}}Statut CVE : demandé, en attente d'attribution. Cette découverte est publiée sous GHSA-q8cw-3m8c-5pf2. À l'attribution du CVE, ce dépôt sera renommé
CVE-YYYY-NNNNN-obsidian-note-toolbar-PoCet cette bannière sera remplacée par le lien CVE.
| Chercheur | Dostxodjayev Abdullox (@squeeze440) |
| Avis | GHSA-q8cw-3m8c-5pf2 |
| CVSS 3.1 | 7.0 (Élevé) |
| Faiblesse | CWE-94, CWE-1336 |
{{prop_NAME}} → {{js:}}Une neutralisation incorrecte de la sortie dans le moteur de substitution de variables de Note Toolbar (plugin Obsidian) permet à un attaquant qui contrôle le frontmatter YAML d'une note (par exemple un collaborateur sur un coffre synchronisé/partagé) d'obtenir une exécution de code arbitraire sur la machine de la victime lorsque celle-ci ouvre simplement la note, en injectant une charge utile {{js: ...}} dans une propriété du frontmatter qu'un élément de barre d'outils préexistant, non-script, référence via {{prop_NAME}} dans son libellé/infobulle/lien.
Note Toolbar (note-toolbar) — plugin communautaire Obsidian
Dépôt : https://github.com/chrisgurney/obsidian-note-toolbar
v1.34.12 (commit 520271c3027eb6da38bab10a686b21e14c664c13, 2026-07-29), contre Obsidian desktop 1.13.4 sous Linux.
7.0 (Élevé) — CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H
AV:L — le composant vulnérable (l'évaluateur JS du plugin, s'exécutant dans le renderer Electron d'Obsidian) n'est pas exposé au réseau ; la charge utile arrive sous forme de contenu local de coffre/fichier synchronisé par tout moyen utilisé par la victime (Git, Syncthing, Obsidian Sync, dossier partagé), ce qui est cohérent avec les découvertes antérieures de cette famille (par ex. obsidian-syncthing-integration, obsidian-codescript-toolkit).AC:H — l'exploitation dépend de conditions hors du contrôle de l'attaquant qui doivent déjà exister sur l'installation de la victime : (1) le réglage « Scripting » du plugin doit déjà être activé (désactivé par défaut), et (2) la victime doit déjà avoir configuré un élément de barre d'outils dont le libellé, l'infobulle ou le lien est une référence nue {{prop_NAME}} vers une clé de frontmatter. Les deux sont des schémas d'utilisation réalistes et documentés, mais aucun n'est la configuration par défaut.PR:N — l'attaquant n'a besoin d'aucun accès au système de la victime, seulement de la capacité de faire entrer une note dans le coffre de la victime avec un frontmatter contrôlé par l'attaquant.UI:R — la victime doit ouvrir/visualiser la note spécifique ; aucun clic sur l'élément de barre d'outils n'est nécessaire, la barre d'outils se rend automatiquement.S:U, C:H/I:H/A:H — le code s'exécute avec un accès Node.js complet (require('child_process'), système de fichiers, etc.) en tant que compte utilisateur local exécutant Obsidian.Note Toolbar prend en charge une variable documentée {{prop_NAME}} qui substitue la valeur du frontmatter d'une note dans le libellé, l'infobulle ou le lien d'un élément de barre d'outils (skills/note-toolbar-variables/SKILL.md, wiki Variables.md). Il prend également en charge une variable {{js: <expr>}} qui évalue l'expression comme du JavaScript vivant lorsque le réglage « Scripting » du plugin est activé.
Le bug réside dans l'ordre des opérations dans src/Toolbar/VariableResolver.ts::replaceVars() :
{{prop_KEY}} sont substitués par la valeur brute de frontmatter[KEY] de la note courante — inconditionnellement, indépendamment du réglage scriptingEnabled, et indépendamment de la source de cette valeur de frontmatter.if (this.ntb.settings.scriptingEnabled)) : la fonction vérifie ensuite s.trim().startsWith('{{js:') sur la chaîne déjà substituée s, et si c'est vrai, retire l'enveloppe {{js:/}} et passe le reste à JavaScriptAdapter.use() pour évaluation.Parce que l'étape 1 s'exécute avant l'étape 2 et réécrit s sur place, une valeur de frontmatter qui commence elle-même par {{js: et se termine par }} est promue de « texte affiché » à « script exécuté » — même si l'élément de barre d'outils n'a jamais été conçu comme un élément script. Le propriétaire de l'élément de barre d'outils n'a jamais tapé que {{prop_status}} ; la charge utile exécutable provient entièrement du frontmatter de la note elle-même.
JavaScriptAdapter.evaluate() (src/Adapters/JavaScriptAdapter.ts:143-205) exécute l'expression avec un véritable AsyncFunction non 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));
Il s'agit du véritable constructeur AsyncFunction du moteur JS, et non d'un interpréteur/sandbox — à l'intérieur du renderer Electron d'Obsidian, il dispose d'un accès complet à require()/Node.js, confirmé dans le PoC ci-dessous via require('child_process').execSync(...).
Point crucial : l'adaptateur JS est intégré et ne nécessite aucun plugin compagnon (src/Adapters/AdapterManager.ts:59 : adapter = this.js; // built-in, doesn't rely on plugin), contrairement aux préfixes de variables Dataview/Templater/JS-Engine qui nécessitent l'installation de ces plugins. Seul le propre commutateur « Scripting » du plugin (scriptingEnabled, par défaut false, src/Settings/NoteToolbarSettings.ts:300) le conditionne.
Enfin, le chemin de rendu qui déclenche la substitution est automatique, non conditionné à un clic : ToolbarRenderer.ts::renderLItems() appelle this.ntb.vars.resolveText(toolbar, file) (src/Toolbar/ToolbarRenderer.ts:418) à chaque rendu d'une barre d'outils pour une note — c'est-à-dire à l'ouverture/visualisation de la note — ce qui résout le libellé et l'infobulle de chaque élément via replaceVars(). Aucun clic sur l'élément de barre d'outils affecté n'est nécessaire.
L'affectation barre d'outils-vers-note est elle-même pilotée par le frontmatter (réglage toolbarProp, clé par défaut notetoolbar, src/Settings/NoteToolbarSettings.ts:317), de sorte qu'une note synchronisée peut sélectionner la barre d'outils qui s'y affiche — mais cela n'est pas nécessaire à l'exploitation si la victime utilise déjà une barre d'outils par défaut ou un mappage de dossier incluant l'élément vulnérable.
Ceci est distinct du risque documenté par le SECURITY.md du mainteneur lui-même (« User scripts... executes JavaScript provided by the user... intentional and by design ») — cela décrit un utilisateur qui conçoit sciemment un élément de barre d'outils {{js:}}, ou qui importe sciemment une configuration de barre d'outils partagée en contenant un. Ici, l'élément de barre d'outils n'est pas du tout un élément script du point de vue du configurateur (c'est un simple libellé d'affichage de propriété), et la charge utile exécutable arrive via le contenu/frontmatter ordinaire d'une note — le même modèle de menace de contenu de coffre non fiable que les autres découvertes d'exécution de code via synchronisation/configuration Obsidian (obsidian-syncthing-integration, obsidian-codescript-toolkit).