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

CVE-2026-50551: SiYuan Stored XSS to RCE via Attribute View Asset Cell

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

A stored XSS flaw in SiYuan's database asset cell renderer lets an attacker plant a malicious link that executes arbitrary code on the victim's desktop the moment they open a shared note.

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

The problem

SiYuan v3.6.5 and earlier render Attribute View asset cell content using escapeAttr(), which only escapes quote characters, leaving < and > untouched. Raw item.name and item.content are also concatenated directly into innerHTML with zero escaping in cell.ts:1008 and blockAttr.ts:93.

The Electron renderer runs with nodeIntegration: true and contextIsolation: false, so any JavaScript that executes there can call require('child_process') directly. A victim opening or even hovering over a poisoned asset cell triggers full RCE. In a sync or collaboration setup the payload propagates to every user automatically via .sy files.

Proof of concept

A working proof-of-concept for CVE-2026-50551 in github.com/siyuan-note/siyuan/kernel, with the exact payload below.

html
<!-- Sink 1: place this in an AV asset cell "Link" field (triggers on page load) -->
<img src=x onerror=alert(document.domain)>

<!-- RCE variant on Electron desktop -->
<img src=x onerror=require('child_process').exec('calc')>

<!-- Sink 2: hover-triggered via aria-label round-trip -->
<!-- escapeAttr() leaves < > intact, so the same payload in the Link field
     also lands in aria-label; popover.ts reads it via getAttribute,
     passes it through decodeURIComponent, then assigns to innerHTML -->
<img src=x onerror=require('child_process').exec('calc')>

The root cause is two distinct sinks in the AV cell renderer. In cell.ts:1008 the template literal closes the attribute and writes >${item.name || item.content}</span> with no escaping at all, so a payload like <img src=x onerror=...> is emitted verbatim into the DOM.

In blockAttr.ts:93 the aria-label attribute uses only escapeAttr() (escapes " and ', not < or >), and the text content is also unescaped.

The hover sink works because popover.ts reads aria-label via getAttribute (which decodes HTML entities), runs the value through decodeURIComponent, then sets messageElement.innerHTML, completing a classic attribute-to-innerHTML round-trip. The patch replaces escapeAttr() with escapeAriaLabel() on every aria-label attribute and wraps all text-content interpolations with escapeHtml(), closing both sinks.

The fix

Upgrade to SiYuan 3.7.0 (commit 2d5d72223df4). The patch replaces escapeAttr() with escapeAriaLabel() for all aria-label attributes in AV cell renderers (cell.ts, blockAttr.ts) and wraps item.name and item.content with escapeHtml() before concatenation into element text content.

Long-term hardening: set contextIsolation: true and nodeIntegration: false in Electron's BrowserWindow options to eliminate the entire XSS-to-RCE escalation path.

Reported by Yunkaiwjs.

References: [1][2][3]

Related research