PoC — frontmatter-gesteuerte beliebige JavaScript-Ausführung in Note Toolbar für Obsidian (GHSA-q8cw-3m8c-5pf2, CVE-2026-87002, CVSS 7.0).
{{prop_NAME}} → {{js:}}CVE-Status: angefragt, Zuweisung ausstehend. Dieser Fund wurde als GHSA-q8cw-3m8c-5pf2 veröffentlicht. Bei CVE-Zuweisung wird dieses Repository in
CVE-YYYY-NNNNN-obsidian-note-toolbar-PoCumbenannt und dieser Banner durch den CVE-Link ersetzt.
| Researcher | Dostxodjayev Abdullox (@squeeze440) |
| Advisory | GHSA-q8cw-3m8c-5pf2 |
| CVSS 3.1 | 7.0 (High) |
| Weakness | CWE-94, CWE-1336 |
{{prop_NAME}} → {{js:}}Unzureichende Ausgabe-Neutralisierung in der Variablen-Ersetzungs-Engine von Note Toolbar (Obsidian-Plugin) ermöglicht einem Angreifer, der das YAML-Frontmatter einer Notiz kontrolliert (z. B. ein Mitarbeiter in einem synchronisierten/geteilten Vault), beliebige Codeausführung auf dem Rechner des Opfers zu erreichen, wenn das Opfer die Notiz lediglich öffnet, indem er eine {{js: ...}}-Nutzlast in eine Frontmatter-Eigenschaft einschleust, auf die ein bereits vorhandenes, nicht als Skript konfiguriertes Toolbar-Element-Label/Tooltip/Link über {{prop_NAME}} verweist.
Note Toolbar (note-toolbar) — Obsidian-Community-Plugin
Repository: https://github.com/chrisgurney/obsidian-note-toolbar
v1.34.12 (Commit 520271c3027eb6da38bab10a686b21e14c664c13, 2026-07-29), gegen Obsidian Desktop 1.13.4 unter Linux.
7.0 (High) — CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H
AV:L — die verwundbare Komponente (der JS-Evaluator des Plugins, der innerhalb des Electron-Renderers von Obsidian läuft) ist nicht netzwerkexponiert; die Nutzlast gelangt als lokaler Vault-/Dateiinhalt hinein, der durch das vom Opfer verwendete Mittel synchronisiert wird (Git, Syncthing, Obsidian Sync, geteilter Ordner), konsistent mit früheren Funden in dieser Familie (z. B. obsidian-syncthing-integration, obsidian-codescript-toolkit).AC:H — die Ausnutzung hängt von Bedingungen außerhalb der Kontrolle des Angreifers ab, die bereits auf der Installation des Opfers vorhanden sein müssen: (1) die „Scripting"-Einstellung des Plugins muss bereits aktiviert sein (standardmäßig deaktiviert), und (2) das Opfer muss bereits ein Toolbar-Element konfiguriert haben, dessen Label, Tooltip oder Link eine bloße {{prop_NAME}}-Referenz auf einen Frontmatter-Schlüssel ist. Beides sind realistische, dokumentierte Nutzungsmuster, aber keines ist die Standardeinstellung.PR:N — der Angreifer benötigt keinen Zugriff auf das System des Opfers, nur die Möglichkeit, eine Notiz mit angreiferkontrolliertem Frontmatter in den Vault des Opfers zu bringen.UI:R — das Opfer muss die spezifische Notiz öffnen/ansehen; kein Klick auf das Toolbar-Element ist erforderlich, die Toolbar wird automatisch gerendert.S:U, C:H/I:H/A:H — Code wird mit vollem Node.js-Zugriff (require('child_process'), Dateisystem usw.) unter dem lokalen Benutzerkonto ausgeführt, das Obsidian ausführt.Note Toolbar unterstützt eine dokumentierte {{prop_NAME}}-Variable, die den Frontmatter-Wert einer Notiz in das Label, den Tooltip oder den Link eines Toolbar-Elements einsetzt (skills/note-toolbar-variables/SKILL.md, Wiki Variables.md). Es unterstützt außerdem eine {{js: <expr>}}-Variable, die den Ausdruck als Live-JavaScript auswertet, wenn die Plugin-Einstellung „Scripting" aktiviert ist.
Der Fehler liegt in der Reihenfolge der Operationen in src/Toolbar/VariableResolver.ts::replaceVars():
{{prop_KEY}}-Platzhalter werden durch den Rohwert von frontmatter[KEY] aus der aktuellen Notiz ersetzt — bedingungslos, unabhängig von der Einstellung scriptingEnabled und unabhängig von der Quelle dieses Frontmatter-Werts.if (this.ntb.settings.scriptingEnabled)): Die Funktion prüft dann s.trim().startsWith('{{js:') auf der bereits ersetzten Zeichenkette s, und falls wahr, entfernt sie den {{js:/}}-Wrapper und übergibt den Rest an JavaScriptAdapter.use() zur Auswertung.Da Schritt 1 vor Schritt 2 läuft und s in-place umschreibt, wird ein Frontmatter-Wert, der selbst mit {{js: beginnt und mit }} endet, von „angezeigtem Text" zu „ausgeführtem Skript" befördert — obwohl das Toolbar-Element nie als Skript-Element verfasst wurde. Der Besitzer des Toolbar-Elements hat nur {{prop_status}} eingegeben; die ausführbare Nutzlast stammt vollständig aus dem Frontmatter der Notiz selbst.
JavaScriptAdapter.evaluate() (src/Adapters/JavaScriptAdapter.ts:143-205) führt den Ausdruck mit einer echten, nicht gesandboxten AsyncFunction aus:
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));
Dies ist der echte AsyncFunction-Konstruktor der JS-Engine, kein Interpreter/Sandbox — innerhalb des Electron-Renderers von Obsidian hat dies vollen require()/Node.js-Zugriff, bestätigt im PoC unten über require('child_process').execSync(...).
Entscheidend ist, dass der JS-Adapter eingebaut ist und kein Begleit-Plugin erfordert (src/Adapters/AdapterManager.ts:59: adapter = this.js; // built-in, doesn't rely on plugin), im Gegensatz zu den Dataview/Templater/JS-Engine-Variablenpräfixen, die die Installation dieser Plugins erfordern. Nur der eigene „Scripting"-Schalter des Plugins (scriptingEnabled, Standard false, src/Settings/NoteToolbarSettings.ts:300) steuert dies.
Schließlich ist der Render-Pfad, der die Ersetzung auslöst, automatisch und nicht klickgesteuert: ToolbarRenderer.ts::renderLItems() ruft this.ntb.vars.resolveText(toolbar, file) (src/Toolbar/ToolbarRenderer.ts:418) jedes Mal auf, wenn eine Toolbar für eine Notiz gerendert wird — d. h. beim Öffnen/Ansehen der Notiz —, was das Label und den Tooltip jedes Elements über replaceVars() auflöst. Kein Klick auf das betroffene Toolbar-Element ist erforderlich.
Die Zuordnung von Toolbar zu Notiz ist selbst frontmatter-gesteuert (Einstellung toolbarProp, Standardschlüssel notetoolbar, src/Settings/NoteToolbarSettings.ts:317), sodass eine synchronisierte Notiz auswählen kann, welche Toolbar auf ihr gerendert wird — dies ist jedoch nicht erforderlich für die Ausnutzung, wenn das Opfer bereits eine Standard-Toolbar oder Ordnerzuordnung verwendet, die das verwundbare Element enthält.
Dies unterscheidet sich von dem Risiko, das die SECURITY.md des Maintainers selbst dokumentiert („User scripts... executes JavaScript provided by the user... intentional and by design") — das beschreibt einen Benutzer, der wissentlich ein {{js:}}-Toolbar-Element verfasst oder wissentlich eine geteilte Toolbar-Konfiguration importiert, die eines enthält. Hier ist das Toolbar-Element aus Sicht des Konfigurierers überhaupt kein Skript-Element (es ist ein einfaches Eigenschaftsanzeige-Label), und die ausführbare Nutzlast gelangt über gewöhnlichen Notizinhalt/-frontmatter — dasselbe Bedrohungsmodell für nicht vertrauenswürdigen Vault-Inhalt wie bei anderen Obsidian-Sync-/Konfigurations-Codeausführungs-Funden (obsidian-syncthing-integration, obsidian-codescript-toolkit).
Dynamisch verifiziert gegen eine echte Obsidian 1.13.4-Desktop-Instanz (Xvfb + fluxbox), wobei das tatsächliche gebaute Plugin (main.js, gebaut aus dem auditierten Quellcode) geladen wurde, nicht eine bloße Funktions-Extraktion.