# SiYuan: Stored XSS to RCE via Unsanitized Attribute View Asset Cell Content

Stored XSS in SiYuan's Attribute View asset cell renderer executes on page load or hover, escalating to RCE in the Electron client via nodeIntegration.

- Payout: Research
- Severity: critical
- Program: SiYuan
- Found by: Yunkaiwjs
- Disclosed: 2026-07-10
- CVE: CVE-2026-50551
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-56mp-4f3v-fgj2
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-56mp-4f3v-fgj2
- Tags: area:os domain:desktop impact:data-leak impact:rce kind:research platform:ghsa sev:critical tech:electron technique:chained technique:incomplete-fix technique:source-review vuln:rce vuln:xss

**Target:** Electron desktop client Attribute View (database) asset cell renderer
**Root cause:** AV asset cell renderers concatenate raw item.name/item.content into innerHTML and use escapeAttr() (quotes only) for aria-label, which survives the attribute->getAttribute->innerHTML round-trip; Electron runs with nodeIntegration:true and contextIsolation:false.
**Steps:**
1. Open SiYuan desktop v3.6.5, create a document, insert an Attribute View table.
2. Add a column of type Asset, click the asset cell, choose Add Link.
3. Paste `<img src=x onerror=alert(1)>` into the Link textarea, leave Title benign, click outside to save.
4. Alert fires immediately via Sink 1 (`cell.ts:1008` raw `>${item.name || item.content}</span>`); hovering the cell also fires via Sink 2 (aria-label -> `showTooltip(decodeURIComponent(tip))` -> `innerHTML`).
5. For RCE use `<img src=x onerror=require('child_process').exec('calc')>`.
**How found:** Noticed the -44588 fix used escapeAriaLabel() (double-escapes `<`) while neighboring AV asset renderers still used the weaker escapeAttr() or no escaping; traced the aria-label -> getAttribute -> showTooltip -> innerHTML round-trip and the raw text concatenation in cell.ts/blockAttr.ts.
**Exploitation:** Attacker crafts a note with an AV asset cell containing an HTML/JS payload; kernel stores MAsset.Content raw, so on victim open the renderer's innerHTML assignment executes the payload, and with nodeIntegration:true the JS calls require('child_process') for arbitrary command execution. Sync/collaboration propagates the note to all users; payload persists in .sy files and re-executes on every open.
**Escalation:** XSS in renderer chained to full RCE and filesystem/credential access because Electron disables context isolation and enables Node integration.
**Insight:** When a project patches an XSS by hardening one escape helper, grep for sibling sinks still using the weaker helper or raw interpolation — incomplete fixes cluster around the same renderer, and aria-label round-trips through getAttribute/innerHTML need double-escaping.

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

**XSS payload in AV asset link field**

```
<img src=x onerror=alert(document.domain)>

  For RCE in Electron desktop:

      <img src=x onerror=require('child_process').exec('calc')>
```

**Unescaped asset cell renderer sink**

```
html += `<img loading="lazy" class="av__cellassetimg ariaLabel" aria-label="${item.content}" src="${getCompressURL(item.content)}">`;
```

