CVE-2026-54067: SiYuan Stored XSS to RCE via CSS Snippet Style Tag Breakout
A malicious CSS snippet containing </style> breaks out of its wrapping style tag in SiYuan's snippet renderer, injecting arbitrary HTML and JavaScript that escalates to full remote code execution on E
The problem
SiYuan's `renderSnippet()` function in `app/src/config/util/snippets.ts` builds a `<style>` tag by interpolating raw snippet content via `insertAdjacentHTML`. No escaping or validation is applied to the `content` field anywhere in the write or read path.
The backend handler at `kernel/api/snippet.go` copies the `content` value from the request body straight into storage. Because Electron windows are created with `nodeIntegration:true`, `contextIsolation:false`, and `webSecurity:false`, any JavaScript that executes in the renderer has direct access to Node.js APIs.
Snippets also sync across all peers via the workspace repository, so a single planted payload runs on every device that pulls the workspace.
Proof of concept
A working proof-of-concept for CVE-2026-54067 in github.com/siyuan-note/siyuan/kernel, with the exact payload below.
# 1. Plant the malicious CSS snippet
curl -X POST http://localhost:16806/api/snippet/setSnippet \
-H "Content-Type: application/json" \
-H "Authorization: Token $TOKEN" \
-d '{"snippets":[{"id":"","name":"poc","type":"css","enabled":true,"content":"</style><img src=x onerror=\"document.title=\\\"SIYUAN_XSS\\\";window.__siyuan_xss=true\">"}]}'
# 2. Verify in DevTools after app boot or reloadSnippet WebSocket event
# ({ markerFired: window.__siyuan_xss === true })
# 3. RCE escalation on Electron (swap marker for):
# </style><img src=x onerror="require('child_process').execSync('open /Applications/Calculator.app')">The HTML parser closes a `<style>` element the moment it sees the literal four-character sequence `</style>` in its text content. Because `insertAdjacentHTML` invokes the full HTML parser, any snippet body containing that sequence terminates the style block and promotes the trailing markup to a sibling node in `<head>`.
The `<img onerror>` handler then executes arbitrary JavaScript with no user click required.
The JS snippet branch in the same function already avoids this by using `document.createElement('script')` and assigning `el.text`, which is a text-node assignment that never passes through the HTML parser. The CSS branch simply did not apply the same pattern.
The patch mirrors that approach: create the element with `createElement('style')`, assign `el.textContent = item.content`, and `appendChild` it, so `</style>` in the body is treated as literal text, not markup. The root causes are CWE-79 (unsanitized HTML interpolation) and CWE-1188 (insecure Electron defaults that make every renderer XSS an instant RCE).
The fix
Upgrade to SiYuan 3.7.0. The patch replaces the `insertAdjacentHTML` template-literal approach in `app/src/config/util/snippets.ts` with `const el = document.createElement('style'); el.id = id; el.textContent = item.content; document.head.appendChild(el);`. This eliminates HTML parsing of snippet content entirely.
As a defense-in-depth measure, the backend `setSnippet` handler should also reject any `content` value containing `</style>` so older renderer versions pulling from a shared synced workspace remain safe.
Related research
- critical · 9.9CVE-2026-54158CVE-2026-54158: SiYuan Stored XSS to RCE via Unescaped Attribute-View Cell Rendering
- critical · 9.9CVE-2026-50551CVE-2026-50551: SiYuan Stored XSS to RCE via Attribute View Asset Cell
- high · 7.5CVE-2026-54066CVE-2026-54066: SiYuan Unauthenticated Path Traversal via Double URL Encoding in /assets/ (Publish Mode)
- high · 7.1CVE-2026-54070CVE-2026-54070: SiYuan Stored XSS via Bazaar README Event Handler Bypass