Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
Tools/GitHubGitHub/squeeze440/obsidian-note-toolbar-poc
Static AnalysisVulnerability AnalysisCode AnalysisExploitationWeb Application ExploitationPapers & ResearchLearning & EducationPayload Development
GitHub
squeeze440/obsidian-note-toolbar-poc

obsidian-note-toolbar-PoC

PoC — frontmatter-driven arbitrary JavaScript execution in Note Toolbar for Obsidian (GHSA-q8cw-3m8c-5pf2, CVE-2026-87002, CVSS 7.0).

View Repository
7 days agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

Frontmatter-Driven Arbitrary Code Execution in Note Toolbar via {{prop_NAME}} → {{js:}} Variable Chain

CVE status: requested, pending assignment. This finding is published as GHSA-q8cw-3m8c-5pf2. On CVE assignment this repository is renamed CVE-YYYY-NNNNN-obsidian-note-toolbar-PoC and this banner is replaced with the CVE link.

ResearcherDostxodjayev Abdullox (@squeeze440)
AdvisoryGHSA-q8cw-3m8c-5pf2
CVSS 3.17.0 (High)
WeaknessCWE-94, CWE-1336

Frontmatter-Driven Arbitrary Code Execution in Note Toolbar via {{prop_NAME}} → {{js:}} Variable Chain

Summary

Improper output neutralization in the variable-substitution engine of Note Toolbar (Obsidian plugin) allows an attacker who controls a note's YAML frontmatter (e.g. a collaborator on a synced/shared vault) to achieve arbitrary code execution on the victim's machine when the victim simply opens the note, by injecting a {{js: ...}} payload into a frontmatter property that a pre-existing, non-script toolbar item label/tooltip/link references via {{prop_NAME}}.

Product

Note Toolbar (note-toolbar) — Obsidian community plugin Repository: https://github.com/chrisgurney/obsidian-note-toolbar

Tested Version

v1.34.12 (commit 520271c3027eb6da38bab10a686b21e14c664c13, 2026-07-29), against Obsidian desktop 1.13.4 on Linux.

Estimated CVSS v3.1

7.0 (High) — CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H

  • AV:L — the vulnerable component (the plugin's JS evaluator, running inside Obsidian's Electron renderer) is not network-facing; the payload arrives as local vault/file content synced in by whatever means the victim uses (Git, Syncthing, Obsidian Sync, shared folder), consistent with prior findings in this family (e.g. obsidian-syncthing-integration, obsidian-codescript-toolkit).
  • AC:H — exploitation depends on conditions outside the attacker's control that must already exist on the victim's install: (1) the plugin's "Scripting" setting must already be enabled (off by default), and (2) the victim must already have configured a toolbar item whose label, tooltip, or link is a bare {{prop_NAME}} reference to some frontmatter key. Both are realistic, documented usage patterns, but neither is the default.
  • PR:N — the attacker needs no access to the victim's system, only the ability to get a note into the victim's vault with attacker-controlled frontmatter.
  • UI:R — the victim must open/view the specific note; no click on the toolbar item is required, the toolbar renders automatically.
  • S:U, C:H/I:H/A:H — code executes with full Node.js access (require('child_process'), filesystem, etc.) as the local user account running Obsidian.

Details

Note Toolbar supports a documented {{prop_NAME}} variable that substitutes a note's frontmatter value into a toolbar item's label, tooltip, or link (skills/note-toolbar-variables/SKILL.md, wiki Variables.md). It also supports a {{js: <expr>}} variable that evaluates the expression as live JavaScript when the "Scripting" plugin setting is enabled.

The bug is in the order of operations in src/Toolbar/VariableResolver.ts::replaceVars():

  1. Lines 74–92: {{prop_KEY}} placeholders are substituted with the raw value of frontmatter[KEY] from the current note — unconditionally, regardless of the scriptingEnabled setting, and regardless of the source of that frontmatter value.
  2. Lines 94–104 (inside if (this.ntb.settings.scriptingEnabled)): the function then checks s.trim().startsWith('{{js:') on the already-substituted string s, and if true, strips the {{js:/}} wrapper and passes the remainder to JavaScriptAdapter.use() for evaluation.

Because step 1 runs before step 2 and rewrites s in place, a frontmatter value that itself begins with {{js: and ends with }} gets promoted from "displayed text" to "executed script" — even though the toolbar item was never authored as a script item. The toolbar item's owner only ever typed {{prop_status}}; the executable payload comes entirely from the note's own frontmatter.

JavaScriptAdapter.evaluate() (src/Adapters/JavaScriptAdapter.ts:143-205) runs the expression with a real, unsandboxed AsyncFunction:

root@kitploit:~
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));

This is the JS engine's genuine AsyncFunction constructor, not an interpreter/sandbox — inside Obsidian's Electron renderer this has full require()/Node.js access, confirmed in the PoC below via require('child_process').execSync(...).

