Skip to content

Attribute-view field names are stored unescaped and interpolated into option elements without escaping, allowing stored cross-site scripting in the database sort menu

Critical
88250 published GHSA-g3jx-227v-x2x4 Aug 1, 2026

Package

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

Affected versions

dev 52e2bcac5

Patched versions

v3.7.4

Description

CVE: This vulnerability corresponds to CVE-2026-73052.

Summary

The attribute-view store writes field names without HTML escaping, and getSortsHTML interpolates those names directly into <option> elements before assigning the result via innerHTML. A field name containing markup therefore executes when a user opens a database's sort menu.

The same root cause reaches a second sink in col.ts, where the view name and field name are placed into a placeholder attribute without escaping.

Details

The value is stored raw. Confirmed against a running instance. POST /api/transactions, with a numeric reqId at the top level:

{"action":"updateAttrViewCol","id":"<keyID>","avID":"<avID>",
 "name":"</option></select><img src=x onerror=alert(9)>","type":"select"}
→ {"code":0}

renderAttributeView  → name returned verbatim
on disk              → "name":"</option></select><img src=x onerror=alert(9)>

The angle brackets and solidus survive intact. This matches the behaviour already observed for attribute-view view names and field icons, and contrasts with document IAL values, which are escaped on write.

The sink. app/src/protyle/render/av/sort.ts:122:

sortHTML += `<option value="${item.id}" ...>${item.icon && unicode2Emoji(item.icon)}${item.name}</option>`;

item.name is interpolated with no escaper. The result is assigned at :56 and :110 via menuElement.innerHTML = getSortsHTML(...). The trigger is opening a database's sort menu.

Why the payload closes elements first. The content of an <option> element is a text context, so injected markup placed there is not parsed as elements and a bare <img> would not execute. The payload therefore begins with </option></select>, which terminates both elements. The <img> that follows is parsed in the surrounding menu container, where its onerror handler runs on load failure. This is why the sink is exploitable despite the apparently inert insertion point.

The same line reaches a second defect. unicode2Emoji(item.icon) on that line returns unsanitized markup on its codepoint branch, which is a separate reported issue. Escaping item.name here does not address it, and sanitizing unicode2Emoji does not address item.name.

Second sink from the same root. app/src/protyle/render/av/col.ts:180 interpolates the view name and field name into a placeholder attribute with no escaper, reached through the same getEditHTML to innerHTML path. A quotation mark in the stored value closes the attribute.

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). Script executing in a renderer therefore reaches Node built-ins including require('child_process').

Scope of observation. The storage behaviour, the API response and the sink and consumer chain are confirmed as described. I have not observed handler execution in a running client, so the final step from innerHTML insertion to onerror firing is stated from the code and from the standard parsing behaviour of </option></select>, not from a captured result.

Relationship to existing advisories. sort.ts, getSortsHTML and <option each appear in no published advisory for this project. GHSA-2h64-* concerns the attribute-view name through a different sink and its body references none of these symbols. GHSA-5xfx-* lists select.ts, cell.ts, col.ts, filter.ts, groups.ts and blockAttr.ts as call sites and does not include sort.ts. The col.ts line reported here, :180, is distinct from the icon interpolations at :107 and :674 reported separately; the two carry different fields through different escaping requirements, and fixing either leaves the other exploitable.

Proof of Concept

Set a database field name to a value that closes its containing elements:

POST /api/transactions
{"reqId":<numeric>, ... "action":"updateAttrViewCol",
 "name":"</option></select><img src=x onerror=alert(9)>", ...}

→ {"code":0}

Confirm it is stored and served unmodified:

POST /api/av/renderAttributeView  → name returned verbatim, markup intact

Then open that database's sort menu in the client.

Impact

A stored value under attacker control executes script when a user opens the sort menu of a database containing the field. In the desktop client the renderer has Node integration enabled, so execution at that point is not confined to the page.

The value is written through an ordinary field-rename operation, so any path by which a document or database reaches a victim, including import, synchronisation, a shared workspace or a distributed package, carries the payload with it.

Suggested fix

Three changes, in increasing order of durability:

  1. escapeHtml(item.name) at sort.ts:122.
  2. escapeAttr on the name interpolations at col.ts:180.
  3. HTML-escape attribute-view names, icons and descriptions on write in kernel/model/attribute_view.go, where they are currently assigned directly from the operation data.

The third closes this class at the source. Document IAL values are already escaped on write, so this would bring attribute-view data in line with the treatment the rest of the store receives.

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