ObjectGrid's data loader builds the server $select from the authored columns / fields
projection with no field-level-security gate, so a field the principal may not read can still be
REQUESTED and returned into the browser.
Filing, not claiming. Observed while implementing objectui#6799 (the authored-columns FLS
re-check), which the maintainer ruling of 2026-08-30 deliberately scoped to generateColumns()
and to that function only. This is the same defect family one layer down, and it is explicitly
out of scope there.
Where
packages/plugin-grid/src/ObjectGrid.tsx, loadSchemaAndData() — getSelectFields() (around
L1402 and the getSelectFields closure below it). It resolves each column's identity through
columnIdentity, adds id, adds the predicate fields the view reads, and sends the result as
the query projection. perms.checkField is never consulted on that path.
Why it is not the same bug as objectui#6799, and not nothing either
objectui#6799 fixed what REACHES THE SCREEN: generateColumns() now drops a column naming a
declared field the principal cannot read, on all three of its default paths. That is the
rendering half, and it is done.
This is the FETCH half. After objectui#6799 the column is hidden, but the field name is still in
the $select, so:
- a server that enforces FLS refuses or omits it — no exposure, and this is the expected case;
- a server that does NOT enforce it on projection returns the value into
data, where it sits in
memory and in the network response for a principal whose policy denies it, with no column on
screen to reveal that it happened.
So the exposure is conditional on the backend, which is exactly the shape "defence in depth"
exists for — the same argument the objectui#6723 and objectui#6799 rulings accepted for the
rendering half ("a security invariant must not rest on every future host having read the docs";
here, on every future backend).
Why it is a decision rather than a mechanical follow-up
Filtering the $select is not free and at least one consequence needs a call:
- The declared-key limit interacts differently here. On the render path an undeclared key is
a legitimate derived column and passes through. In a $select an undeclared key is what the
host asked the SERVER for, and dropping vs. keeping it changes what the query returns, not
just what is drawn. ensureId and the predicate fields.id is force-added so row navigation works, and
fields the view's predicates read are added even though no column shows them. If FLS filtering
is applied naively it can strip those and break navigation or filtering rather than close a
hole.- It may well be correct to decide the server owns this outright — which is the option the
objectui#6723 / objectui#6799 rulings declined for the RENDER path, but the reasoning does not
transfer automatically, because a projection is a request to a component that has its own
enforcement duty.
Not measured against a live backend: this is a code reading taken while implementing
objectui#6799. Severity deliberately left to triage.
Related: objectui#6799 (the render half, fixed), objectui#6723.
Generated by Claude Code
ObjectGrid's data loader builds the server$selectfrom the authoredcolumns/fieldsprojection with no field-level-security gate, so a field the principal may not read can still be
REQUESTED and returned into the browser.
Filing, not claiming. Observed while implementing objectui#6799 (the authored-
columnsFLSre-check), which the maintainer ruling of 2026-08-30 deliberately scoped to
generateColumns()and to that function only. This is the same defect family one layer down, and it is explicitly
out of scope there.
Where
packages/plugin-grid/src/ObjectGrid.tsx,loadSchemaAndData()—getSelectFields()(aroundL1402 and the
getSelectFieldsclosure below it). It resolves each column's identity throughcolumnIdentity, addsid, adds the predicate fields the view reads, and sends the result asthe query projection.
perms.checkFieldis never consulted on that path.Why it is not the same bug as objectui#6799, and not nothing either
objectui#6799 fixed what REACHES THE SCREEN:
generateColumns()now drops a column naming adeclared field the principal cannot read, on all three of its default paths. That is the
rendering half, and it is done.
This is the FETCH half. After objectui#6799 the column is hidden, but the field name is still in
the
$select, so:data, where it sits inmemory and in the network response for a principal whose policy denies it, with no column on
screen to reveal that it happened.
So the exposure is conditional on the backend, which is exactly the shape "defence in depth"
exists for — the same argument the objectui#6723 and objectui#6799 rulings accepted for the
rendering half ("a security invariant must not rest on every future host having read the docs";
here, on every future backend).
Why it is a decision rather than a mechanical follow-up
Filtering the
$selectis not free and at least one consequence needs a call:a legitimate derived column and passes through. In a
$selectan undeclared key is what thehost asked the SERVER for, and dropping vs. keeping it changes what the query returns, not
just what is drawn.
ensureIdand the predicate fields.idis force-added so row navigation works, andfields the view's predicates read are added even though no column shows them. If FLS filtering
is applied naively it can strip those and break navigation or filtering rather than close a
hole.
objectui#6723 / objectui#6799 rulings declined for the RENDER path, but the reasoning does not
transfer automatically, because a projection is a request to a component that has its own
enforcement duty.
Not measured against a live backend: this is a code reading taken while implementing
objectui#6799. Severity deliberately left to triage.
Related: objectui#6799 (the render half, fixed), objectui#6723.
Generated by Claude Code