CVE-2026-54070: SiYuan Stored XSS via Bazaar README Event Handler Bypass
A malicious SiYuan marketplace package can embed modern HTML event handlers in its README that bypass the Lute sanitizer and run JavaScript in an Administrator's browser when they view the package lis
The problem
SiYuan's `renderPackageREADME` (kernel/bazaar/readme.go) converts package READMEs from Markdown to HTML using the lute engine with `SetSanitize(true)`. The lute sanitizer's `allowAttr` function uses a denylist approach: it only rejects attribute names that appear in a hardcoded `eventAttrs` map copied from the w3schools legacy handler list.
That map does not include modern Pointer Events, Focus Events, Animation, or Transition handlers. Attributes like `onpointerover`, `onpointerdown`, `onauxclick`, `onbeforetoggle`, `onfocusin`, `onanimationstart`, and `ontransitionend` pass through untouched. The frontend then assigns the rendered HTML directly to `mdElement.innerHTML` with no DOMPurify pass, inside a plain div in the main document.
No CSP header is sent, so the injected handler fires on the first matching user interaction.
Proof of concept
A working proof-of-concept for CVE-2026-54070 in github.com/siyuan-note/siyuan/kernel, with the exact payload below.
<!-- README.md for a malicious Bazaar package -->
<div onpointerover="alert(document.domain)">plugin description</div>
<!-- Kernel response for getInstalledPlugin / getBazaarPackageREADME
returns the handler verbatim in data.packages[].preferredReadme:
<div onpointerover="alert(document.domain)">plugin description</div>
Classic onerror is blocked; onpointerover is NOT in eventAttrs and passes through.
Moving the pointer over the README div triggers JS in the SiYuan origin. -->
<!-- Minimal curl to confirm the raw output -->
<!-- POST /api/bazaar/getInstalledPlugin
Authorization: Token <API-TOKEN>
{"frontend":"all","keyword":""} -->The root cause is CWE-184 (Incomplete List of Disallowed Inputs). Lute's `allowAttr` in `render/sanitizer.go` returns `false` only when the attribute name exists in `eventAttrs`, a fixed map derived from the legacy w3schools handler list. That list predates the Pointer Events, FocusEvent bubbling, Web Animations, and CSS Transitions APIs, so `onpointerover` and its siblings are simply absent.
The fix in 3.7.0 replaces the denylist approach. Rather than trying to enumerate every current and future event attribute, the patched sanitizer switches to an allowlist model: only explicitly permitted, non-executable attributes survive. A `strings.HasPrefix(name, "on")` guard is the minimal additional check that would have caught this class of bypass entirely.
The missing Content-Security-Policy on kernel responses is a contributing factor: even a default-src CSP would have blocked the inline handler without any sanitizer changes.
The fix
Upgrade SiYuan to 3.7.0 or later (Go module pseudo-version >= 0.0.0-20260628153353-2d5d72223df4). The patched release replaces the lute event-attribute denylist with an allowlist-based approach so unlisted `on*` attributes are rejected by default. As a defense-in-depth measure, add a `Content-Security-Policy: default-src 'self'` header on all kernel HTTP responses to neutralize any future inline-handler bypasses.
Reported by Jan Kahmen (turingpoint).
Related research
- critical · 9.9CVE-2026-50551CVE-2026-50551: SiYuan Stored XSS to RCE via Attribute View Asset Cell
- critical · 9.9CVE-2026-54067CVE-2026-54067: SiYuan Stored XSS to RCE via CSS Snippet Style Tag Breakout
- critical · 9.9CVE-2026-54158CVE-2026-54158: SiYuan Stored XSS to RCE via Unescaped Attribute-View Cell Rendering
- high · 7.5CVE-2026-54066CVE-2026-54066: SiYuan Unauthenticated Path Traversal via Double URL Encoding in /assets/ (Publish Mode)