
PoC — Obsidian用Note Toolbarにおけるfrontmatter駆動の任意JavaScript実行(GHSA-q8cw-3m8c-5pf2、CVE-2026-87002、CVSS 7.0)。
{{prop_NAME}} → {{js:}}変数チェーンを介したFrontmatter駆動の任意コード実行CVEステータス: 申請済み、割り当て待ち。この脆弱性は GHSA-q8cw-3m8c-5pf2として公開されています。CVEが割り当てられた時点で、このリポジトリは
CVE-YYYY-NNNNN-obsidian-note-toolbar-PoCに改名され、このバナーはCVEリンクに置き換えられます。
| 研究者 | Dostxodjayev Abdullox (@squeeze440) |
| アドバイザリ | GHSA-q8cw-3m8c-5pf2 |
| CVSS 3.1 | 7.0 (High) |
| 脆弱性 | CWE-94, CWE-1336 |
{{prop_NAME}} → {{js:}}変数チェーンを介したFrontmatter駆動の任意コード実行Note Toolbar(Obsidianプラグイン)の変数置換エンジンにおける出力の不適切な中和により、ノートのYAML frontmatterを制御できる攻撃者(例: 同期/共有Vaultの共同編集者)が、既存の非スクリプトのツールバー項目のラベル/ツールチップ/リンクが{{prop_NAME}}を介して参照しているfrontmatterプロパティに{{js: ...}}ペイロードを注入することで、被害者が単にノートを開くだけで被害者のマシン上で任意コード実行を達成できます。
Note Toolbar (note-toolbar) — Obsidianコミュニティプラグイン
リポジトリ: https://github.com/chrisgurney/obsidian-note-toolbar
v1.34.12 (コミット520271c3027eb6da38bab10a686b21e14c664c13、2026-07-29)、Linux上のObsidianデスクトップ1.13.4に対して。
7.0 (High) — CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H
AV:L — 脆弱なコンポーネント(ObsidianのElectronレンダラー内で動作するプラグインのJS評価器)はネットワークに面していません。ペイロードは、被害者が使用する何らかの手段(Git、Syncthing、Obsidian Sync、共有フォルダ)で同期されたローカルVault/ファイルの内容として到着します。これはこの系統の先行する知見(例: obsidian-syncthing-integration、obsidian-codescript-toolkit)と一致します。AC:H — 悪用は、被害者のインストールに既に存在していなければならない攻撃者の制御外の条件に依存します: (1) プラグインの「Scripting」設定が既に有効になっていること(デフォルトではオフ)、(2) 被害者が既に、ラベル、ツールチップ、またはリンクが何らかのfrontmatterキーへの単なる{{prop_NAME}}参照であるツールバー項目を設定していること。どちらも現実的で文書化された使用パターンですが、どちらもデフォルトではありません。PR:N — 攻撃者は被害者のシステムへのアクセスを必要とせず、攻撃者が制御するfrontmatterを持つノートを被害者のVaultに入れる能力だけを必要とします。UI:R — 被害者は特定のノートを開く/表示する必要があります。ツールバー項目をクリックする必要はなく、ツールバーは自動的にレンダリングされます。S:U、C:H/I:H/A:H — コードはObsidianを実行しているローカルユーザーアカウントとして、完全なNode.jsアクセス(require('child_process')、ファイルシステムなど)で実行されます。Note Toolbarは、ノートのfrontmatter値をツールバー項目のラベル、ツールチップ、またはリンクに代入する、文書化された{{prop_NAME}}変数をサポートしています(skills/note-toolbar-variables/SKILL.md、wiki Variables.md)。また、「Scripting」プラグイン設定が有効な場合に式をライブJavaScriptとして評価する{{js: <expr>}}変数もサポートしています。
バグはsrc/Toolbar/VariableResolver.ts::replaceVars()の操作順序にあります:
{{prop_KEY}}プレースホルダーは、現在のノートのfrontmatter[KEY]の生の値で代入されます — scriptingEnabled設定に関係なく、またそのfrontmatter値のソースに関係なく、無条件に。if (this.ntb.settings.scriptingEnabled)内): 関数は次に、既に代入された文字列sに対してs.trim().startsWith('{{js:')をチェックし、真であれば{{js:/}}ラッパーを剥がして残りをJavaScriptAdapter.use()に渡して評価します。ステップ1がステップ2より先に実行され、sをその場で書き換えるため、それ自体が{{js:で始まり}}で終わるfrontmatter値は、「表示テキスト」から「実行スクリプト」に昇格されます — ツールバー項目がスクリプト項目として作成されたことが一度もないにもかかわらず。ツールバー項目の所有者は{{prop_status}}と入力しただけであり、実行可能なペイロードは完全にノート自身のfrontmatterから来ています。
JavaScriptAdapter.evaluate()(src/Adapters/JavaScriptAdapter.ts:143-205)は、本物のサンドボックス化されていないAsyncFunctionで式を実行します:
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));
これはJSエンジンの本物のAsyncFunctionコンストラクタであり、インタプリタ/サンドボックスではありません — ObsidianのElectronレンダラー内では完全なrequire()/Node.jsアクセスを持ち、以下のPoCでrequire('child_process').execSync(...)を介して確認されています。
重要なことに、JSアダプタは組み込みであり、コンパニオンプラグインを必要としません(src/Adapters/AdapterManager.ts:59: adapter = this.js; // built-in, doesn't rely on plugin)。これは、それらのプラグインのインストールを必要とするDataview/Templater/JS-Engine変数プレフィックスとは異なります。それをゲートするのは、プラグイン自身の「Scripting」トグル(scriptingEnabled、デフォルトfalse、src/Settings/NoteToolbarSettings.ts:300)だけです。
最後に、代入をトリガーするレンダリングパスは自動的であり、クリックでゲートされません: ToolbarRenderer.ts::renderLItems()は、ノートに対してツールバーがレンダリングされるたびにthis.ntb.vars.resolveText(toolbar, file)(src/Toolbar/ToolbarRenderer.ts:418)を呼び出します — つまりノートを開く/表示するたびに — これがreplaceVars()を介してすべての項目のラベルとツールチップを解決します。影響を受けるツールバー項目をクリックする必要はありません。
ツールバーからノートへの割り当て自体がfrontmatter駆動です(toolbarProp設定、デフォルトキーnotetoolbar、src/Settings/NoteToolbarSettings.ts:317)。そのため、同期されたノートはそれ自体でどのツールバーがレンダリングされるかを選択できます — ただし、被害者が既に脆弱な項目を含むデフォルトツールバーまたはフォルダマッピングを使用している場合、悪用にはこれは必要ありません。
これは、メンテナ自身のSECURITY.mdが文書化しているリスク(「ユーザースクリプト...ユーザーが提供したJavaScriptを実行する...意図的かつ設計によるもの」)とは異なります — それは、ユーザーが{{js:}}ツールバー項目を故意に作成するか、それを含む共有ツールバー設定を故意にインポートすることを説明しています。ここでは、ツールバー項目は設定者の観点からはスクリプト項目では全くなく(単なるプロパティ表示ラベルです)、実行可能なペイロードは通常のノート内容/frontmatterを介して到着します — 他のObsidian同期/設定コード実行の知見(obsidian-syncthing-integration、obsidian-codescript-toolkit)と同じ信頼できないVault内容の脅威モデルです。
実際のObsidian 1.13.4デスクトップインスタンス(Xvfb + fluxbox)に対して、監査済みソースからビルドされた実際のプラグイン(main.js)をロードして動的に検証済みであり、単なる関数抽出ではありません。
Project Alpha.mdを含むVault PoCVault/:
---
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プラグインをインストールし、コミュニティプラグインを信頼し、プラグイン自身の設定UIを介して「Toolbar」という名前の1つのツールバーを作成し、Labelフィールドが{{prop_status}}である単一の項目を持つ(単なるプロパティ表示ラベル — スクリプト項目として設定されたことは一度もない)。
プラグイン設定Scriptingをオンにする(被害者のオプトイン、デフォルトではオフ)。
単にProject Alpha.mdを開く/表示する — ツールバー項目をクリックしない — と、ツールバーがok(JS式の戻り値)を表示してレンダリングされ、注入されたシェルコマンドが実際に実行されます:
ターミナルの証明(~/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
生のfrontmatterペイロードとレンダリングされたokツールバーを示すObsidianのスクリーンショット(~/engagements/obsidian-note-toolbar/evidence/01_obsidian_toolbar_rce_trigger.png)。
ソースノートは~/engagements/obsidian-note-toolbar/evidence/poc_note_frontmatter.mdに保存されています。
Obsidianを実行しているローカルユーザーとしての完全な任意コード実行であり、単にノートを開くだけでトリガーされます — クリックも、明示的な「スクリプト実行」アクションも、ダイアログもありません。チーム/共有Vaultのワークフロー(Obsidian Sync、GitベースのVault、Syncthing、Google Driveなど)では、ノートのfrontmatterを編集できる任意の貢献者が、Note ToolbarのScripting設定を有効にし、ノートプロパティを表示するツールバー項目を持つ他のすべての共同編集者に対してRCEを達成できます。
{{...}}テンプレート/変数構文が、それ自体がさらなるテンプレートディレクティブを含む攻撃者制御の代入値を中和しません。VariableResolver.replaceVars()では、スクリプトプレフィックスのチェック({{js:、{{dv:、{{jse:、<%/{{tp:)は、{{prop_*}}/{{note_title}}などの代入が実行された後の文字列ではなく、元の、代入前の文字列に対して評価されるべきです — つまり、ツールバー項目自体がスクリプトディレクティブとして作成されたかどうかを事前に判断し、代入されたfrontmatter値がスクリプトディレクティブとして再解釈されることを決して許さないようにします。具体的には: hasVarsスタイルのスクリプトプレフィックスチェックをPROP/SELECTION/NOTE_TITLEなどの代入ブロックの前に実行し、代入後の文字列に対してstartsWith('{{js:'/'{{dv:'/...)を再チェックすることをスキップします。多層防御として、frontmatterから来る値もエスケープ/中和して、{{...}}文法に再び入ることができないようにすることができます。
Dostxodjayev Abdullox
このリポジトリのSECURITY.mdは、GitHubのプライベート脆弱性報告が有効であり、推奨チャネルであることを確認しています(「GitHubプライベート脆弱性報告: このリポジトリのSecurityタブの下にある『Report a vulnerability』ボタンを使用してください」)。GitHubアカウントを持たない報告者向けにGoogleフォームのフォールバックがあります。独立して再検証済み: gh api repos/chrisgurney/obsidian-note-toolbar/private-vulnerability-reporting --jq .enabled → true、およびgh api repos/chrisgurney/obsidian-note-toolbar/security-advisories --jq 'length' → 0(先行するアドバイザリなし、重複リスクなし)。