CVE-2026-54070: SiYuan: XSS in Bazaar README steals admin API token

GHSA-w7cg-whh7-xp28 HIGH
Published July 10, 2026
CISO Take

SiYuan's plugin marketplace renders third-party README files to HTML using a sanitizer that blocks a legacy list of event-handler attributes but misses modern ones like onpointerover, onfocusin, and onbeforetoggle, and the frontend inserts that HTML directly into the admin console with no client-side sanitization, CSP, or frame isolation. Any community package author — no install required, just a marketplace browse and one hover or click by an Administrator who has accepted the one-time marketplace trust prompt — can run JavaScript in the authenticated SiYuan origin, steal the kernel API token, and pivot to full administrator control including plugin installation and container shell access. This isn't in CISA's KEV list, has no public exploit or Nuclei template, and its raw EPSS score (0.0018) is low, though it still sits in the top 92nd percentile for exploitation likelihood, and the high attack complexity (AC:H) mostly reflects the research needed to find the unlisted handlers rather than the difficulty of using them once known. With 488 downstream dependents and a plugin ecosystem that assumes a baseline of author trust, this is a supply-chain-shaped XSS: a single malicious or compromised Bazaar package compromises every instance whose administrator views its listing. Patch to the fixed commit (module version >= 0.0.0-20260628153353-2d5d72223df4), and until then set bazaar.trust=false and avoid browsing community packages from unverified authors.

Sources: NVD GitHub Advisory EPSS CISA KEV OpenSSF ATLAS

What is the risk?

CVSS 7.1 (High) with a confidentiality/integrity-heavy profile (C:H/I:H/A:L) reflects the real outcome — full administrator API token theft and downstream kernel control — but exploitation requires two gating conditions: the Administrator must have opted into marketplace trust (bazaar.trust, default false) and must interact (hover/click/focus) with the rendered README. That user-interaction and trust-consent requirement, plus the absence of KEV listing, public exploit code, or a Nuclei template, keeps near-term mass-exploitation likelihood low despite the low absolute EPSS score (0.0018). The real risk driver is the attack surface shape: it takes only one malicious or compromised community package to compromise any instance that browses its listing, so risk scales with the size and openness of the Bazaar ecosystem rather than with attacker sophistication — once the unlisted event handlers are public (as they now are via this advisory), the exploit itself is trivial to reproduce.

How does the attack unfold?

Malicious package staging
Attacker publishes a new Bazaar plugin (or updates a trusted one) whose README.md embeds an inline event handler absent from lute's sanitizer denylist, such as onpointerover.
AML.T0011.001
Marketplace delivery
An Administrator who has accepted the one-time marketplace trust consent opens Settings → Marketplace and views the package listing or an installed plugin's README, which is rendered unsanitized into the admin DOM.
User execution
The Administrator hovers, clicks, or focuses the rendered element, firing the inline handler and executing attacker JavaScript in the fully authenticated SiYuan origin.
AML.T0011
Token theft and kernel takeover
The script exfiltrates the kernel API token and the attacker replays it against admin APIs like installBazaarPlugin to gain full, persistent control of the workspace and its container.
AML.T0091.000

What systems are affected?

Package Ecosystem Vulnerable Range Patched
Panel go < 0.0.0-20260628153353-2d5d72223df4 0.0.0-20260628153353-2d5d72223df4
5.7K OpenSSF 6.6 488 dependents Pushed 8d ago 61% patched ~10d to patch Full package profile →

Do you use Panel? You're affected.

How severe is it?

CVSS 3.1
7.1 / 10
EPSS
0.2%
chance of exploitation in 30 days
Higher than 8% of all CVEs
Exploitation Status
No known exploitation
Sophistication
Moderate

What is the attack surface?

AV AC PR UI S C I A
AV Network
AC High
PR None
UI Required
S Unchanged
C High
I High
A Low

What should I do?

1 step
  1. Upgrade SiYuan kernel to the version incorporating the Go module fix (>= 0.0.0-20260628153353-2d5d72223df4); track the vendor advisory GHSA-w7cg-whh7-xp28 for the official release tag. Until patched: set bazaar.trust to false (or leave it at its default) to disable marketplace README rendering, and avoid opening or hovering over listings from unverified Bazaar authors, including already-installed plugins pending update. As defense-in-depth regardless of patch status, add CSP, X-Frame-Options, and X-Content-Type-Options headers at the kernel or a reverse-proxy in front of it, and apply DOMPurify client-side before any innerHTML assignment in app/src/config/bazaar.ts. For detection, monitor kernel API logs for installBazaarPlugin or other admin-API calls immediately following a Bazaar browse session from the same token, and audit whether the upstream lute sanitizer (render/sanitizer.go eventAttrs list) has been patched to an allowlist model rather than a denylist.

How is it classified?

Which compliance frameworks are affected?

This CVE is relevant to:

