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.
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:
view.ts:291:202<textarea … data-value="${escapeAttr(view.desc)}">${view.desc}</textarea>col.ts:117:112<textarea … data-value="${escapeAttr(colData.desc)}">${colData.desc}</textarea>groups.ts:198:198<div class="b3-menu__label…">${item.name || ""}</div>groups.ts:207:207…</svg> ${column.name || ""}groups.ts:263:263<span class="b3-menu__accelerator">${column ? column.name : ""}</span>view.ts:324:235<span class="b3-menu__accelerator">${fields.filter(…)[0].name}</span>All reach
innerHTML:getGroupsHTMLatgroups.ts:56,:289,:328,:378;getViewHTMLatopenMenuPanel.ts:823;getEditHTMLatcol.ts:483,:716andopenMenuPanel.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
escapeAttron the value fordata-valueand then interpolate the identical value into the element body with nothing. Parsing sink 2 with the value the kernel returns produces: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:
The kernel setters apply no constraint:
view.Desc = strings.TrimSpace(operation.Data.(string))andkeyValues.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:
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 yieldsdiv > </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.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. 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__labelandb3-menu__accelerator.GHSA-5xfx-* concerns the cell-value branches of
genAVValueHTML, which is a differenttextarea, the cell editor. It does mentiongroups.ts, but only within its list of call sites pipinggenAVValueHTMLoutput intoinnerHTML. It shares consumers with this report and none of its producers.GHSA-g3jx-227v-x2x4 concerns
sort.ts:122andcol.ts:180, and does not reach thegroups.tsorview.tsmetadata paths. GHSA-25rp-* concerns a description reachingaria-labelthroughdecodeURIComponent, a different sink and a different escaping context.Proof of Concept
Set a field description to a value that closes the containing element:
Confirm it is stored and served unmodified:
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: trueandcontextIsolation: false, that script reaches Node built-ins includingrequire('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
escapeHtmlto 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 emptylabel is constructed, so that a group name emitted by the server cannot itself carry markup.