critical · 9.9CVE-2026-54067Jul 10, 2026

CVE-2026-54067: SiYuan Stored XSS to RCE via CSS Snippet Style Tag Breakout

Rohit Hatagale
AI Security Researcher, SecureLayer7

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…

Packagegithub.com/siyuan-note/siyuan/kernel
Ecosystemgo
Affected< 0.0.0-20260628153353-2d5d72223df4
Fixed in0.0.0-20260628153353-2d5d72223df4

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.

bash
# 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.

Reporter not attributed.

References: [1][2][3]

Related research