EU AI Act
Article 15 - Accuracy, robustness and cybersecurity
ISO 42001
A.6.2 - AI system security
NIST AI RMF
GOVERN 6.1 - Policies address risks from third-party AI system components
OWASP LLM Top 10
LLM05 - Supply Chain Vulnerabilities

Frequently Asked Questions

What is CVE-2026-54070?

SiYuan's plugin marketplace renders third-party README files to HTML using a sanitizer that blocks a legacy list of event-handler attributes but misses modern ones like onpointerover, onfocusin, and onbeforetoggle, and the frontend inserts that HTML directly into the admin console with no client-side sanitization, CSP, or frame isolation. Any community package author — no install required, just a marketplace browse and one hover or click by an Administrator who has accepted the one-time marketplace trust prompt — can run JavaScript in the authenticated SiYuan origin, steal the kernel API token, and pivot to full administrator control including plugin installation and container shell access. This isn't in CISA's KEV list, has no public exploit or Nuclei template, and its raw EPSS score (0.0018) is low, though it still sits in the top 92nd percentile for exploitation likelihood, and the high attack complexity (AC:H) mostly reflects the research needed to find the unlisted handlers rather than the difficulty of using them once known. With 488 downstream dependents and a plugin ecosystem that assumes a baseline of author trust, this is a supply-chain-shaped XSS: a single malicious or compromised Bazaar package compromises every instance whose administrator views its listing. Patch to the fixed commit (module version >= 0.0.0-20260628153353-2d5d72223df4), and until then set bazaar.trust=false and avoid browsing community packages from unverified authors.

Is CVE-2026-54070 actively exploited?

No confirmed active exploitation of CVE-2026-54070 has been reported, but organizations should still patch proactively.

How to fix CVE-2026-54070?

Upgrade SiYuan kernel to the version incorporating the Go module fix (>= 0.0.0-20260628153353-2d5d72223df4); track the vendor advisory GHSA-w7cg-whh7-xp28 for the official release tag. Until patched: set bazaar.trust to false (or leave it at its default) to disable marketplace README rendering, and avoid opening or hovering over listings from unverified Bazaar authors, including already-installed plugins pending update. As defense-in-depth regardless of patch status, add CSP, X-Frame-Options, and X-Content-Type-Options headers at the kernel or a reverse-proxy in front of it, and apply DOMPurify client-side before any innerHTML assignment in app/src/config/bazaar.ts. For detection, monitor kernel API logs for installBazaarPlugin or other admin-API calls immediately following a Bazaar browse session from the same token, and audit whether the upstream lute sanitizer (render/sanitizer.go eventAttrs list) has been patched to an allowlist model rather than a denylist.

What systems are affected by CVE-2026-54070?

This vulnerability affects the following AI/ML architecture patterns: plugin / extension marketplaces, AI-powered knowledge-management UI, admin/management consoles.

What is the CVSS score for CVE-2026-54070?

CVE-2026-54070 has a CVSS v3.1 base score of 7.1 (HIGH). The EPSS exploitation probability is 0.18%.

What is the AI security impact?

Affected AI Architectures

plugin / extension marketplacesAI-powered knowledge-management UIadmin/management consoles

MITRE ATLAS Techniques

AML.T0010.001 AI Software
AML.T0011 User Execution
AML.T0011.001 Malicious Package

Compliance Controls Affected

EU AI Act: Article 15
ISO 42001: A.6.2
NIST AI RMF: GOVERN 6.1
OWASP LLM Top 10: LLM05

What are the technical details?

Original Advisory

