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.
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?
What systems are affected?
| Package | Ecosystem | Vulnerable Range | Patched |
|---|---|---|---|
| Panel | go | < 0.0.0-20260628153353-2d5d72223df4 | 0.0.0-20260628153353-2d5d72223df4 |
Do you use Panel? You're affected.
How severe is it?
What is the attack surface?
What should I do?
1 step-
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:
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
MITRE ATLAS Techniques
AML.T0010.001 AI Software AML.T0011 User Execution AML.T0011.001 Malicious Package Compliance Controls Affected
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
Primary
CWE-79 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
Primary
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 References
Timeline
Related Vulnerabilities
CVE-2024-13152 10.0 Mobuy Panel: SQLi allows unauthenticated DB takeover
Same package: panel CVE-2026-47744 9.9 Shopper: RBAC bypass allows full admin takeover
Same package: panel CVE-2026-54158 9.9 SiYuan: XSS→RCE via workspace sync in Electron app
Same package: panel CVE-2024-13147 9.8 B2B Login Panel: SQLi enables unauthenticated DB access
Same package: panel CVE-2024-5960 9.8 Panel: plaintext credential storage enables domain compromise
Same package: panel