# SiYuan: Stored XSS in Bazaar marketplace via package README event handlers

Lute's event-handler denylist omits modern handlers like onpointerover, so a malicious Bazaar package README runs JS in the Administrator's SiYuan origin on hover.

- Payout: Research
- Severity: high
- Program: SiYuan
- Found by: kah-ja
- Disclosed: 2026-07-10
- CVE: CVE-2026-54070
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-w7cg-whh7-xp28
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-w7cg-whh7-xp28
- Tags: area:web domain:web impact:ato impact:integrity impact:privesc kind:research platform:ghsa sev:high tech:go technique:incomplete-fix technique:source-review vuln:xss

**Target:** Bazaar package README rendered into Settings → Marketplace panel (kernel /api/bazaar endpoints)
**Root cause:** lute's sanitizer uses an attribute denylist (eventAttrs copied from the w3schools legacy handler list) instead of an allowlist, so modern handlers (onpointerover, onpointerdown, onauxclick, onbeforetoggle, onfocusin, onanimationstart, ontransitionend) pass through. The frontend assigns the rendered HTML to mdElement.innerHTML with no DOMPurify and no CSP/sandbox.
**Steps:**
1. Create a package dir with plugin.json and a README.md containing `<div onpointerover="alert(document.domain)">plugin description</div>`.
2. POST to `/api/bazaar/getInstalledPlugin` with `Authorization: Token <API-TOKEN>` and body `{"frontend":"all","keyword":""}`; response `data.packages[].preferredReadme` contains the handler verbatim (control `<img src=x onerror=...>` is escaped).
3. Open the package in Settings → Marketplace and hover the README; handler fires in the SiYuan origin.
**How found:** Noticed the sanitizer is a denylist built from the w3schools legacy handler list; checked which modern handlers were missing and confirmed the frontend sink uses innerHTML with no DOMPurify and no CSP headers.
**Exploitation:** A third-party Bazaar package author ships a README with a non-blocklisted event handler. When an Administrator who accepted marketplace trust consent browses the listing (install not required), the handler executes on one pointer/focus/click interaction, yielding JS in the authenticated origin; attacker steals conf.api.token for full Administrator API access and can pivot to installBazaarPlugin and kernel control.
**Escalation:** XSS → theft of kernel API token (conf.api.token) → full Administrator API access → installBazaarPlugin → kernel control (image ships a shell).
**Insight:** When a sanitizer blocks event handlers by name, diff its list against the full modern DOM event set (pointer, beforetoggle, focusin, animation, transition) — any omission is a live XSS if the sink is innerHTML without CSP.

## Proof of concept (copied exactly from the original report)

**Malicious plugin README with onpointerover 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
```

**curl request to getInstalledPlugin returning unsanitized handler**

```
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":""}'
```

