CVE-2026-54070: SiYuan Cross-Site Scripting Vulnerability

HIGH
Advisory Identifier: GHSA-w7cg-whh7-xp28
CVSS 7.1
CWE-79 CWE-184
Published
Advisory Summary

CVE-2026-54070 is a HIGH (CVSS 7.1) cross-site scripting vulnerability (CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')) affecting SiYuan libraries in the Go ecosystem. The issue involves stored xss in bazaar marketplace via package readme event handlers. Remediation is available by upgrading affected packages to version 0.0.0-20260628153353-2d5d72223df4.

CVE-2026-54070 at a Glance

CVE Identifier CVE-2026-54070
GHSA Identifier GHSA-w7cg-whh7-xp28
Severity HIGH
CVSS v3.1 Base Score 7.1
CWE Classification CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
Affected Product SiYuan
Package Ecosystem Go
Published Date
Last Updated

Security Impact

CVSS: 7.1

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:L).

Attack Vector Network (AV:N)
Attack Complexity High (AC:H)
Privileges Required None (PR:N)
User Interaction Required (UI:R)
Scope Unchanged (S:U)
Confidentiality High (C:H)
Integrity High (I:H)
Availability Low (A:L)

How to Fix CVE-2026-54070

To remediate this vulnerability, upgrade all affected SiYuan dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

github.com/siyuan-note/siyuan/kernel fixed: 0.0.0-20260628153353-2d5d72223df4
Upgrade github.com/siyuan-note/siyuan/kernel to 0.0.0-20260628153353-2d5d72223df4
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for github.com/siyuan-note/siyuan/kernel

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-54070, inspect your project dependencies for SiYuan:

inspect dependencies for github.com/siyuan-note/siyuan/kernel

Analyze the terminal output to locate any version of SiYuan in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 0.0.0-20260628153353-2d5d72223df4), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting'), where the product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users. In SiYuan, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## 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)

CVE-2026-54070 Affected SiYuan Versions

1 component(s)
Fixed Versions:
0.0.0-20260628153353-2d5d72223df4
Product / Package Ecosystem Affected Versions Fixed Version
github.com/siyuan-note/siyuan/kernel
Go
< 0.0.0-20260628153353-2d5d72223df4
0.0.0-20260628153353-2d5d72223df4

References

Frequently Asked Questions

What is CVE-2026-54070?
CVE-2026-54070 is a security vulnerability in SiYuan involving Stored XSS in Bazaar marketplace via package README event handlers under CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').
Which SiYuan versions are affected?
Affected versions include releases prior to 0.0.0-20260628153353-2d5d72223df4 across affected packages: github.com/siyuan-note/siyuan/kernel.
What version fixes CVE-2026-54070?
Upgrading to 0.0.0-20260628153353-2d5d72223df4 (or the latest patched release for your respective branch) resolves the vulnerability.
Is github.com/siyuan-note/siyuan/kernel affected?
Yes, github.com/siyuan-note/siyuan/kernel is affected in releases prior to 0.0.0-20260628153353-2d5d72223df4. Upgrade your dependency to 0.0.0-20260628153353-2d5d72223df4 or later.
What is the CVSS score of CVE-2026-54070?
CVE-2026-54070 has a CVSS v3.1 base score of 7.1 (HIGH) with vector string CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:L.
What CWE is associated with CVE-2026-54070?
CVE-2026-54070 is classified under CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.).
How can I check Go dependencies for CVE-2026-54070?
Run the dependency tree command: 'inspect dependencies for github.com/siyuan-note/siyuan/kernel'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-w7cg-whh7-xp28",
  "modified" : "2026-07-10T19:27:25Z",
  "published" : "2026-07-10T19:27:24Z",
  "aliases" : [ "CVE-2026-54070" ],
  "summary" : "SiYuan: Stored XSS in Bazaar marketplace via package README event handlers",
  "details" : "## Summary\n\n`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.\n\nThat 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.\n\nThe 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.\n\nThe README is rendered when an Administrator opens a package in Settings → Marketplace, after the one-time marketplace trust consent. Install is not required.\n\nResult: 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.\n\n## Affected\n\nsiyuan-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.\nCondition: 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`.\nBoth the online browse path (`getBazaarPackageREADME`) and the installed-package path (`getInstalledPlugin`) reach the same sink.\n\n## Root cause\n\n`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.\n`render/sanitizer.go:235-334` (lute): `eventAttrs` is the w3schools handler list and contains no pointer, beforetoggle, focusin, animation, or transition handlers.\n`kernel/bazaar/readme.go:108-118`: `renderPackageREADME` builds the engine with `SetSanitize(true)` and returns the HTML string to the caller.\n`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`).\n`app/src/config/bazaar.ts:600` and `:609`: `mdElement.innerHTML = data.preferredReadme` / `= response.data.html`, no DOMPurify, target is a plain div.\nKernel HTTP responses carry no CSP/X-Frame-Options/X-Content-Type-Options header (live-confirmed), so an inline handler is not blocked.\n\n## Reproduction\n\n`b3log/siyuan:v3.6.5` Docker, default config, access auth code set, marketplace trust accepted.\n\n1. Place a package whose README carries a non-blocklisted handler (an online community package produces the identical render at browse time):\n\n```\nmkdir -p workspace/data/plugins/evil-plugin\ncat > workspace/data/plugins/evil-plugin/plugin.json <<'JSON'\n{\"name\":\"evil-plugin\",\"author\":\"x\",\"version\":\"1.0.0\",\"minAppVersion\":\"3.0.0\",\n \"displayName\":{\"default\":\"Evil\"},\"description\":{\"default\":\"poc\"},\n \"readme\":{\"default\":\"README.md\"},\"backends\":[\"all\"],\"frontends\":[\"all\"]}\nJSON\nprintf '<div onpointerover=\"alert(document.domain)\">plugin description</div>\\n' \\\n  > workspace/data/plugins/evil-plugin/README.md\n```\n\n2. Request the rendered README the way the Marketplace panel does:\n\n```\ncurl -s -X POST http://127.0.0.1:6806/api/bazaar/getInstalledPlugin \\\n  -H \"Authorization: Token <API-TOKEN>\" -H \"Content-Type: application/json\" \\\n  -d '{\"frontend\":\"all\",\"keyword\":\"\"}'\n```\n\nResponse `data.packages[].preferredReadme` contains the handler verbatim:\n\n```\n<div onpointerover=\"alert(document.domain)\">plugin description</div>\n```\n\nA control `<img src=x onerror=...>` in the same README is returned HTML-escaped and inert.\n\n3. In Settings → Marketplace, open the package and move the pointer over its README.\n\nLive-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.\n\n## Impact\n\n- JavaScript execution in the Administrator's authenticated origin on a marketplace package view plus one hover/click/focus, no install needed.\n- Theft of the kernel API token (`conf.api.token`), which grants full Administrator API access.\n- Pivot to `installBazaarPlugin` and kernel control; the runtime image ships a shell.\n- A single malicious community package reaches every instance that views its listing.\n\n## Credit\n\nJan Kahmen, [turingpoint](https://turingpoint.de) (jan@turingpoint.de)",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:L"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Go",
      "name" : "github.com/siyuan-note/siyuan/kernel"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "0.0.0-20260628153353-2d5d72223df4"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/siyuan-note/siyuan/security/advisories/GHSA-w7cg-whh7-xp28"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-54070"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/siyuan-note/siyuan"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-184", "CWE-79" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-07-10T19:27:24Z",
    "nvd_published_at" : "2026-06-24T22:16:48Z"
  }
}