Skip to content

Stored XSS results to Electron RCE in SiYuan marketplace via unescaped `data-obj` attribute (incomplete fix for GHSA-27qc-m5gf-jv5r) / Bypass for CVE-2026-45375's patch

Critical
88250 published GHSA-x88j-wgpr-h22x Jun 14, 2026

Package

gomod github.com/siyuan-note/siyuan/kernel (Go)

Affected versions

<= v3.6.5

Patched versions

v3.7.0

Description

Summary

The fix for GHSA-27qc-m5gf-jv5r ("Bazaar marketplace stored XSS") is incomplete. It HTML-escapes the
visible text fields of a marketplace package (name, version, author, description) when they are
rendered into text nodes, but it does not escape the same untrusted fields when they are serialized into
the data-obj HTML attribute of each marketplace card. Because the attribute is single-quoted and the
value is produced with JSON.stringify() (which does not escape ', <, or >), a package whose name
contains a single quote breaks out of the attribute and injects arbitrary HTML. In the desktop client the
main BrowserWindow runs with nodeIntegration: true, contextIsolation: false, so the injected markup
escalates from DOM XSS to arbitrary OS command execution.

This is the same root cause and same impact as the original advisory, reached through a sibling sink the
patch did not cover. It is exploitable on the current patched code (dev branch / v3.7.0).

Root cause

Marketplace cards are built by bazaar.ts and inserted with innerHTML (e.g. bazaar.ts:476,
:1084, :1123, :1148). Each card serializes raw package fields into a single-quoted data-obj
attribute. Representative code (bazaar.ts:262-272, dev):

const dataObj = {
    bazaarType,
    themeMode: themeMode,
    updated: item.updated,
    name: item.name,        // RAW package.json name attacker controlled
    repoURL: item.repoURL,  // RAW
    repoHash: item.repoHash,
    downloads: item.downloads,
    downloaded: false,
};
return `<div data-obj='${JSON.stringify(dataObj)}' class="b3-card...">
    ...
        ${escapeHtml(item.preferredName)}   // text node correctly escaped by the patch
    ...`;

Two things which make this exploitable:

  1. The patch only escaped the text-node sinks. escapeHtml (app/src/util/escape.ts) escapes &
    and < and is applied to ${escapeHtml(item.preferredName)} etc., but it is never applied to
    data-obj. JSON.stringify escapes " but leaves ', <, > intact, and the attribute uses
    single quotes so a ' in name terminates the attribute and following markup escapes the element.

  2. The kernel deliberately un-escapes these fields before sending them to the UI. Commit
    30cdde531 ("HTML escaping no longer affects the display and search of marketplace packages") removed
    the server-side escaping introduced by the original fix and added unescapePackageDisplayStrings
    (kernel/bazaar/package.go), called from getStageIndex (kernel/bazaar/stage.go), which runs
    html.UnescapeString over Name, Author, Version, etc. Local packages are likewise no longer
    escaped in ParsePackageJSON. The raw '/</> therefore reach the data-obj attribute on the
    patched build.

Additional data-obj sinks with the same flaw: bazaar.ts:308-321 (_genUpdateItemHTML),
:415-430 (plugin list), :511-517 (_genReadmeHTML detail/side panel). repoURL and repoHash
are injected raw alongside name and are equally abusable.

Impact

Any user who merely views the affected package in the marketplace (Settings → Marketplace
browse list, "Downloaded" list, update list, or the package detail panel) parses the injected HTML.
No click beyond viewing the list is required (the payload uses <img onerror> which fires on parse).
In the Electron desktop client (nodeIntegration:true, contextIsolation:false, webSecurity:false)
the handler can call require('child_process').exec(...) for arbitrary command execution with the
user's privileges. Persists across restarts and sync.

Delivery vectors: a malicious package published to the community bazaar, or a crafted local package
manifest (installed plugin/theme/template/widget/icon).

Proof of concept

Malicious plugin.json (or theme/template/widget/icon manifest)

{
  "name": "x'><img src=x onerror=alert(document.domain)>",
  "author": "attacker",
  "url": "https://github.com/attacker/evil-plugin",
  "version": "1.0.0",
  "displayName": { "default": "Cool Plugin" },
  "description": { "default": "totally legit" }
}

For real RCE in the desktop client, swap the handler (keep it quote-free so JSON.stringify cannot
corrupt it), e.g.:

x'><img src=x onerror=top.require(String.fromCharCode(99,104,105,108,100,95,112,114,111,99,101,115,115)).exec('calc')>

Resulting DOM (card inserted via innerHTML)

<div data-obj='{"bazaarType":"plugins",...,"name":"x'><img src=x onerror=alert(document.domain)>",...}' class="b3-card">

The ' after name":"x closes data-obj, > closes the <div>, and <img onerror> becomes a live
sibling node and executes.

Reproduction harness (verified)

A standalone harness using the verbatim escapeHtml/escapeAttr from app/src/util/escape.ts
and the verbatim card template from bazaar.ts:262-306, inserting the card via innerHTML exactly
as bazaar.ts:476 does, was loaded in headless Chromium 147. Entering the payload
x'><img src=x onerror=alert(document.domain)> as the package name:

alert() fired: true
alert() message (document.domain): "127.0.0.1"
VERDICT: XSS TRIGGERED

Control test: the identical payload routed only through the patched escapeHtml text-node sink (with
the data-obj attribute removed) does NOT execute confirming the data-obj attribute is the
specific unfixed sink.

Remediation

Escape the serialized object before placing it in the attribute, in every card builder
(bazaar.ts ~272, ~321, ~430, ~517):

// single-quoted attribute -> escape at least the single quote (and ideally < >):
`<div data-obj='${escapeAttr(JSON.stringify(dataObj))}' class="b3-card...">`

or switch to a double-quoted attribute with "-escaping, or store the object out-of-band
(e.g. a WeakMap keyed by element) instead of in an HTML attribute. Escaping the visible text fields
alone is insufficient because the same untrusted fields are also serialized into data-obj.

Severity

Critical

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
Required
Scope
Changed
Confidentiality
High
Integrity
High
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:H

CVE ID

CVE-2026-55570

Weaknesses

Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')

The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users. Learn more on MITRE.

Improper Control of Generation of Code ('Code Injection')

The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment. Learn more on MITRE.

Improper Encoding or Escaping of Output

The product prepares a structured message for communication with another component, but encoding or escaping of the data is either missing or done incorrectly. As a result, the intended structure of the message is not preserved. Learn more on MITRE.

Credits