# SiYuan: Remote Code Execution in the Electron desktop client via stored XSS in synced table captions

Unescaped table caption in SiYuan notes becomes stored XSS; Electron renderer with nodeIntegration enabled turns it into RCE on sync.

- Payout: Research
- Severity: critical
- Program: SiYuan
- Found by: ngocnn97
- Disclosed: 2026-04-08
- CVE: CVE-2026-39846
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-phhp-9rm9-6gr2
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-phhp-9rm9-6gr2
- Tags: area:os domain:desktop impact:integrity impact:rce kind:research platform:ghsa sev:critical tech:electron technique:source-review vuln:misconfiguration vuln:rce vuln:xss

**Target:** SiYuan Electron desktop client note renderer / sync
**Root cause:** Table caption attribute is saved into node IAL properties without HTML escaping, then HTML-unescaped and written directly into the rendered DOM, creating a stored XSS sink.
**Steps:**
1. Craft a `.sy.zip` note containing a table block whose `caption` property holds `<img src=x onerror=require('child_process').exec('calc')>`.
2. On attacker Client A: `Import -> SiYuan .sy.zip`.
3. Trigger sync on Client A to upload the malicious note to the shared sync target.
4. On victim Client B, trigger sync to download the note.
5. Open the synced note on Client B; the caption is unescaped into DOM and the onerror handler fires.
**How found:** Researcher inspected the table caption handling path, noticed caption saved to IAL without escaping and later HTML-unescaped into output DOM, then confirmed a crafted encoded caption rendered as a live DOM element; also determined .sy.zip import into a synced workspace was the practical delivery path over Markdown import.
**Exploitation:** Attacker imports a crafted .sy.zip into a synced workspace and syncs it to the shared target; victim syncs and opens the note, the unescaped caption renders as live HTML, and because the Electron renderer has nodeIntegration:true and contextIsolation:false, the onerror payload calls require('child_process').exec('calc') executing code as the logged-in user.
**Escalation:** Browser-only XSS escalated to full RCE by abusing Electron renderer config (nodeIntegration enabled, contextIsolation disabled).
**Insight:** Table/block caption and other IAL metadata fields are often escaped on input but unescaped on render — check every attribute that round-trips through HTML-unescape into the DOM, and in Electron targets verify nodeIntegration/contextIsolation to gauge XSS-to-RCE.

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

**RCE payload in table caption**

```
&lt;img src=x onerror=require('child_process').exec('calc')&gt;
```

**Safe XSS validation payload**

```
&lt;img src=x onerror=alert('caption-xss')&gt;
```

