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, собранный из проверенного исходного кода), а не извлечением отдельной функции.