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:
escapeHtml(item.name) at sort.ts:122.
escapeAttr on the name interpolations at col.ts:180.
- 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.
CVE: This vulnerability corresponds to CVE-2026-73052.
Summary
The attribute-view store writes field names without HTML escaping, and
getSortsHTMLinterpolates those names directly into<option>elements before assigning the result viainnerHTML. 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 aplaceholderattribute without escaping.Details
The value is stored raw. Confirmed against a running instance.
POST /api/transactions, with a numericreqIdat the top level:{"action":"updateAttrViewCol","id":"<keyID>","avID":"<avID>", "name":"</option></select><img src=x onerror=alert(9)>","type":"select"}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:item.nameis interpolated with no escaper. The result is assigned at:56and:110viamenuElement.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 itsonerrorhandler 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. Escapingitem.namehere does not address it, and sanitizingunicode2Emojidoes not addressitem.name.Second sink from the same root.
app/src/protyle/render/av/col.ts:180interpolates the view name and field name into aplaceholderattribute with no escaper, reached through the samegetEditHTMLtoinnerHTMLpath. A quotation mark in the stored value closes the attribute.Consequence in the desktop application.
app/electron/main.jssetsnodeIntegration: true,contextIsolation: falseandwebSecurity: falseon every window (lines 913, 1019-1022, 1933-1936, 2157, 2197, 2248). Script executing in a renderer therefore reaches Node built-ins includingrequire('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
innerHTMLinsertion toonerrorfiring 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,getSortsHTMLand<optioneach 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-* listsselect.ts,cell.ts,col.ts,filter.ts,groups.tsandblockAttr.tsas call sites and does not includesort.ts. Thecol.tsline reported here,:180, is distinct from the icon interpolations at:107and:674reported 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:
Confirm it is stored and served unmodified:
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:
escapeHtml(item.name)atsort.ts:122.escapeAttron the name interpolations atcol.ts:180.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.