Skip to content

Database menu labels are escaped in the attribute and left raw in the body of the same line, turning a field description into Remote Code Execution on the desktop client

Critical
88250 published GHSA-gcm3-qcq3-72rv Aug 1, 2026

Package

gomod github.com/siyuan-note/siyuan/kernel (Go)

Affected versions

master and dev

Patched versions

v3.7.4

Description

Summary

Six template sites across the database group, view and field-edit menus interpolate attribute-view metadata into HTML without escaping, and the results are assigned via innerHTML. The values reach them unvalidated from the kernel.

Two of the six escape the same value correctly in an attribute on the same line and leave the element body raw. A description containing </textarea> closes the element and the markup that follows is parsed.

One of the affected values is constructed by the kernel itself: an empty-group placeholder label built by interpolating a raw field name into Field [%s] is empty.

Details

The six sinks, with line numbers on dev and master:

Sink dev master Interpolation
1 view.ts:291 :202 <textarea … data-value="${escapeAttr(view.desc)}">${view.desc}</textarea>
2 col.ts:117 :112 <textarea … data-value="${escapeAttr(colData.desc)}">${colData.desc}</textarea>
3 groups.ts:198 :198 <div class="b3-menu__label…">${item.name || ""}</div>
4 groups.ts:207 :207 …</svg> ${column.name || ""}
5 groups.ts:263 :263 <span class="b3-menu__accelerator">${column ? column.name : ""}</span>
6 view.ts:324 :235 <span class="b3-menu__accelerator">${fields.filter(…)[0].name}</span>

All reach innerHTML: getGroupsHTML at groups.ts:56, :289, :328, :378; getViewHTML at openMenuPanel.ts:823; getEditHTML at col.ts:483, :716 and openMenuPanel.ts:475, :1286, :1374. The triggers are opening a database's group menu, view menu or field-edit menu.

The attribute is escaped and the body is not, on one line. Sinks 1 and 2 call escapeAttr on the value for data-value and then interpolate the identical value into the element body with nothing. Parsing sink 2 with the value the kernel returns produces:

data-value  = "</textarea><img src=x onerror=alert('COLDESC')>"    ← escapeAttr applied
</textarea>                                                         ← body closed the element
<img>  { src: "x", onerror: "alert('COLDESC')" }                    ← parsed event handler

The escaping helper is present, imported and used three characters to the left of the interpolation that is missing it.

The sources are unvalidated. Confirmed against a live kernel:

setAttrViewViewDesc  data = </textarea><img src=x onerror=alert('VIEWDESC')>  → {"code":0}
setAttrViewColDesc   data = </textarea><img src=x onerror=alert('COLDESC')>   → {"code":0}
renderAttributeView  → view.desc and col.desc returned verbatim

The kernel setters apply no constraint: view.Desc = strings.TrimSpace(operation.Data.(string)) and keyValues.Key.Desc = operation.Data.(string).

The group-name sink has a second, server-constructed source. After grouping a view by a select field, the kernel returned both of these as group names:

PWN
Field [</option></select><img src=x onerror=alert(9)>] is empty

The first is select-option content. The second is the kernel's own placeholder label, produced by interpolating the raw field name into Field [%s] is empty. Parsing that at sink 3 yields div > </option> </select> img{ onerror: "alert(9)" }. Two independent attacker-controlled sources reach the same sink, and one of them is assembled server-side.

Consequence in the desktop application. app/electron/main.js sets nodeIntegration: true, contextIsolation: false and webSecurity: false on every window (lines 913, 1019-1022, 1933-1936, 2157, 2197, 2248), so script executing in a renderer reaches Node built-ins including require('child_process').

Scope of observation. Storage, the API responses and the parsed results above are confirmed as described. I have not observed handler invocation in a running client, so the step from the parsed attribute to execution is stated from the Electron configuration rather than from a captured runtime event.

Relationship to existing advisories. Each of the six file and line references returns no match across the published advisories for this project, as do getGroupsHTML, getViewHTML, view.desc, b3-menu__label and b3-menu__accelerator.

GHSA-5xfx-* concerns the cell-value branches of genAVValueHTML, which is a different textarea, the cell editor. It does mention groups.ts, but only within its list of call sites piping genAVValueHTML output into innerHTML. It shares consumers with this report and none of its producers.

GHSA-g3jx-227v-x2x4 concerns sort.ts:122 and col.ts:180, and does not reach the groups.ts or view.ts metadata paths. GHSA-25rp-* concerns a description reaching aria-label through decodeURIComponent, a different sink and a different escaping context.

Proof of Concept

Set a field description to a value that closes the containing element:

POST /api/transactions
{"reqId":<numeric>, ... "action":"setAttrViewColDesc",
 "data":"</textarea><img src=x onerror=alert('COLDESC')>"}

→ {"code":0}

Confirm it is stored and served unmodified:

POST /api/av/renderAttributeView
→ col.desc returned verbatim

Then open that field's edit menu.

For the server-constructed variant, set a field name to </option></select><img src=x onerror=alert(9)>, group the view by a select field, and open the group menu. The kernel's own empty-group label carries the payload.

Impact

A stored value under attacker control executes script when a user opens a database's group, view or field-edit menu. Because the renderer runs with nodeIntegration: true and contextIsolation: false, that script reaches Node built-ins including require('child_process'), so execution is not confined to the page.

The values are written through ordinary description and rename operations, so any path by which a database reaches a victim, including import, synchronisation, a shared workspace or a distributed package, carries the payload.

Suggested fix

Apply escapeHtml to the interpolation in all six sites. For sinks 1 and 2 this is the escaping already present on the same line, applied to the second interpolation as well.

Separately, escape the field name in the kernel where the Field [%s] is empty label is constructed, so that a group name emitted by the server cannot itself carry markup.

Severity

Critical

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
Required
Scope
Changed
Confidentiality
High
Integrity
High
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:H

CVE ID

No known CVE

Weaknesses

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. Learn more on MITRE.

Credits