Same shared-root-cause family as the other frontend findings in this
review (unescaped reflection leading to RCE via the Electron
nodeIntegration:true configuration), but a different injection
mechanism (HTML-attribute breakout rather than tag injection) and a
different, very low-friction trigger (hovering over a file entry while
browsing, not clicking a link or dragging a file). Confirmed as a real
gap, not a design choice, by direct comparison with the correctly-fixed
sibling implementation of the identical UI feature elsewhere in the same
codebase.
Summary
The file-tree picker used by SiYuan's "move document to" and similar
path-selection dialogs builds each entry's hover-tooltip
(aria-label attribute) by directly concatenating four document
metadata fields, bookmark, alias, memo, and an alternate name field,
into a double-quoted HTML attribute string with no escaping. These four
fields are ordinary, user-settable custom attributes any document
author can set on any document (via SiYuan's standard "custom
attributes"/bookmark UI), and documents carrying them routinely reach
other users through sharing, sync, or import. A document crafted with a
double quote in any of these fields breaks out of the aria-label
attribute and injects arbitrary HTML attributes, including inline event
handlers such as onmouseover, onto the list item. Given every SiYuan
BrowserWindow runs with nodeIntegration:true,
contextIsolation:false, and no CSP, an injected handler has immediate
require("child_process") access. The trigger requires only that the
victim open the "move to"/path-picker dialog and hover their mouse over
the malicious document's entry while browsing, no click, no
confirmation. This is CWE-79 (Cross-Site Scripting), specifically an
attribute-context injection, escalating to CWE-94 (Code Injection) via
the Electron configuration.
Details
app/src/util/pathName.ts, inside getLeaf() (used by the dialog built
in movePathTo()), around line 711:
fileHTML += `<li data-box="${notebookId}" class="b3-list-item" data-path="${item.path}">
<span style="padding-left: ${item.path.split("/").length * 8}px" class="b3-list-item__toggle b3-list-item__toggle--hl${item.subFileCount === 0 ? " fn__hidden" : ""}">
<svg class="b3-list-item__arrow"><use xlink:href="#iconRight"></use></svg>
</span>
${unicode2Emoji(item.icon || (item.subFileCount === 0 ? window.siyuan.storage[Constants.LOCAL_IMAGES].file : window.siyuan.storage[Constants.LOCAL_IMAGES].folder), "b3-list-item__graphic", true)}
<span class="b3-list-item__text ariaLabel" data-position="parentE" aria-label="${getDocDisplayName(item.name, item.titleEmpty, true)} <small class='ft__on-surface'>${item.hSize}</small>${item.bookmark ? "<br>" + window.siyuan.languages.bookmark + " " + item.bookmark : ""}${item.name1 ? "<br>" + window.siyuan.languages.name + " " + item.name1 : ""}${item.alias ? "<br>" + window.siyuan.languages.alias + " " + item.alias : ""}${item.memo ? "<br>" + window.siyuan.languages.memo + " " + item.memo : ""}${item.subFileCount !== 0 ? window.siyuan.languages.includeSubFile.replace("x", item.subFileCount) : ""}<br>${window.siyuan.languages.modifiedAt} ${item.hMtime}<br>${window.siyuan.languages.createdAt} ${item.hCtime}">${getDocDisplayName(item.name, item.titleEmpty, true)}</span>
${countHTML}
</li>`;
item.bookmark, item.name1 (alternate/renamed title), item.alias,
and item.memo are interpolated raw, directly inside the double-quoted
aria-label="..." attribute, with no HTML-escaping of any kind. Only
the displayed title text (via getDocDisplayName(..., true), note the
true third argument requesting escaping) is safe; the tooltip content
built around it is not.
Confirmed as a real, specific gap, not a deliberate design choice,
by direct comparison with the desktop main file dock's implementation
of the identical feature, app/src/layout/dock/Files.ts:1554:
return `${escapeMethod(getDocDisplayName(item.name, item.titleEmpty))} <small class='ft__on-surface'>${item.hSize}</small>${item.bookmark ? "<br>" + window.siyuan.languages.bookmark + " " + escapeMethod(item.bookmark) : ""}${item.name1 ? "<br>" + window.siyuan.languages.name + " " + escapeMethod(item.name1) : ""}${item.alias ? "<br>" + window.siyuan.languages.alias + " " + escapeMethod(item.alias) : ""}${item.memo ? "<br>" + window.siyuan.languages.memo + " " + escapeMethod(item.memo) : ""}...`;
Here, every one of the same four fields is correctly wrapped in
escapeMethod(). This is the exact same tooltip content, for the exact
same file-tree feature, in a different file, correctly escaped in one
and not the other, direct proof the omission in pathName.ts is a gap
rather than an intentional exemption.
Step-by-step reproduction
- On any document (one the attacker can get in front of the victim via
sharing, sync, import, or a received workspace/notebook), set the
"Memo" custom attribute (or Bookmark, or Alias) to a payload
containing a double quote to break out of the aria-label attribute,
for example:
" onmouseover="require('child_process').exec('calc')
- Get this document into the victim's workspace (share the notebook,
send an exported .sy/import bundle, or have it present via sync).
- In the SiYuan desktop app, have the victim trigger any action that
opens the path-picker/"move to" dialog for a notebook containing this
document, for example right-clicking a document and choosing "Move
to," or using a reference/link "move to path" action, then expanding
the folder containing the crafted document in the resulting tree.
- The moment the victim's mouse hovers over the crafted document's
list entry (a completely ordinary action while browsing the tree to
pick a destination), the browser evaluates the injected
onmouseover attribute and executes the payload.
- Given the Electron window's
nodeIntegration:true/
contextIsolation:false configuration, require("child_process")
succeeds, achieving arbitrary OS command execution at the victim's
privilege level.
(Not run in a live Electron instance in this review, static analysis
of the TypeScript source only; both the vulnerable and the correctly-
fixed sibling implementation are read directly and quoted in full
above, and the "move to" dialog wiring (getLeaf called from the
.b3-list-item__toggle click handler inside movePathTo's dialog) is
traced directly from source to confirm this is a live, reachable code
path, not dead code.)
Impact
Any SiYuan user who opens a path-picker dialog (an extremely routine
action, used for moving documents, and other path-selection flows) on a
notebook containing a document with attacker-crafted bookmark, alias,
memo, or alternate-name metadata achieves code execution on the victim's
machine the instant their mouse passes over that document's entry in the
tree, with no click and no other confirmation required. Since these four
metadata fields are ordinary document attributes any author can set,
and documents routinely arrive via sharing, sync, or import from other
parties, this is a low-friction, high-impact stored XSS-to-RCE chain
distinct in mechanism (attribute-context injection via a missing
escapeMethod() call) from the other findings in this review round.
## Affected products
| Field | Value |
|---|---|
| Ecosystem | **npm** |
| Package name | `siyuan` (desktop application, `app/src/util/pathName.ts`) |
| Affected versions | Present at current HEAD (commit `eef1056`/`1673b75`, reviewed 2026-08-03) |
| Patched versions | *(none yet, leave blank until a fix is released)* |
## Severity
| Field | Value |
|---|---|
| Vector string | `CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H` |
| Score | **8.4 (High)**, matching the severity class of the other frontend findings in this review round. Local vector, low complexity (a mouse hover is trivial to induce during ordinary use), no privileges required by the attacker, user interaction required but of an unusually low-friction kind (hovering, not clicking), scope change via the Electron configuration escaping the renderer boundary, complete confidentiality/integrity/availability impact once OS command execution is achieved. |
## Weaknesses (CWE)
- **CWE-79**: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (primary), specifically an HTML-attribute-context injection
- **CWE-94**: Improper Control of Generation of Code ('Code Injection') (resulting impact, via the `nodeIntegration:true` Electron configuration)
## Notes for filing
- Same broad root-cause family (unescaped reflection + Electron's
`nodeIntegration:true`) as this review round's other two frontend
advisories (upload-filename/AI-provider reflection into
`showMessage`, and the unvalidated `shell.openExternal` calls), but a
distinct sink, distinct injection mechanism (attribute breakout, not
tag injection into a message panel), and a distinct, arguably lower-
friction trigger (hover vs. drag-drop or click). Worth filing
separately given the different code path and different fix location,
while noting the shared theme when reporting.
- Suggested fix: wrap `item.bookmark`, `item.name1`, `item.alias`, and
`item.memo` in the same `escapeMethod()` (or equivalent
`Lute.EscapeHTMLStr`) call already correctly used for the identical
fields in `app/src/layout/dock/Files.ts:1554`, and audit for any
other call sites building this same tooltip content independently.
Same shared-root-cause family as the other frontend findings in this
review (unescaped reflection leading to RCE via the Electron
nodeIntegration:trueconfiguration), but a different injectionmechanism (HTML-attribute breakout rather than tag injection) and a
different, very low-friction trigger (hovering over a file entry while
browsing, not clicking a link or dragging a file). Confirmed as a real
gap, not a design choice, by direct comparison with the correctly-fixed
sibling implementation of the identical UI feature elsewhere in the same
codebase.
Summary
The file-tree picker used by SiYuan's "move document to" and similar
path-selection dialogs builds each entry's hover-tooltip
(
aria-labelattribute) by directly concatenating four documentmetadata fields, bookmark, alias, memo, and an alternate name field,
into a double-quoted HTML attribute string with no escaping. These four
fields are ordinary, user-settable custom attributes any document
author can set on any document (via SiYuan's standard "custom
attributes"/bookmark UI), and documents carrying them routinely reach
other users through sharing, sync, or import. A document crafted with a
double quote in any of these fields breaks out of the
aria-labelattribute and injects arbitrary HTML attributes, including inline event
handlers such as
onmouseover, onto the list item. Given every SiYuanBrowserWindowruns withnodeIntegration:true,contextIsolation:false, and no CSP, an injected handler has immediaterequire("child_process")access. The trigger requires only that thevictim open the "move to"/path-picker dialog and hover their mouse over
the malicious document's entry while browsing, no click, no
confirmation. This is CWE-79 (Cross-Site Scripting), specifically an
attribute-context injection, escalating to CWE-94 (Code Injection) via
the Electron configuration.
Details
app/src/util/pathName.ts, insidegetLeaf()(used by the dialog builtin
movePathTo()), around line 711:item.bookmark,item.name1(alternate/renamed title),item.alias,and
item.memoare interpolated raw, directly inside the double-quotedaria-label="..."attribute, with no HTML-escaping of any kind. Onlythe displayed title text (via
getDocDisplayName(..., true), note thetruethird argument requesting escaping) is safe; the tooltip contentbuilt around it is not.
Confirmed as a real, specific gap, not a deliberate design choice,
by direct comparison with the desktop main file dock's implementation
of the identical feature,
app/src/layout/dock/Files.ts:1554:Here, every one of the same four fields is correctly wrapped in
escapeMethod(). This is the exact same tooltip content, for the exactsame file-tree feature, in a different file, correctly escaped in one
and not the other, direct proof the omission in
pathName.tsis a gaprather than an intentional exemption.
Step-by-step reproduction
sharing, sync, import, or a received workspace/notebook), set the
"Memo" custom attribute (or Bookmark, or Alias) to a payload
containing a double quote to break out of the
aria-labelattribute,for example:
send an exported
.sy/import bundle, or have it present via sync).opens the path-picker/"move to" dialog for a notebook containing this
document, for example right-clicking a document and choosing "Move
to," or using a reference/link "move to path" action, then expanding
the folder containing the crafted document in the resulting tree.
list entry (a completely ordinary action while browsing the tree to
pick a destination), the browser evaluates the injected
onmouseoverattribute and executes the payload.nodeIntegration:true/contextIsolation:falseconfiguration,require("child_process")succeeds, achieving arbitrary OS command execution at the victim's
privilege level.
(Not run in a live Electron instance in this review, static analysis
of the TypeScript source only; both the vulnerable and the correctly-
fixed sibling implementation are read directly and quoted in full
above, and the "move to" dialog wiring (
getLeafcalled from the.b3-list-item__toggleclick handler insidemovePathTo's dialog) istraced directly from source to confirm this is a live, reachable code
path, not dead code.)
Impact
Any SiYuan user who opens a path-picker dialog (an extremely routine
action, used for moving documents, and other path-selection flows) on a
notebook containing a document with attacker-crafted bookmark, alias,
memo, or alternate-name metadata achieves code execution on the victim's
machine the instant their mouse passes over that document's entry in the
tree, with no click and no other confirmation required. Since these four
metadata fields are ordinary document attributes any author can set,
and documents routinely arrive via sharing, sync, or import from other
parties, this is a low-friction, high-impact stored XSS-to-RCE chain
distinct in mechanism (attribute-context injection via a missing
escapeMethod()call) from the other findings in this review round.