Observed while implementing objectui#6677 (PR #6722), which reordered
ObjectGrid.generateColumns()'s default paths. Filing, not claiming — and NOT
fixed in that PR, which was deliberately scoped to the row-key fallback.
The asymmetry
generateColumns() re-applies field-level security to its columns at exactly
one place, in the object-schema path — packages/plugin-grid/src/ObjectGrid.tsx
(measured on efbd027cb):
if (perms?.isLoaded && schema.objectName
&& !perms.checkField(schema.objectName, fieldName, 'read')) return;
The inline-data path has no equivalent check. Both paths can serve an
object-bound grid, and which one runs depends on who fetched the rows and
whether the author declared a projection:
| rows from |
fields declared |
path taken |
grid re-applies FLS |
| grid fetches |
no |
object-schema |
yes |
| grid fetches |
yes |
object-schema |
yes |
host passes data |
no |
object-schema (since PR #6722) |
yes |
host passes data |
yes |
inline-data |
no |
So the same object, same declared projection, renders through a code path that
does or does not re-check FLS purely according to who owns the fetch. Before PR
#6722 the third row was in the same position; that PR closed it for the
undeclared case and this is the remainder.
Why it is not obviously exploitable, and why that is not the same as fine
ListView — the dominant host — filters its own effectiveFields through FLS
before forwarding, and its source says so explicitly (ObjectGrid re-applies
FLS only on the DERIVED path, not on the explicit-columns one — so widening
here would be a widening past the field gate). So through ListView the gate is
applied host-side and the grid's omission is redundant.
The exposure, if any, is a host that is not ListView: a direct
ObjectGrid schema={{ objectName, fields }} data={rows} composition, or a future
host that forwards an authored projection without filtering it. The grid would
render a column for a field the principal cannot read, populated from whatever
the host's payload contained.
Not measured in a browser — this is a code reading, and the severity call is
deliberately left to triage rather than asserted here.
The decision behind it
Closing this is not a one-line move, which is why PR #6722 left it alone: it
asks whether the grid's FLS re-check belongs on the AUTHORED projection at all,
or whether authored columns are the host's responsibility. The two paths also
differ in two other ways that would have to be settled together — the schema
path resolves labels through resolveFieldLabel (i18n) and drops names the
object does not declare (if (!field) return;), while the inline path humanises
labels locally and keeps host-joined / derived keys. Converging them without
deciding those would silently drop derived columns.
Related: objectui#6677 (the branch-order half, fixed), objectui#6598.
Generated by Claude Code
Observed while implementing objectui#6677 (PR #6722), which reordered
ObjectGrid.generateColumns()'s default paths. Filing, not claiming — and NOTfixed in that PR, which was deliberately scoped to the row-key fallback.
The asymmetry
generateColumns()re-applies field-level security to its columns at exactlyone place, in the object-schema path —
packages/plugin-grid/src/ObjectGrid.tsx(measured on
efbd027cb):The inline-data path has no equivalent check. Both paths can serve an
object-bound grid, and which one runs depends on who fetched the rows and
whether the author declared a projection:
fieldsdeclareddatadataSo the same object, same declared projection, renders through a code path that
does or does not re-check FLS purely according to who owns the fetch. Before PR
#6722 the third row was in the same position; that PR closed it for the
undeclared case and this is the remainder.
Why it is not obviously exploitable, and why that is not the same as fine
ListView— the dominant host — filters its owneffectiveFieldsthrough FLSbefore forwarding, and its source says so explicitly (
ObjectGridre-appliesFLS only on the DERIVED path, not on the explicit-columns one — so widening
here would be a widening past the field gate). So through
ListViewthe gate isapplied host-side and the grid's omission is redundant.
The exposure, if any, is a host that is not
ListView: a directObjectGrid schema={{ objectName, fields }} data={rows}composition, or a futurehost that forwards an authored projection without filtering it. The grid would
render a column for a field the principal cannot read, populated from whatever
the host's payload contained.
Not measured in a browser — this is a code reading, and the severity call is
deliberately left to triage rather than asserted here.
The decision behind it
Closing this is not a one-line move, which is why PR #6722 left it alone: it
asks whether the grid's FLS re-check belongs on the AUTHORED projection at all,
or whether authored columns are the host's responsibility. The two paths also
differ in two other ways that would have to be settled together — the schema
path resolves labels through
resolveFieldLabel(i18n) and drops names theobject does not declare (
if (!field) return;), while the inline path humaniseslabels locally and keeps host-joined / derived keys. Converging them without
deciding those would silently drop derived columns.
Related: objectui#6677 (the branch-order half, fixed), objectui#6598.
Generated by Claude Code