PoC — выполнение произвольного JavaScript через frontmatter в Note Toolbar для Obsidian (GHSA-q8cw-3m8c-5pf2, CVE-2026-87002, CVSS 7.0).
{{prop_NAME}} → {{js:}}Статус CVE: запрошен, ожидает присвоения. Данная уязвимость опубликована как GHSA-q8cw-3m8c-5pf2. После присвоения CVE данный репозиторий будет переименован в
CVE-YYYY-NNNNN-obsidian-note-toolbar-PoC, а этот баннер заменён ссылкой на CVE.
| Исследователь | Dostxodjayev Abdullox (@squeeze440) |
| Advisory | GHSA-q8cw-3m8c-5pf2 |
| CVSS 3.1 | 7.0 (High) |
| Слабость | CWE-94, CWE-1336 |
{{prop_NAME}} → {{js:}}Ненадлежащая нейтрализация вывода в движке подстановки переменных Note Toolbar (плагин Obsidian) позволяет злоумышленнику, контролирующему YAML frontmatter заметки (например, участнику совместной работы в синхронизируемом/общем хранилище), добиться выполнения произвольного кода на машине жертвы при простом открытии заметки путём внедрения полезной нагрузки {{js: ...}} в свойство frontmatter, на которое ссылается уже существующий, не являющийся скриптовым элемент панели инструментов через {{prop_NAME}} в метке, всплывающей подсказке или ссылке.
Note Toolbar (note-toolbar) — плагин сообщества Obsidian
Репозиторий: https://github.com/chrisgurney/obsidian-note-toolbar
v1.34.12 (коммит 520271c3027eb6da38bab10a686b21e14c664c13, 2026-07-29), на Obsidian desktop 1.13.4 под 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 — уязвимый компонент (JS-вычислитель плагина, работающий внутри Electron-рендерера Obsidian) не имеет сетевого интерфейса; полезная нагрузка поступает как локальное содержимое хранилища/файла, синхронизированное любым способом, который использует жертва (Git, Syncthing, Obsidian Sync, общая папка), что согласуется с предыдущими находками этого семейства (например, obsidian-syncthing-integration, obsidian-codescript-toolkit).AC:H — эксплуатация зависит от условий, не контролируемых злоумышленником, которые должны уже существовать в установке жертвы: (1) настройка плагина "Scripting" должна быть уже включена (по умолчанию выключена), и (2) жертва должна уже иметь настроенный элемент панели инструментов, метка, всплывающая подсказка или ссылка которого является простой ссылкой {{prop_NAME}} на некоторый ключ frontmatter. Оба варианта являются реалистичными, документированными шаблонами использования, но ни один из них не является поведением по умолчанию.PR:N — злоумышленнику не нужен доступ к системе жертвы, только возможность поместить заметку в хранилище жертвы с контролируемым злоумышленником frontmatter.UI:R — жертва должна открыть/просмотреть конкретную заметку; клик по элементу панели инструментов не требуется, панель отображается автоматически.S:U, C:H/I:H/A:H — код выполняется с полным доступом к Node.js (require('child_process'), файловая система и т. д.) от имени локальной учётной записи пользователя, запускающего Obsidian.Note Toolbar поддерживает документированную переменную {{prop_NAME}}, которая подставляет значение frontmatter заметки в метку, всплывающую подсказку или ссылку элемента панели инструментов (skills/note-toolbar-variables/SKILL.md, wiki Variables.md). Он также поддерживает переменную {{js: <expr>}}, которая вычисляет выражение как живой JavaScript, когда включена настройка плагина "Scripting".
Ошибка заключается в порядке операций в src/Toolbar/VariableResolver.ts::replaceVars():
{{prop_KEY}} заменяются необработанным значением frontmatter[KEY] из текущей заметки — безусловно, независимо от настройки scriptingEnabled и независимо от источника этого значения frontmatter.if (this.ntb.settings.scriptingEnabled)): затем функция проверяет s.trim().startsWith('{{js:') для уже подставленной строки s, и если это так, удаляет обёртку {{js:/}} и передаёт остаток в JavaScriptAdapter.use() для вычисления.Поскольку шаг 1 выполняется до шага 2 и перезаписывает s на месте, значение frontmatter, которое само начинается с {{js: и заканчивается на }}, повышается из "отображаемого текста" в "выполняемый скрипт" — даже несмотря на то, что элемент панели инструментов никогда не создавался как скриптовый элемент. Владелец элемента панели инструментов вводил только {{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));
Это подлинный конструктор AsyncFunction движка JS, а не интерпретатор/песочница — внутри Electron-рендерера Obsidian он имеет полный доступ к 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 сопровождающего ("User scripts... executes JavaScript provided by the user... intentional and by design") — там описывается пользователь, сознательно создающий элемент панели инструментов {{js:}} или сознательно импортирующий общую конфигурацию панели инструментов, содержащую его. Здесь элемент панели инструментов вообще не является скриптовым элементом с точки зрения настройщика (это обычная метка отображения свойства), а исполняемая полезная нагрузка поступает через обычное содержимое/frontmatter заметки — та же модель угроз недоверенного содержимого хранилища, что и в других находках выполнения кода через синхронизацию/конфигурацию Obsidian (obsidian-syncthing-integration, obsidian-codescript-toolkit).
Динамически проверено на реальном экземпляре Obsidian 1.13.4 desktop (Xvfb + fluxbox) с загрузкой фактического собранного плагина (main.js, собранный из проверенного исходного кода), а не извлечением отдельной функции.
Хранилище PoCVault/ с 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 установлен, плагины сообщества доверены, одна панель инструментов с именем "Toolbar" создана через собственный UI настроек плагина с единственным элементом, поле 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
Скриншот Obsidian, показывающий необработанную полезную нагрузку frontmatter и отрендеренную панель инструментов ok (~/engagements/obsidian-note-toolbar/evidence/01_obsidian_toolbar_rce_trigger.png).
Исходная заметка сохранена в ~/engagements/obsidian-note-toolbar/evidence/poc_note_frontmatter.md.
Полное выполнение произвольного кода от имени локального пользователя, запускающего Obsidian, инициируемое простым открытием заметки — без клика, без явного действия "запустить скрипт", без диалога. В рабочем процессе команды/общего хранилища (Obsidian Sync, хранилище на базе Git, Syncthing, Google Drive и т. д.) любой участник, способный редактировать frontmatter заметки, может добиться RCE у каждого другого участника, у которого включена настройка Scripting в Note Toolbar и есть любой элемент панели инструментов, отображающий свойство заметки.
{{...}} не нейтрализует контролируемые злоумышленником значения подстановки, которые сами содержат дальнейшие директивы шаблона.В 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 private vulnerability reporting: use the 'Report a vulnerability' button under the Security tab of this repo"), с резервной формой Google Form для сообщающих без учётной записи GitHub. Независимо перепроверено: 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 (нет предыдущих advisory, нет риска дублирования).