PoC — execução arbitrária de JavaScript orientada por frontmatter no Note Toolbar para Obsidian (GHSA-q8cw-3m8c-5pf2, CVE-2026-87002, CVSS 7.0).
{{prop_NAME}} → {{js:}}Status do CVE: solicitado, aguardando atribuição. Esta descoberta está publicada como GHSA-q8cw-3m8c-5pf2. Após a atribuição do CVE, este repositório será renomeado
CVE-YYYY-NNNNN-obsidian-note-toolbar-PoCe este banner será substituído pelo link do CVE.
| Pesquisador | Dostxodjayev Abdullox (@squeeze440) |
| Aviso | GHSA-q8cw-3m8c-5pf2 |
| CVSS 3.1 | 7.0 (Alto) |
| Fraqueza | CWE-94, CWE-1336 |
{{prop_NAME}} → {{js:}}A neutralização inadequada de saída no motor de substituição de variáveis do Note Toolbar (plugin do Obsidian) permite que um atacante que controla o frontmatter YAML de uma nota (por exemplo, um colaborador em um cofre sincronizado/compartilhado) alcance execução de código arbitrário na máquina da vítima quando a vítima simplesmente abre a nota, injetando um payload {{js: ...}} em uma propriedade de frontmatter que um item de barra de ferramentas pré-existente, não-script, referencia via {{prop_NAME}} em seu rótulo/tooltip/link.
Note Toolbar (note-toolbar) — plugin da comunidade Obsidian
Repositório: https://github.com/chrisgurney/obsidian-note-toolbar
v1.34.12 (commit 520271c3027eb6da38bab10a686b21e14c664c13, 2026-07-29), contra Obsidian desktop 1.13.4 no Linux.
7.0 (Alto) — CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H
AV:L — o componente vulnerável (o avaliador JS do plugin, executando dentro do renderer Electron do Obsidian) não é voltado à rede; o payload chega como conteúdo local de cofre/arquivo sincronizado por qualquer meio que a vítima utilize (Git, Syncthing, Obsidian Sync, pasta compartilhada), consistente com descobertas anteriores desta família (por exemplo, obsidian-syncthing-integration, obsidian-codescript-toolkit).AC:H — a exploração depende de condições fora do controle do atacante que já devem existir na instalação da vítima: (1) a configuração "Scripting" do plugin já deve estar habilitada (desativada por padrão), e (2) a vítima já deve ter configurado um item de barra de ferramentas cujo rótulo, tooltip ou link seja uma referência {{prop_NAME}} pura a alguma chave de frontmatter. Ambas são padrões de uso realistas e documentados, mas nenhuma é o padrão.PR:N — o atacante não precisa de acesso ao sistema da vítima, apenas da capacidade de colocar uma nota no cofre da vítima com frontmatter controlado pelo atacante.UI:R — a vítima deve abrir/visualizar a nota específica; nenhum clique no item da barra de ferramentas é necessário, a barra de ferramentas é renderizada automaticamente.S:U, C:H/I:H/A:H — o código executa com acesso total ao Node.js (require('child_process'), sistema de arquivos, etc.) como a conta de usuário local que executa o Obsidian.O Note Toolbar suporta uma variável documentada {{prop_NAME}} que substitui o valor de frontmatter de uma nota no rótulo, tooltip ou link de um item da barra de ferramentas (skills/note-toolbar-variables/SKILL.md, wiki Variables.md). Também suporta uma variável {{js: <expr>}} que avalia a expressão como JavaScript ativo quando a configuração "Scripting" do plugin está habilitada.
O bug está na ordem das operações em src/Toolbar/VariableResolver.ts::replaceVars():
{{prop_KEY}} são substituídos pelo valor bruto de frontmatter[KEY] da nota atual — incondicionalmente, independentemente da configuração scriptingEnabled, e independentemente da origem desse valor de frontmatter.if (this.ntb.settings.scriptingEnabled)): a função então verifica s.trim().startsWith('{{js:') na string s já substituída, e se verdadeiro, remove o invólucro {{js:/}} e passa o restante para JavaScriptAdapter.use() para avaliação.Como o passo 1 executa antes do passo 2 e reescreve s no local, um valor de frontmatter que ele mesmo começa com {{js: e termina com }} é promovido de "texto exibido" para "script executado" — mesmo que o item da barra de ferramentas nunca tenha sido criado como um item de script. O proprietário do item da barra de ferramentas apenas digitou {{prop_status}}; o payload executável vem inteiramente do próprio frontmatter da nota.
JavaScriptAdapter.evaluate() (src/Adapters/JavaScriptAdapter.ts:143-205) executa a expressão com um AsyncFunction real e sem 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));
Este é o construtor AsyncFunction genuíno do motor JS, não um interpretador/sandbox — dentro do renderer Electron do Obsidian isso tem acesso total a require()/Node.js, confirmado no PoC abaixo via require('child_process').execSync(...).
Crucialmente, o adaptador JS é integrado e não requer plugin complementar (src/Adapters/AdapterManager.ts:59: adapter = this.js; // built-in, doesn't rely on plugin), diferentemente dos prefixos de variável Dataview/Templater/JS-Engine que exigem esses plugins instalados. Apenas o próprio toggle "Scripting" do plugin (scriptingEnabled, padrão false, src/Settings/NoteToolbarSettings.ts:300) o controla.
Por fim, o caminho de renderização que dispara a substituição é automático, não condicionado a clique: ToolbarRenderer.ts::renderLItems() chama this.ntb.vars.resolveText(toolbar, file) (src/Toolbar/ToolbarRenderer.ts:418) toda vez que uma barra de ferramentas é renderizada para uma nota — ou seja, ao abrir/visualizar a nota — o que resolve o rótulo e o tooltip de cada item via replaceVars(). Nenhum clique no item da barra de ferramentas afetado é necessário.
A atribuição de barra de ferramentas para nota é ela mesma orientada por frontmatter (configuração toolbarProp, chave padrão notetoolbar, src/Settings/NoteToolbarSettings.ts:317), então uma nota sincronizada pode selecionar qual barra de ferramentas é renderizada nela — mas isso não é necessário para a exploração se a vítima já usa uma barra de ferramentas padrão ou mapeamento de pasta que inclua o item vulnerável.
Isso é distinto do risco que o próprio SECURITY.md do mantenedor documenta ("User scripts... executes JavaScript provided by the user... intentional and by design") — isso descreve um usuário criando conscientemente um item de barra de ferramentas {{js:}}, ou importando conscientemente uma configuração de barra de ferramentas compartilhada que contenha um. Aqui, o item da barra de ferramentas não é de forma alguma um item de script da perspectiva do configurador (é um rótulo simples de exibição de propriedade), e o payload executável chega via conteúdo/frontmatter comum de nota — o mesmo modelo de ameaça de conteúdo de cofre não confiável que outras descobertas de execução de código de sincronização/configuração do Obsidian (obsidian-syncthing-integration, obsidian-codescript-toolkit).
Verificado dinamicamente contra uma instância real do Obsidian 1.13.4 desktop (Xvfb + fluxbox), carregando o plugin real compilado (main.js compilado a partir do código-fonte auditado), não uma extração de função isolada.
Cofre PoCVault/ com 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
---
Plugin Note Toolbar instalado, plugins da comunidade confiáveis, uma barra de ferramentas chamada "Toolbar" criada via a própria UI de Configurações do plugin com um único item cujo campo Label é {{prop_status}} (um rótulo simples de exibição de propriedade — nunca configurado como item de script).
Configuração do plugin Scripting ativada (opt-in da vítima, desativada por padrão).
Simplesmente abrir/visualizar Project Alpha.md — sem clique no item da barra de ferramentas — renderiza a barra de ferramentas mostrando ok (o valor de retorno da expressão JS), e os comandos de shell injetados executam de verdade:
Prova no terminal (~/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
Captura de tela do Obsidian mostrando o payload de frontmatter bruto e a barra de ferramentas renderizada com ok (~/engagements/obsidian-note-toolbar/evidence/01_obsidian_toolbar_rce_trigger.png).
Nota de origem preservada em ~/engagements/obsidian-note-toolbar/evidence/poc_note_frontmatter.md.
Execução de código arbitrário completa como o usuário local que executa o Obsidian, disparada por meramente abrir uma nota — sem clique, sem ação explícita de "executar script", sem diálogo. Em um fluxo de trabalho de equipe/cofre compartilhado (Obsidian Sync, cofre com Git, Syncthing, Google Drive, etc.), qualquer colaborador capaz de editar o frontmatter de uma nota pode alcançar RCE em todos os outros colaboradores que tenham a configuração Scripting do Note Toolbar habilitada e qualquer item de barra de ferramentas que exiba uma propriedade de nota.
{{...}} não neutraliza valores de substituição controlados pelo atacante que eles mesmos contêm outras diretivas de template.Em VariableResolver.replaceVars(), as verificações de prefixo de scripting ({{js:, {{dv:, {{jse:, <%/{{tp:) devem ser avaliadas contra a string original, pré-substituição, não a string após a substituição de {{prop_*}}/{{note_title}}/etc. ter sido executada — ou seja, determinar de antemão se o próprio item da barra de ferramentas foi criado como uma diretiva de script, e nunca permitir que um valor de frontmatter substituído seja reinterpretado como uma. Concretamente: realizar a verificação de prefixo de scripting no estilo hasVars antes do bloco de substituição PROP/SELECTION/NOTE_TITLE/etc., e pular a reverificação de startsWith('{{js:'/'{{dv:'/...) na string pós-substituição. Como defesa em profundidade, valores vindos do frontmatter também poderiam ser escapados/neutralizados para que nunca possam reentrar na gramática {{...}}.
Dostxodjayev Abdullox
O SECURITY.md deste repositório confirma que o relato privado de vulnerabilidades do GitHub está habilitado e é o canal preferido ("GitHub private vulnerability reporting: use the 'Report a vulnerability' button under the Security tab of this repo"), com um formulário do Google como alternativa para relatores sem conta no GitHub. Reverificado de forma independente: gh api repos/chrisgurney/obsidian-note-toolbar/private-vulnerability-reporting --jq .enabled → true, e gh api repos/chrisgurney/obsidian-note-toolbar/security-advisories --jq 'length' → 0 (nenhum aviso prévio, sem risco de duplicata).