OS command injection (RCE) in QOwnNotes via a synced note filename

A note filename is spliced into a bash -c string by the per-note Git-log feature, so a note delivered through a shared or synced notebook executes arbitrary commands when its history is viewed. Reported, fixed in 26.10.11.

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, git

Overview

QOwnNotes is an open-source, cross-platform (Qt/C++) notebook that stores each note as a plain Markdown file in a folder, and is designed around folder sync - Nextcloud, ownCloud, Syncthing, a shared drive. That sync model is the whole point of the product, and it is also what turns a local string-handling mistake into remote code execution.

The per-note Git history feature (“Show note git versions”) built a shell command line by concatenating the note’s filename into a string and handing it to bash -c. Because a note’s filename is attacker-controlled the moment the notebook is shared or synced, a crafted filename turns “view this note’s history” into “run this attacker’s command,” with the privileges of the desktop user. This is a textbook OS command injection (CWE-78), but reached through a data channel (the filesystem, via sync) that users do not think of as untrusted input.

Reported to the maintainer on 2026-10-23, acknowledged the same day, and fixed in release 26.10.11 on 2026-10-24.

Product QOwnNotes (desktop, Qt/C++)
Class OS command injection (CWE-78) -> remote code execution
Affected < 26.10.11
Fixed in 26.10.11 (note path hardened); 26.10.12 (libgit2 viewer enabled in all builds)
Vector Attacker-controlled note filename 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-jp2h-69pm-p69c (GitHub as CNA; CVE requested)

Root cause

The sink is Utils::Git::showLog() (src/utils/git.cpp). Rendering a note’s Git history produced a command string in which the note’s file path was interpolated with only double-quote wrapping - no escaping, no argument vector - and that string was then executed through a shell:

// reconstructed from the vulnerable pre-26.10.11 path
QString cmd = gitLogCommand;              // user's configured git-log command
cmd += " \"" + filePath + "\"";           // note path glued in, only "-quoted
QProcess::execute("bash", {"-c", cmd});   // shell parses the whole string

The wrapping quotes are the entire defense, so a single " in filePath closes the quoted region and everything after it is parsed by bash as new shell syntax. Nothing escapes the path, and nothing ends option parsing.

Two facts make the path genuinely attacker-controlled rather than theoretical:

  • Filenames survive sanitization. Note::cleanupFileName() strips only /, \ and : on Linux/macOS (the aggressive quote-stripping variant is gated to FAT/NTFS). Notes that arrive by external sync are not run through cleanup at all - they are read off disk as-is. So shell metacharacters like ", `, $, ; live happily in a .md filename.
  • Only / is awkward. Because the single stripped shell-relevant character is /, a working payload just has to be slash-free; the note folder is already the process working directory, so no absolute paths are needed.

Preconditions and impact

Three conditions, all of which sit inside normal QOwnNotes usage:

  1. The user has configured an external Git-log command (the documented way to use the note versioning feature).
  2. The attacker controls a note filename in that notebook - i.e. the notebook is shared or synced, which is QOwnNotes’ core Nextcloud/ownCloud workflow.
  3. The user opens Note -> Show note git versions on the attacker’s note.

When they do, the injected command runs as the user. This is the same shape as the gitk crafted-filename command execution (CVE-2025-27614): a Git UI feature that trusts a filename it should treat as hostile data.

# attacker names a note (slash-free, breaks out of the "-quoted region):
meeting-notes";touch $HOME/QOWN_RCE_PROOF;"z.md

# the per-note git-log then runs, effectively:
bash -c  git log "…/meeting-notes";touch $HOME/QOWN_RCE_PROOF;"z.md"
#                                   └──────── injected, runs as the user ────────┘

Confirmed end-to-end on the shipped v26.10.10 AppImage (headless Xvfb + xfwm4): a note synced into the folder with a payload filename, external git-log enabled in the real UI, then “Show note git versions” - the injected command executed and wrote its marker. A benign marker (touch) stands in for what would be a reverse shell in a real attack.

The fix

Fixed by the maintainer in 26.10.11 by removing the shell from the code path entirely. The note path is no longer concatenated into a command string; instead the configured command is tokenized with QProcess::splitCommand() and run as an argument vector (setProgram / setArguments / startDetached), so the note path is passed as one opaque argument that no shell ever parses. Defense-in-depth was added on top: rejection of NUL / unterminated quotes, rejection of a %notePath% substring that could smuggle the path back into the program token, and a leading ./ prefix on the path so it can never be read as an option.

In 26.10.12 the maintainer went further and enabled the built-in libgit2 history viewer in all release builds (issue #3802), so the default path uses the library and never shells out at all. I re-verified both: the single-string QProcess::start(QString) overload is absent from the binary, and there are no bash -c / sh -c wrapper literals on either code path. CWE-78 is closed.

The general lesson. Never build a shell command line out of a filename - or any value you did not generate yourself. Pass arguments as an argument vector so the shell is never in the loop, and remember that on a sync-first application the filesystem is an attacker-reachable input channel.

Disclosure timeline

  • 2026-10-23 - Reported to the maintainer (Patrizio Bekerle) by email with a full write-up and a screen-recording PoC; proposed coordinated disclosure.
  • 2026-10-23 - Acknowledged the same day.
  • 2026-10-24 - Advisory GHSA-jp2h-69pm-p69c opened (GitHub as CNA), reporter credited, CVE requested; fix released in 26.10.11 (release notes reference the advisory and the credit).
  • 2026-10-24 - Fix independently verified in the released tag; 26.10.12 adds the libgit2 viewer as further hardening.

References

  • QOwnNotes 26.10.11 / 26.10.12 release notes (fix and credit).
  • Advisory: GHSA-jp2h-69pm-p69c. CVE: requested via GitHub, pending assignment.
  • CWE-78: Improper Neutralization of Special Elements used in an OS Command.
  • Prior art for the pattern: CVE-2025-27614 (gitk, crafted filename -> command execution).

Credit

Reported by Dilanka Kaushal Hewage (r3vpwnx). Thanks to Patrizio Bekerle for the prompt fix and the clean coordinated-disclosure handling.