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…
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