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.
Vault PoCVault/ mit Project Alpha.md:
---
status: "{{js: require('child_process').execSync('id > /tmp/ntb_poc_proof.txt; date >> /tmp/ntb_poc_proof.txt; whoami >> /tmp/ntb_poc_proof.txt'); return 'ok';}}"
notetoolbar: Toolbar
---
Note Toolbar-Plugin installiert, Community-Plugins als vertrauenswürdig eingestuft, eine Toolbar namens „Toolbar" über die eigene Einstellungs-UI des Plugins erstellt mit einem einzigen Element, dessen Label-Feld {{prop_status}} ist (ein einfaches Eigenschaftsanzeige-Label — nie als Skript-Element konfiguriert).
Plugin-Einstellung Scripting aktiviert (Opt-in des Opfers, standardmäßig deaktiviert).
Einfaches Öffnen/Ansehen von Project Alpha.md — kein Klick auf das Toolbar-Element — rendert die Toolbar mit Anzeige von ok (der Rückgabewert des JS-Ausdrucks), und die eingeschleusten Shell-Befehle werden tatsächlich ausgeführt:
Terminal-Beweis (~/engagements/obsidian-note-toolbar/evidence/02_terminal_proof_of_execution.png):
$ cat /tmp/ntb_poc_proof.txt
uid=1000(kali) gid=1000(kali) groups=1000(kali),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),105(docker),992(kvm)
Fri Jul 31 09:31:33 PM +05 2026
kali
Obsidian-Screenshot, der die rohe Frontmatter-Nutzlast und die gerenderte ok-Toolbar zeigt (~/engagements/obsidian-note-toolbar/evidence/01_obsidian_toolbar_rce_trigger.png).
Quellnotiz erhalten unter ~/engagements/obsidian-note-toolbar/evidence/poc_note_frontmatter.md.
Vollständige beliebige Codeausführung als der lokale Benutzer, der Obsidian ausführt, ausgelöst durch bloßes Öffnen einer Notiz — kein Klick, keine explizite „Skript ausführen"-Aktion, kein Dialog. In einem Team-/Shared-Vault-Workflow (Obsidian Sync, Git-gestützter Vault, Syncthing, Google Drive usw.) kann jeder Mitwirkende, der das Frontmatter einer Notiz bearbeiten kann, RCE auf jedem anderen Mitarbeiter erreichen, der die Scripting-Einstellung von Note Toolbar aktiviert hat und ein Toolbar-Element besitzt, das eine Notizeigenschaft anzeigt.
{{...}}-Template-/Variablensyntax neutralisiert keine angreiferkontrollierten Ersetzungswerte, die selbst weitere Template-Direktiven enthalten.In VariableResolver.replaceVars() sollten die Skripting-Präfix-Prüfungen ({{js:, {{dv:, {{jse:, <%/{{tp:) gegen die ursprüngliche, vor der Ersetzung stehende Zeichenkette ausgewertet werden, nicht gegen die Zeichenkette nach der {{prop_*}}/{{note_title}}/etc.-Ersetzung — d. h. im Voraus bestimmen, ob das Toolbar-Element selbst als Skript-Direktive verfasst wurde, und niemals zulassen, dass ein ersetzter Frontmatter-Wert als eine solche neu interpretiert wird. Konkret: die Skripting-Präfix-Prüfung im hasVars-Stil vor dem PROP/SELECTION/NOTE_TITLE/etc.-Ersetzungsblock durchführen und das erneute Prüfen von startsWith('{{js:'/'{{dv:'/...) auf der Zeichenkette nach der Ersetzung überspringen. Als Defense in Depth könnten aus dem Frontmatter stammende Werte außerdem escaped/neutralisiert werden, sodass sie niemals wieder in die {{...}}-Grammatik eintreten können.
Dostxodjayev Abdullox
Die SECURITY.md dieses Repositorys bestätigt, dass die private Schwachstellenmeldung über GitHub aktiviert ist und der bevorzugte Kanal ist („GitHub private vulnerability reporting: use the 'Report a vulnerability' button under the Security tab of this repo"), mit einem Google-Formular als Fallback für Melder ohne GitHub-Konto. Unabhängig erneut verifiziert: gh api repos/chrisgurney/obsidian-note-toolbar/private-vulnerability-reporting --jq .enabled → true, und gh api repos/chrisgurney/obsidian-note-toolbar/security-advisories --jq 'length' → 0 (keine früheren Advisories, kein Duplikatrisiko).