OS command injection (RCE) in VNote Tasks via a shared notebook

VNote's Tasks feature substituted magic variables such as ${bufferName} and ${selectedText} into a bash -c command string with no escaping, so a note filename or editor selection delivered through a shared or synced notebook ran arbitrary commands the moment a task executed. Reported through GitHub, fixed upstream in commit 0f5edd8.

Severity
High
CVSS
7.8 (CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H)
Published
2026
Tags
CWE-78, command-injection, rce, coordinated-disclosure, desktop, qt, vnote

Overview

VNote is a popular open-source, cross-platform (Qt/C++) note-taking application. Its Tasks feature runs a task’s command through a shell and substitutes “magic variables” such as ${bufferName}, ${buffer} and ${selectedText} into the command string. Those variables were inserted with no shell escaping, so a note whose filename (or whose selected text) contained shell metacharacters such as $(...) ran as a command the moment the user executed a task.

Because note files and per-notebook task definitions both travel inside a notebook, and because opening, importing, cloning, or syncing a notebook from an external source are all first-class VNote workflows, a notebook obtained from an untrusted source achieved arbitrary command execution as the user with a single click in the Tasks dock. The important property is that the task itself can be completely benign and user-authored: the payload lives in the data (a note filename or a selection), not in the command the user reviewed.

This is a different bug from CVE-2026-86022, which patched only the external-program “Open With” path. The Tasks sink is a genuine bash -c shell injection, in a different file, and it was present at master HEAD when reported.

Product VNote (desktop, Qt/C++)
Class OS command injection (CWE-78) -> remote code execution
Affected <= 4.11.0 (and master HEAD at report time)
Fixed in commit 0f5edd8 on master (“Fix task variable command injection”)
Vector Attacker-controlled note filename or selection in a shared / synced notebook
Severity High - CVSS:3.1 7.8 (AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H)
Advisory GHSA-j49v-rq9m-fphv (reporter credited); CVE requested

Root cause

Three pieces combine into the bug.

1. The shell builder does not escape

In src/core/services/shellexecution.cpp, setupProcess() builds, for a bash shell, a string of the form bash -c "<program> <quoteSpaces(args)>". The quoting helper only wraps a token in double quotes when it contains a space, and escapes nothing:

QString ShellExecution::quoteSpace(const QString &p_arg) {
  if (p_arg.contains(QLatin1Char(' ')))
    return QLatin1Char('"') + p_arg + QLatin1Char('"'); // wraps on space only
  else
    return p_arg;                                       // otherwise verbatim
}

So $(...), backticks, and the characters ; & | " \ all stay active. Command substitution with $(...) and backticks executes even inside the double quotes this adds, and a literal " breaks out of the quoting entirely.

2. Magic variables are substituted raw

In src/core/services/task.cpp the command and args are run through variableMgr().evaluate(), and in src/core/services/taskvariablemgr.cpp the values are inserted without quoting. ${bufferName} is the note filename, ${buffer} is the note path, and ${selectedText} is the raw editor selection. All of them are attacker-influenced data, and all of them reach the shell string verbatim.

3. Tasks ship inside a notebook

In src/core/services/taskservice.cpp, getNotebookTaskFolder() returns <root>/vx_notebook/tasks, and the loader walks that folder for *.json, so every task file that travels with a notebook becomes a runnable task. The note files themselves are listed from vx_notebook/contents/vx.json. Both the trap (a note named with a payload) and the trigger (a task that references a magic variable) can be shipped together in one shareable notebook.

Proof of concept

Reproduced end to end on the shipped v4.11.0 Linux AppImage.

The attacker prepares a bundled notebook with a note file named report$(id>VNOTE_TASKS_RCE_PROOF).md (registered in vx_notebook/contents/vx.json so VNote lists it) and a task file vx_notebook/tasks/export.json:

{"version":"0.1.3","type":"shell","label":"Export note (HTML)","command":"echo","args":["${bufferName}"]}

The victim opens or syncs the notebook, opens the note, and runs Tasks > Notebook Tasks > "Export note (HTML)". VNote executes:

bash -c  echo report$(id>VNOTE_TASKS_RCE_PROOF).md

The injected $(id>...) runs and writes a file named VNOTE_TASKS_RCE_PROOF in the task working directory containing the output of id. The console reports the task finished successfully, with no indication that anything beyond echo ran.

The note filename cannot contain /, so a payload that writes within the working directory is used here. A ${selectedText} task removes even that constraint, since the selection can contain any character.

Relationship to CVE-2026-86022

CVE-2026-86022 (GHSA-6m2r-wm6v-c8qp, fixed in 4.3.0) was argument injection in the external-program “Open With” path (sessionconfig.cpp and processutils.cpp), which relied on QProcess::start(QString) re-tokenisation, used no shell, and required a " in the filename. The Tasks issue described here is a different file, a genuine shell (bash -c) sink, and is not addressed by the 4.3.0 fix.

Suggested fix

Run tasks with an argument vector (QProcess::setProgram / setArguments) rather than a shell string. When a shell is genuinely required, pass variable-derived values as positional parameters, for example:

bash -c 'template with "$1" "$2"' _ "$a" "$b"

and never concatenate them into the -c string. If the current design is kept, every substituted token must be quoted properly (single-quote with '\'' escaping), not the space-only quoteSpace(). Treating notebook-shipped tasks as untrusted (confirm on first run, show the exact command) is sound defense in depth.

Disclosure

  • 2026-10-24 - Reported to the maintainer through GitHub Private Vulnerability Reporting (advisory GHSA-j49v-rq9m-fphv), with a full write-up and a screen recording of the proof of concept. Credited as reporter.
  • 2026-10-24 - The maintainer initially closed the report, taking the position that a user is responsible for the tasks they choose to run.
  • 2026-10-24 - After I clarified the core issue - that the unescaped magic variables make a user’s own benign task execute code from data alone (a note filename or selection), independent of whether notebook-shipped tasks are trusted - the maintainer agreed and committed a fix in commit 0f5edd8 (“Fix task variable command injection”). It makes task shell commands and options static, passes variable values only as literal arguments, and no longer expands them recursively; the bundled Git task was migrated and a regression test was added. The advisory was then closed without publishing, so no CVE was assigned through GitHub.
  • A CVE has been requested through a CNA so the issue has a durable public identifier; this post will be updated with the CVE ID once it is assigned.

I want to represent the maintainer’s position fairly. For a task that ships inside an untrusted notebook, “you imported a notebook with executable tasks and chose to run one” is a reasonable stance, and it is the same question editors like VS Code answered with a Workspace Trust prompt rather than by treating the behavior as expected. The core issue, though, is narrower and is not a matter of user choice: the magic variables were substituted into bash -c without escaping, so a user’s own trusted, benign task became code execution purely from data (a note filename or a selection) that arrived through VNote’s ordinary sync, clone and import workflows. The user never sees the payload and never agreed to run it. That is the part this write-up documents, and it is the part the fix addresses by moving variable values into literal arguments.

For VNote users

The fix is on master (commit 0f5edd8) but, at the time of writing, is not yet in a tagged release; the current release, 4.11.0, is still affected. Until you are running a build that includes the fix, do not run tasks on notebooks you did not create yourself, and be cautious opening notebooks obtained by clone, import, or sync from sources you do not control.


Researcher: Dilanka Kaushal Hewage (r3vpwnx). Found as part of a wider review of command injection in desktop note applications.