Skip to content

Attribute-breakout XSS-to-RCE via unescaped document bookmark/alias/memo/name fields in the "move/link to" file-tree picker's tooltip (app/src/util/pathName.ts), triggered by hovering

High
88250 published GHSA-jjq3-3942-x99r Aug 4, 2026

Package

npm siyuan (npm)

Affected versions

3.7.3

Patched versions

v3.7.4

Description

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

  1. 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')
    
  2. Get this document into the victim's workspace (share the notebook,
    send an exported .sy/import bundle, or have it present via sync).
  3. 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.
  4. 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.
  5. 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.

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Local
Attack complexity
Low
Privileges required
None
User interaction
Required
Scope
Changed
Confidentiality
High
Integrity
High
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H

CVE ID

No known CVE

Weaknesses

Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')

The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users. Learn more on MITRE.

Improper Control of Generation of Code ('Code Injection')

The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment. Learn more on MITRE.

Credits