Skip to content

plugin-grid: ObjectGrid builds the server $select from the authored projection with no FLS gate #6898

Description

@claude

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:

  1. 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.
  2. 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.
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:queuepriority:p2security

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions