Filed by the domain:ui seat (session session_012wwHa4aaFybxXrfmfHioDM) on behalf of the #7215 lane, which measured all of this but could not file it: its duplicate-check search hit the API rate limit and repo-scoped REST answers 403 from that container, so there was no channel to check for duplicates and it correctly declined to file blind. Filed unassigned.
Duplicate check run by this seat before filing, with a control that fires: the query returned 3 on-topic issues including #7216 (the sibling $select gap) and none matching this. A non-empty on-topic hit set makes the absence a reading rather than a broken query.
What PR #7229 fixed, and where it stopped
objectui#7215 / PR #7229 FLS-gated $expand at the two projection sites in its scope — plugin-grid/src/ObjectGrid.tsx and plugin-list/src/ListView.tsx — by gating the output of buildExpandFields.
buildExpandFields is called from five more places, none of them gated, none in that card's scope, none touched by it.
Measured
| site | shape | checkField in file |
|---|
packages/plugin-calendar/src/ObjectCalendar.tsx:350 | buildExpandFields(objectSchema?.fields) — no column list | 0 |
packages/plugin-gantt/src/ObjectGantt.tsx:674 | same, no column list | 0 |
packages/app-shell/src/views/RecordDetailView.tsx:402 | same, no column list | 0 |
packages/plugin-detail/src/DetailView.tsx:526 | passes a column list, ungated | 1, at :249 — the render half, not the projection |
packages/plugin-dashboard/src/ObjectDataTable.tsx:680 | builds its own $expand whitelist via computeLookupExpand | 0 |
⚠️ The first three are the sharpest, and it is not an edge case
buildExpandFields reads an absent column list as "no column restriction" and falls back to every declared relation on the object — denied ones included.
So calendar, gantt and record-detail do not merely fail to filter a column list; they have no column list, and therefore expand the maximum possible set by default. That is the ordinary configuration of those surfaces, not a corner of it.
This is also exactly why PR #7229 put its gate on the helper's output rather than its input, and that reasoning transfers unchanged: gating an input that is undefined is impossible, and gating an empty one widens the result. Whoever takes this card should read #7229's implementation first — the shape is already settled and pinned by tests there.
Severity — precise rather than alarming
Same grading as objectui#6898 and #7215: p2, defence-in-depth, not a live disclosure against ObjectStack's own server. The #7215 lane established the mechanism by reading it rather than assuming:
FieldMasker.maskRecord does delete result[field] and objectql's engine writes the expanded record back under that same key, so one statement removes both; the expansion sub-read itself takes the referenced object's full CRUD + RLS + FLS treatment (objectstack#7626).
It is load-bearing for a non-enforcing backend, and the client-request side is real regardless: these components ask the server to resolve relations the current principal cannot read.
The ordering constraint, carried forward
⚠️checkField answers false for an undeclared key, so a naive gate also drops derived and computed fields. Gating buildExpandFields's output avoids this structurally — the helper already returns a subset of the object's declared reference-bearing fields, so every name judged is declared and the trap is unreachable. Do not re-derive this; copy #7229's shape.
Not measured
Whether any of these five surfaces is reachable by a principal with a denied lookup in a shipped configuration. #7215 established that for its own two sites; these five were measured as code shape, not as a reproduction. The first task on this card is a failing test per site, on the model of packages/plugin-grid/src/__tests__/expandFls-7215.test.tsx. If a site turns out to be unreachable, that is a finding — record it and pin the behaviour rather than fixing it.
Related: objectui#7215 / PR #7229 (the two sites already fixed; read its implementation first) · objectui#6898 (the original $select gate — note #7229 found it had a live bypass through the expand-root union) · objectui#7216 (the sibling gap: ListView's speculative view bindings reach $select known-field-checked but not FLS-checked).
Filed by the
domain:uiseat (sessionsession_012wwHa4aaFybxXrfmfHioDM) on behalf of the #7215 lane, which measured all of this but could not file it: its duplicate-check search hit the API rate limit and repo-scoped REST answers 403 from that container, so there was no channel to check for duplicates and it correctly declined to file blind. Filed unassigned.Duplicate check run by this seat before filing, with a control that fires: the query returned 3 on-topic issues including #7216 (the sibling
$selectgap) and none matching this. A non-empty on-topic hit set makes the absence a reading rather than a broken query.What PR #7229 fixed, and where it stopped
objectui#7215 / PR #7229 FLS-gated
$expandat the two projection sites in its scope —plugin-grid/src/ObjectGrid.tsxandplugin-list/src/ListView.tsx— by gating the output ofbuildExpandFields.buildExpandFieldsis called from five more places, none of them gated, none in that card's scope, none touched by it.Measured
checkFieldin filepackages/plugin-calendar/src/ObjectCalendar.tsx:350buildExpandFields(objectSchema?.fields)— no column listpackages/plugin-gantt/src/ObjectGantt.tsx:674packages/app-shell/src/views/RecordDetailView.tsx:402packages/plugin-detail/src/DetailView.tsx:526:249— the render half, not the projectionpackages/plugin-dashboard/src/ObjectDataTable.tsx:680$expandwhitelist viacomputeLookupExpandbuildExpandFieldsreads an absent column list as "no column restriction" and falls back to every declared relation on the object — denied ones included.So calendar, gantt and record-detail do not merely fail to filter a column list; they have no column list, and therefore expand the maximum possible set by default. That is the ordinary configuration of those surfaces, not a corner of it.
This is also exactly why PR #7229 put its gate on the helper's output rather than its input, and that reasoning transfers unchanged: gating an input that is
undefinedis impossible, and gating an empty one widens the result. Whoever takes this card should read #7229's implementation first — the shape is already settled and pinned by tests there.Severity — precise rather than alarming
Same grading as objectui#6898 and #7215: p2, defence-in-depth, not a live disclosure against ObjectStack's own server. The #7215 lane established the mechanism by reading it rather than assuming:
It is load-bearing for a non-enforcing backend, and the client-request side is real regardless: these components ask the server to resolve relations the current principal cannot read.
The ordering constraint, carried forward
checkFieldanswers false for an undeclared key, so a naive gate also drops derived and computed fields. GatingbuildExpandFields's output avoids this structurally — the helper already returns a subset of the object's declared reference-bearing fields, so every name judged is declared and the trap is unreachable. Do not re-derive this; copy #7229's shape.Not measured
Whether any of these five surfaces is reachable by a principal with a denied lookup in a shipped configuration. #7215 established that for its own two sites; these five were measured as code shape, not as a reproduction. The first task on this card is a failing test per site, on the model of
packages/plugin-grid/src/__tests__/expandFls-7215.test.tsx. If a site turns out to be unreachable, that is a finding — record it and pin the behaviour rather than fixing it.Related: objectui#7215 / PR #7229 (the two sites already fixed; read its implementation first) · objectui#6898 (the original
$selectgate — note #7229 found it had a live bypass through the expand-root union) · objectui#7216 (the sibling gap:ListView's speculative view bindings reach$selectknown-field-checked but not FLS-checked).