GitHub Advisoryadvisory · Go

GHSA-25rp-h46x-2hjm SiYuan: Electron Renderer RCE via decodeURIComponent-driven tooltip XSS in aria-label sink (incomplete fix for CVE-2026-34585)

CRITICAL Published 2026-05-08 Modified 2026-06-08 Official page ↗
Aliases
CVE-2026-44588
Ecosystem
Go
Packages
github.com/siyuan-note/siyuan/kernel
Severity
Critical
CVSS vector
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
Weaknesses
CWE-116, CWE-1188, CWE-79
Reviewed
2026-05-08

Affected packages

• Go github.com/siyuan-note/siyuan/kernel: <= 0.0.0-20260421031503-96dfe0bea474 (no fix yet)

Description

Summary The tooltip mouseover handler in `app/src/block/popover.ts` reads `aria-label` via `getAttribute` and passes it through `decodeURIComponent` before assigning to `messageElement.innerHTML` in `app/src/dialog/tooltip.ts:41`. The encoder used at the producer side, `escapeAriaLabel` in `app/src/util/escape.ts:19-25`, only handles HTML special characters (`"`, `'`, `` element, and the `onerror` handler fires. Because the renderer runs with `nodeIntegration: true, contextIsolation: false, webSecurity: false` (`app/electron/main.js:407-411`), `require('child_process')` is reachable from the injected handler, escalating to arbitrary code execution. Doc titles, AV column names + descriptions, AV select options, file-tree tooltips all reach this sink because they're rendered into `class="ariaLabel"` elements with `aria-label="${escapeAriaLabel(...)}"`. Doc title is the easiest plant — any user with create/rename access lands the payload, and the file survives `.sy.zip` round-trip without modification. Why a "double HTML-decode" framing is wrong A naïve reading of the chain might suggest that `&amp;lt;` (the encoder output) decodes once at attribute-parse time to `&lt;`, then a second time at `innerHTML` time to ` { if (!html) { return html; } return html.replace(/"/g, "&quot;").replace(/'/g, "&apos;") .replace(/${escapeGreat(title)} ``` Same pattern at `:1448`, `protyle/render/av/blockAttr.ts:205`, `protyle/render/av/col.ts:134`, `protyle/render/av/select.ts:36`, `search/unRef.ts:113`. The `title` is built from `getNotebookName(item.box) + getDisplayName(item.hPath, false)` (line 1398). The `hPath` returned by `/api/search/fullTextSearchBlock` carries the user-set doc title verbatim — `%XX` URL-escapes pass through, only HTML special chars are entity-encoded by the kernel. Consumer. `app/src/block/popover.ts:33,144`: ```ts let tip = aElement.getAttribute("aria-label") || ""; // literal stored attribute value // ... branch logic that doesn't apply to plain search results ... showTooltip(decodeURIComponent(tip), aElement, ...); // ← decodes %XX into raw chars ``` `decodeURIComponent` is presumably present to handle URL-encoded asset paths in some hyperlink tooltips, but it's applied unconditionally to every aria-label-sourced tip — that's what enables this bypass. Sink. `app/src/dialog/tooltip.ts:41`: ```ts messageElement.innerHTML = message; // ← HTML parser sees the now-decoded raw `` `'`, literal `"`): | step | result | |------|--------| | in-memory title | `%3Cimg src=x onerror="alert('SiYuan')"%3E` | | `escapeAriaLabel` writes (only `"` and `'` get encoded — neither appears here as raw chars when `'` is `%27`) | `%3Cimg src=x onerror=&quot;alert(%27SiYuan%27)&quot;%3E` | | HTML attribute set: `aria-label="..."` ; browser one-decodes named entities when storing | in-DOM value = `%3Cimg src=x onerror="alert(%27SiYuan%27)"%3E` | | `getAttribute("aria-label")` | `%3Cimg src=x onerror="alert(%27SiYuan%27)"%3E` (verbatim) | | `decodeURIComponent(tip)` | `` (real `` chars) | | `messageElement.innerHTML = …` | HTML parser tokenizes raw ``, creates element, fails to load `src=x`, fires `onerror` → JS runs | Renderer + reachability. Renderer posture and auto-admin gates same as the AV-name advisory (Advisory 1): `nodeIntegration:true, contextIsolation:false, webSecurity:false` at `app/electron/main.js:407-411`; empty-`AccessAuthCode` local auto-admin at `kernel/model/session.go:261-287`; `chrome-extension://` Origin allowlist at `session.go:277`. Suggested fix 1. Primary — `app/src/dialog/tooltip.ts:41`: replace ```ts messageElement.innerHTML = message; ``` with ```ts messageElement.textContent = message; ``` For tooltips that legitimately need markup (memo rendering, hyperlink preview cards), introduce an explicit `{html: true}` flag on `showTooltip(...)` and route the message through `DOMPurify.sanitize(message)` before assigning to `innerHTML`. 2. Drop `decodeURIComponent` at `popover.ts:144` for the generic aria-label path. Apply it only on the few callers that intentionally pass URL-encoded asset paths (e.g. the local-asset hyperlink preview branch already inside the function), and apply it inside `try`/`catch` with a clear scope. Aria-label content is not URL-encoded by design; decoding it is a footgun that converts otherwise-safe attributes into pre-parsed HTML. 3. Consolidate the four escape helpers in `app/src/util/escape.ts` (`escapeHtml`, `escapeAttr`, `escapeAriaLabel`, `escapeGreat`) into one `Lute.EscapeHTMLStr`-equivalent that escapes `&`, ``, `"`, `'`. Context-specific encoders without compile-time enforcement keep producing bug-class variants. 4. (Defense-in-depth) Switch the main BrowserWindow to `contextIsolation: true` with a preload bridge — caps every future renderer XSS at "DOM only," not RCE. --- Reproduction (copy-paste-ready) Tested on Windows with SiYuan v3.6.5 (kernel + Electron) and Microsoft Edge as the offline parser-validation engine. Linux/macOS users substitute `py` with `python3` and use any modern Chromium-based browser (Edge/Chrome/Brave) for the standalone validation step. Prereqs 1. Install SiYuan v3.6.5 from https://github.com/siyuan-note/siyuan/releases and launch once. Do not set an `AccessAuthCode` (default). 2. Verify the kernel is up: ```sh curl -s http://127.0.0.1:6806/api/system/version # → {"code":0,"msg":"","data":"3.6.5"} ``` 3. Create at least one notebook (the file tree's "+" button) so `lsNotebooks` returns a usable id. Pin variables: ```sh API=http://127.0.0.1:6806 NOTEBOOK_ID=$(curl -s -X POST $API/api/notebook/lsNotebooks \ -H 'Content-Type: application/json' -d '{}' \ | python -c 'import sys,json; print(json.load(sys.stdin)["data"]["notebooks"][0]["id"])') echo "Using notebook: $NOTEBOOK_ID" ``` Step A — Browser-only validation of the chain (no SiYuan needed) This proves the bug class on its own. Save as `decode-chain.html`, open in any Chromium-based browser: ```html Click "Simulate" — if status turns red, the chain works. <span id="src" class="ariaLabel" aria-label="%3Cimg src=x onerro…

Related

Alias · CVE (1)

  • CVE-2026-44588 SiYuan is an open-source personal knowledge management system. vulnerability

Weakness · CWE (3)

  • CWE-116 Improper Encoding or Escaping of Output weakness
  • CWE-1188 Initialization of a Resource with an Insecure Default weakness
  • CWE-79 Improper Neutralization of Input During Web Page Generation weakness

References