## Summary `renderPackageREADME` in `kernel/bazaar/readme.go` renders a Bazaar package README from Markdown to HTML with the lute engine and `SetSanitize(true)`. The lute sanitizer is an event-handler blocklist: `allowAttr` rejects only attribute names present in a fixed `eventAttrs` map copied from the w3schools legacy handler list. That map omits modern event handlers. `onpointerover`, `onpointerdown`, `onauxclick`, `onbeforetoggle`, `onfocusin`, `onanimationstart`, and `ontransitionend` are not in the list, so the sanitizer passes them through verbatim on any tag. The frontend assigns the rendered HTML to `mdElement.innerHTML` in `app/src/config/bazaar.ts` with no client-side DOMPurify on this path, into a normal element in the main document (no iframe, no sandbox). The kernel sends no Content-Security-Policy, X-Frame-Options, or X-Content-Type-Options header on any response, so an inline handler runs when its event fires. The README is rendered when an Administrator opens a package in Settings → Marketplace, after the one-time marketplace trust consent. Install is not required. Result: a third-party Bazaar package author runs JavaScript in the Administrator's authenticated SiYuan origin when the Administrator views and interacts with the package listing, and gains full control of the workspace. ## Affected siyuan-note/siyuan, `<= 3.6.5` (latest release, 2026-04-21). Confirmed live-exploitable on the `b3log/siyuan:v3.6.5` image; identical code on `master` HEAD. Condition: the Administrator has accepted the marketplace trust consent (`bazaar.trust`, default false) and browses community Bazaar packages. The lute dependency pin is `github.com/88250/lute v1.7.7-0.20260419134724-bb68012f231d`. Both the online browse path (`getBazaarPackageREADME`) and the installed-package path (`getInstalledPlugin`) reach the same sink. ## Root cause `render/sanitizer.go:225-232` (lute): `allowAttr(name)` returns false only when `name` exists in the `eventAttrs` map, an attribute denylist rather than an allowlist. `render/sanitizer.go:235-334` (lute): `eventAttrs` is the w3schools handler list and contains no pointer, beforetoggle, focusin, animation, or transition handlers. `kernel/bazaar/readme.go:108-118`: `renderPackageREADME` builds the engine with `SetSanitize(true)` and returns the HTML string to the caller. `kernel/bazaar/readme.go:48-88`: `GetBazaarPackageREADME` renders an untrusted remote package README; `kernel/api/bazaar.go` exposes it at `/api/bazaar/getBazaarPackageREADME` (`router.go:423`, `CheckAuth`). `app/src/config/bazaar.ts:600` and `:609`: `mdElement.innerHTML = data.preferredReadme` / `= response.data.html`, no DOMPurify, target is a plain div. Kernel HTTP responses carry no CSP/X-Frame-Options/X-Content-Type-Options header (live-confirmed), so an inline handler is not blocked. ## Reproduction `b3log/siyuan:v3.6.5` Docker, default config, access auth code set, marketplace trust accepted. 1. Place a package whose README carries a non-blocklisted handler (an online community package produces the identical render at browse time): ``` mkdir -p workspace/data/plugins/evil-plugin cat > workspace/data/plugins/evil-plugin/plugin.json <<'JSON' {"name":"evil-plugin","author":"x","version":"1.0.0","minAppVersion":"3.0.0", "displayName":{"default":"Evil"},"description":{"default":"poc"}, "readme":{"default":"README.md"},"backends":["all"],"frontends":["all"]} JSON printf '<div onpointerover="alert(document.domain)">plugin description</div>\n' \ > workspace/data/plugins/evil-plugin/README.md ``` 2. Request the rendered README the way the Marketplace panel does: ``` curl -s -X POST http://127.0.0.1:6806/api/bazaar/getInstalledPlugin \ -H "Authorization: Token <API-TOKEN>" -H "Content-Type: application/json" \ -d '{"frontend":"all","keyword":""}' ``` Response `data.packages[].preferredReadme` contains the handler verbatim: ``` <div onpointerover="alert(document.domain)">plugin description</div> ``` A control `<img src=x onerror=...>` in the same README is returned HTML-escaped and inert. 3. In Settings → Marketplace, open the package and move the pointer over its README. Live-verified: the rendered HTML is assigned to `mdElement.innerHTML` (no CSP, no sandbox) and the `onpointerover` handler executes `alert(document.domain)` in the SiYuan origin on hover. Handlers do not auto-fire on insertion; one pointer/focus/click interaction on the listing triggers them. ## Impact - JavaScript execution in the Administrator's authenticated origin on a marketplace package view plus one hover/click/focus, no install needed. - Theft of the kernel API token (`conf.api.token`), which grants full Administrator API access. - Pivot to `installBazaarPlugin` and kernel control; the runtime image ships a shell. - A single malicious community package reaches every instance that views its listing. ## Credit Jan Kahmen, [turingpoint](https://turingpoint.de) (jan@turingpoint.de)

Exploitation Scenario

An adversary publishes a new Bazaar plugin — or pushes a malicious update to an already-trusted one — whose README.md embeds an inline handler such as `<div onpointerover="...">` that isn't present in lute's outdated event-handler denylist. An Administrator who has previously accepted the marketplace trust consent opens Settings → Marketplace to browse or review the package; the moment they move the pointer over, click, or tab-focus the rendered README element, the handler fires and attacker JavaScript executes in the fully authenticated SiYuan origin. The script reads the kernel API token from the local config, exfiltrates it to an attacker-controlled endpoint, and the attacker replays it against /api/bazaar/installBazaarPlugin and other admin APIs — including installing further plugins — to gain persistent, full workspace and container control, all without the Administrator ever installing the malicious package.

Weaknesses (CWE)

CWE-184 — Incomplete List of Disallowed Inputs: The product implements a protection mechanism that relies on a list of inputs (or properties of inputs) that are not allowed by policy or otherwise require other action to neutralize before additional processing takes place, but the list is incomplete.

  • [Implementation] Do not rely exclusively on detecting disallowed inputs. There are too many variants to encode a character, especially when different environments are used, so there is a high likelihood of missing some variants. Only use detection of disallowed inputs as a mechanism for detecting suspicious activity. Ensure that you are using other protection mechanisms that only identify "good" input - such as lists of allowed inputs - and ensure that you are properly encoding your outputs.

Source: MITRE CWE corpus.

CVSS Vector

CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:L

Timeline

Published
July 10, 2026
Last Modified
July 10, 2026
First Seen
July 10, 2026

Related Vulnerabilities