Critically, the JS adapter is built-in and requires no companion plugin (src/Adapters/AdapterManager.ts:59: adapter = this.js; // built-in, doesn't rely on plugin), unlike the Dataview/Templater/JS-Engine variable prefixes which need those plugins installed. Only the plugin's own "Scripting" toggle (scriptingEnabled, default false, src/Settings/NoteToolbarSettings.ts:300) gates it.

Finally, the render path that triggers substitution is automatic, not click-gated: ToolbarRenderer.ts::renderLItems() calls this.ntb.vars.resolveText(toolbar, file) (src/Toolbar/ToolbarRenderer.ts:418) every time a toolbar is rendered for a note — i.e. on note open/view — which resolves every item's label and tooltip via replaceVars(). No click on the affected toolbar item is required.

Toolbar-to-note assignment is itself frontmatter-driven (toolbarProp setting, default key notetoolbar, src/Settings/NoteToolbarSettings.ts:317), so a synced note can select which toolbar renders on it — but this is not required for exploitation if the victim already uses a default toolbar or folder mapping that includes the vulnerable item.

This is distinct from the risk the maintainer's own SECURITY.md documents ("User scripts... executes JavaScript provided by the user... intentional and by design") — that describes a user knowingly authoring a {{js:}} toolbar item, or knowingly importing a shared toolbar config containing one. Here, the toolbar item is not a script item at all from the configurer's perspective (it is a plain property-display label), and the executable payload arrives via ordinary note content/frontmatter — the same untrusted-vault-content threat model as other Obsidian sync/config code-execution findings (obsidian-syncthing-integration, obsidian-codescript-toolkit).

Proof of Concept

Dynamically verified against a real Obsidian 1.13.4 desktop instance (Xvfb + fluxbox), loading the actual built plugin (main.js built from the audited source), not a bare-function extraction.

  1. Vault PoCVault/ with Project Alpha.md:

    root@kitploit:~
    ---
    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
    ---
    
  2. Note Toolbar plugin installed, community plugins trusted, one toolbar named "Toolbar" created via the plugin's own Settings UI with a single item whose Label field is {{prop_status}} (a plain property-display label — never configured as a script item).

  3. Plugin setting Scripting toggled on (victim opt-in, off by default).

  4. Simply opening/viewing Project Alpha.md — no click on the toolbar item — renders the toolbar showing ok (the JS expression's return value), and the injected shell commands execute for real:

    Terminal proof (~/engagements/obsidian-note-toolbar/evidence/02_terminal_proof_of_execution.png):

    root@kitploit:~
    $ 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 showing the raw frontmatter payload and the rendered ok toolbar (~/engagements/obsidian-note-toolbar/evidence/01_obsidian_toolbar_rce_trigger.png).

    Source note preserved at ~/engagements/obsidian-note-toolbar/evidence/poc_note_frontmatter.md.

Impact

Full arbitrary code execution as the local user running Obsidian, triggered by merely opening a note — no click, no explicit "run script" action, no dialog. In a team/shared-vault workflow (Obsidian Sync, Git-backed vault, Syncthing, Google Drive, etc.), any contributor able to edit a note's frontmatter can achieve RCE on every other collaborator who has Note Toolbar's Scripting setting enabled and any toolbar item that displays a note property.

Weaknesses (CWE)

  • CWE-94: Improper Control of Generation of Code ('Code Injection') — untrusted frontmatter data is concatenated into a string later interpreted as an executable script directive.
  • CWE-1336: Improper Neutralization of Special Elements Used in a Template Engine — the {{...}} template/variable syntax does not neutralize attacker-controlled substitution values that themselves contain further template directives.

Remediation

In VariableResolver.replaceVars(), the scripting-prefix checks ({{js:, {{dv:, {{jse:, <%/{{tp:) should be evaluated against the original, pre-substitution string, not the string after {{prop_*}}/{{note_title}}/etc. substitution has run — i.e. determine up front whether the toolbar item itself was authored as a script directive, and never let a substituted frontmatter value be re-interpreted as one. Concretely: perform the hasVars-style scripting-prefix check before the PROP/SELECTION/NOTE_TITLE/etc. substitution block, and skip re-checking startsWith('{{js:'/'{{dv:'/...) on the string post-substitution. As defense in depth, values coming from frontmatter could also be escaped/neutralized so they can never re-enter the {{...}} grammar.

Credit

Dostxodjayev Abdullox

Reporting Channel

This repository's SECURITY.md confirms GitHub private vulnerability reporting is enabled and is the preferred channel ("GitHub private vulnerability reporting: use the 'Report a vulnerability' button under the Security tab of this repo"), with a Google Form fallback for reporters without a GitHub account. Independently re-verified: gh api repos/chrisgurney/obsidian-note-toolbar/private-vulnerability-reporting --jq .enabled → true, and gh api repos/chrisgurney/obsidian-note-toolbar/security-advisories --jq 'length' → 0 (no prior advisories, no duplicate-risk).

Download Tool