Summary
The attribute-view store accepts an arbitrary string for a table column's width and applies no validation to it. Four frontend render sites interpolate that value directly into a style attribute with no escaping, including the template used for every table body cell. A quotation mark in the stored value terminates the attribute and injects an event handler.
Every other attribute in the same template is defended. Two of them are protected by exactly the kind of kernel-side validator this field lacks.
Details
The field is a free-form string. kernel/av/layout_table.go:65 declares Width string, and the setter applies no constraint at all:
// kernel/model/attribute_view.go:6216 (dev), :4681 (master)
column.Width = operation.Data.(string)
No length, character set, unit or CSS check. Confirmed end to end against kernel 3.7.4-alpha.2:
setAttrViewColWidth with data = 200px;x:y" onmouseover="alert(1)" data-x="
→ {"code":0}
renderAttributeView → width returned verbatim
on disk → "width":"200px;x:y\"...
Four unescaped sinks.
app/src/protyle/render/av/render.ts:206 — column header
app/src/protyle/render/av/render.ts:216 and :219 — calculation footers
app/src/protyle/render/av/row.ts:224 — every table body cell
Present identically on master at 170, 181, 184 and 193.
The break-out is confirmed by parsing, not only by reading. Feeding the exact row.ts:218-225 template to a strict HTML parser, with width set to 200px" onmouseover="alert(1)" x=", produces this attribute list on the element:
style = width: 200px
onmouseover = alert(1) ← a genuine event-handler attribute
x = ;
The injected handler is a real attribute in the parsed DOM, not an inference from the template text.
Every neighbouring field is defended. Within the same HTML literal:
| Field |
Protection |
icon, desc |
escapeAttr() at render |
name |
escapeHtml() at render |
align |
Kernel-validated: align.IsValid(), else ErrInvalidColumnAlign |
dateFormat |
Kernel-validated: format.IsValid(), else an error |
width |
Neither |
Two of the immediate neighbours already carry the kernel-side validator pattern that this field needs.
Not addressed by the recent fix. Commit 63e60bc4b modified kernel/model/attribute_view.go, but only the colour call sites. column.Width = operation.Data.(string) remains at dev HEAD 98a4fb37b, and all four render sites remain unescaped.
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. The storage behaviour, the API response, the four interpolation sites and the resulting parsed attribute list are all confirmed as described. I have not observed handler invocation in a running client, so the final step from the injected attribute to execution is stated from the parse result and the Electron configuration rather than from a captured runtime event.
Two adjacent paths I checked and excluded, so they need no attention:
row.ts:437, style="width: ${item.style.width}", reads its value back through CSSOM, which normalises the quotation mark away.
relation.ts:394 and :414, grid-template-columns:${gridTemplate}, appear identical but getRelationGridTemplate returns only the literals 32px, 240px, 200px and 160px, so no attacker input reaches them.
Relationship to existing advisories. column.width, colWidth, setAttrViewColWidth, av__widthdrag, style="width, grid-template and av__calc each appear in no published advisory for this project. The only match on the substring width: is GHSA-x63q-* (Mermaid javascript: links, CVE-2026-40322), an incidental occurrence inside a style snippet.
Proof of Concept
Set a table column's width to a value containing a quotation mark:
POST /api/transactions
{"reqId":<numeric>, ... "action":"setAttrViewColWidth",
"data":"200px;x:y\" onmouseover=\"alert(1)\" data-x=\""}
→ {"code":0}
Confirm it is stored and served unmodified:
POST /api/av/renderAttributeView
→ width returned verbatim, quotation marks intact
Then open any table view of that database.
Impact
A stored value under attacker control breaks out of the style attribute on every cell of a table column, injecting an event handler. Because row.ts:224 is the body-cell template, the injection is present on every row rather than on a single header element, and it renders on opening the view rather than on any deliberate interaction.
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 the ordinary column-resize operation, 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
Two changes:
- Apply
escapeAttr to the width value at all four render sites.
- Add a
FilterWidthValue in the kernel, mirroring the FilterColorValue introduced in 63e60bc4b, accepting only an empty string or a value matching ^\d+(\.\d+)?(px|em|rem|%)$. Apply it in both setAttributeViewColWidth and the bulk setAttributeViewColsWidth.
The second is the durable half, and it matches the treatment align and dateFormat already receive in the same structure. It also neutralises any value already stored.
Summary
The attribute-view store accepts an arbitrary string for a table column's width and applies no validation to it. Four frontend render sites interpolate that value directly into a
styleattribute with no escaping, including the template used for every table body cell. A quotation mark in the stored value terminates the attribute and injects an event handler.Every other attribute in the same template is defended. Two of them are protected by exactly the kind of kernel-side validator this field lacks.
Details
The field is a free-form string.
kernel/av/layout_table.go:65declaresWidth string, and the setter applies no constraint at all:No length, character set, unit or CSS check. Confirmed end to end against kernel 3.7.4-alpha.2:
Four unescaped sinks.
app/src/protyle/render/av/render.ts:206— column headerapp/src/protyle/render/av/render.ts:216and:219— calculation footersapp/src/protyle/render/av/row.ts:224— every table body cellPresent identically on master at
170,181,184and193.The break-out is confirmed by parsing, not only by reading. Feeding the exact
row.ts:218-225template to a strict HTML parser, withwidthset to200px" onmouseover="alert(1)" x=", produces this attribute list on the element:The injected handler is a real attribute in the parsed DOM, not an inference from the template text.
Every neighbouring field is defended. Within the same HTML literal:
icon,descescapeAttr()at rendernameescapeHtml()at renderalignalign.IsValid(), elseErrInvalidColumnAligndateFormatformat.IsValid(), else an errorwidthTwo of the immediate neighbours already carry the kernel-side validator pattern that this field needs.
Not addressed by the recent fix. Commit
63e60bc4bmodifiedkernel/model/attribute_view.go, but only the colour call sites.column.Width = operation.Data.(string)remains at dev HEAD98a4fb37b, and all four render sites remain unescaped.Consequence in the desktop application.
app/electron/main.jssetsnodeIntegration: true,contextIsolation: falseandwebSecurity: falseon every window (lines 913, 1019-1022, 1933-1936, 2157, 2197, 2248), so script executing in a renderer reaches Node built-ins includingrequire('child_process').Scope of observation. The storage behaviour, the API response, the four interpolation sites and the resulting parsed attribute list are all confirmed as described. I have not observed handler invocation in a running client, so the final step from the injected attribute to execution is stated from the parse result and the Electron configuration rather than from a captured runtime event.
Two adjacent paths I checked and excluded, so they need no attention:
row.ts:437,style="width: ${item.style.width}", reads its value back through CSSOM, which normalises the quotation mark away.relation.ts:394and:414,grid-template-columns:${gridTemplate}, appear identical butgetRelationGridTemplatereturns only the literals32px,240px,200pxand160px, so no attacker input reaches them.Relationship to existing advisories.
column.width,colWidth,setAttrViewColWidth,av__widthdrag,style="width,grid-templateandav__calceach appear in no published advisory for this project. The only match on the substringwidth:is GHSA-x63q-* (Mermaidjavascript:links, CVE-2026-40322), an incidental occurrence inside a style snippet.Proof of Concept
Set a table column's width to a value containing a quotation mark:
Confirm it is stored and served unmodified:
Then open any table view of that database.
Impact
A stored value under attacker control breaks out of the
styleattribute on every cell of a table column, injecting an event handler. Becauserow.ts:224is the body-cell template, the injection is present on every row rather than on a single header element, and it renders on opening the view rather than on any deliberate interaction.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 the ordinary column-resize operation, 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
Two changes:
escapeAttrto the width value at all four render sites.FilterWidthValuein the kernel, mirroring theFilterColorValueintroduced in63e60bc4b, accepting only an empty string or a value matching^\d+(\.\d+)?(px|em|rem|%)$. Apply it in bothsetAttributeViewColWidthand the bulksetAttributeViewColsWidth.The second is the durable half, and it matches the treatment
alignanddateFormatalready receive in the same structure. It also neutralises any value already stored.