ObjectGrid.generateColumns() re-applies field-level security on two of its
three authored-column paths. After objectui#6723 lands, the AUTHORED columns
path is the one that is left.
Filing, not claiming. Observed while implementing objectui#6723 (the inline-data
half), which was deliberately scoped by the maintainer ruling of 2026-08-29 to
the inline-data path and to that path only.
The remaining asymmetry
generateColumns() has three default paths. Measured on d06059f24 plus the
objectui#6723 branch:
| path | reached when | re-applies FLS |
|---|
authored columns (normalizeColumns(schemaColumns), both the ListColumn[] and the string[] arms) | schema.columns present and non-empty | no |
inline-data (hasInlineData, authored fields) | host passes data and schema.fields is declared | yes, as of objectui#6723 |
| object-schema | everything else | yes |
Evidence on d06059f24:
git grep -n checkField -- packages/plugin-grid/src/ObjectGrid.tsx returns
exactly two lines — one comment and one call — and the call is inside the
fieldsToShow.forEach of the object-schema path. The if (cols) branch above
it returns from either of its two arms before that point and contains no
checkField of its own.
Why it is not obviously exploitable, and why that is not the same as fine
Identical to the argument on objectui#6723. ListView, the dominant host,
filters its own effectiveFields through checkField before forwarding, and
that filtered array is exactly what it sends as columns — so through
ListView the gate is applied host-side and this omission is redundant.
ListView.tsx's own hasAuthoredColumns docblock already states the property
as a known fact ("ObjectGrid re-applies FLS only on the DERIVED path, not on
the explicit-columns one"), and objectui#6598 relies on it deliberately: when
the field gate removes every authored column, ListView keeps sending the empty
projection precisely because the grid would not re-check.
The exposure, if any, is the same shape objectui#6723 described: a direct
ObjectGrid schema={{ objectName, columns }} composition, or a future host that
forwards an authored projection unfiltered. Unlike the inline-data case, this
one does not even need the host to supply rows — the columns path runs whether
the grid fetches or not.
Why this is a decision and not a mechanical follow-up
The objectui#6723 ruling took defence in depth over "the host owns it", on the
stated ground that a security invariant must not rest on every future host
having read the docs. That reasoning does not distinguish fields from
columns, which is why this is worth putting in front of triage rather than
assuming either answer. But two things differ enough that it is a separate call:
- The declared-key limit needs restating for
ListColumn[]. The
objectui#6723 fix judges only keys the object declares, so host-joined and
derived keys survive. A ListColumn carries more than a name (label,
link, action, prefix, width), and a column whose field the object
does not declare is a legitimate authored derived column. The same limit
should apply, but "which key is judged" has to be read through
columnIdentity / resolvesToDataColumn rather than off a bare string. - objectui#6598 pinned a behaviour that assumes the current answer. Its
hasAuthoredColumns predicate and the test that pins it are written around
"the explicit-columns path does not re-check". Adding the check does not
invalidate that predicate — it stays correct for a different reason — but the
pin's stated rationale would need rewriting in the same change, and that is
the kind of thing that should be decided rather than done in passing.
Not measured in a browser: this is a code reading plus the ablation run on the
objectui#6723 branch. Severity is deliberately left to triage.
Related: objectui#6723 (the inline-data half, fixed), objectui#6598,
objectui#6677.
Generated by Claude Code
ObjectGrid.generateColumns()re-applies field-level security on two of itsthree authored-column paths. After objectui#6723 lands, the AUTHORED
columnspath is the one that is left.
Filing, not claiming. Observed while implementing objectui#6723 (the inline-data
half), which was deliberately scoped by the maintainer ruling of 2026-08-29 to
the inline-data path and to that path only.
The remaining asymmetry
generateColumns()has three default paths. Measured ond06059f24plus theobjectui#6723 branch:
columns(normalizeColumns(schemaColumns), both theListColumn[]and thestring[]arms)schema.columnspresent and non-emptyhasInlineData, authoredfields)dataandschema.fieldsis declaredEvidence on
d06059f24:git grep -n checkField -- packages/plugin-grid/src/ObjectGrid.tsxreturnsexactly two lines — one comment and one call — and the call is inside the
fieldsToShow.forEachof the object-schema path. Theif (cols)branch aboveit returns from either of its two arms before that point and contains no
checkFieldof its own.Why it is not obviously exploitable, and why that is not the same as fine
Identical to the argument on objectui#6723.
ListView, the dominant host,filters its own
effectiveFieldsthroughcheckFieldbefore forwarding, andthat filtered array is exactly what it sends as
columns— so throughListViewthe gate is applied host-side and this omission is redundant.ListView.tsx's ownhasAuthoredColumnsdocblock already states the propertyas a known fact ("
ObjectGridre-applies FLS only on the DERIVED path, not onthe explicit-columns one"), and objectui#6598 relies on it deliberately: when
the field gate removes every authored column,
ListViewkeeps sending the emptyprojection precisely because the grid would not re-check.
The exposure, if any, is the same shape objectui#6723 described: a direct
ObjectGrid schema={{ objectName, columns }}composition, or a future host thatforwards an authored projection unfiltered. Unlike the inline-data case, this
one does not even need the host to supply rows — the
columnspath runs whetherthe grid fetches or not.
Why this is a decision and not a mechanical follow-up
The objectui#6723 ruling took defence in depth over "the host owns it", on the
stated ground that a security invariant must not rest on every future host
having read the docs. That reasoning does not distinguish
fieldsfromcolumns, which is why this is worth putting in front of triage rather thanassuming either answer. But two things differ enough that it is a separate call:
ListColumn[]. Theobjectui#6723 fix judges only keys the object declares, so host-joined and
derived keys survive. A
ListColumncarries more than a name (label,link,action,prefix,width), and a column whosefieldthe objectdoes not declare is a legitimate authored derived column. The same limit
should apply, but "which key is judged" has to be read through
columnIdentity/resolvesToDataColumnrather than off a bare string.hasAuthoredColumnspredicate and the test that pins it are written around"the explicit-columns path does not re-check". Adding the check does not
invalidate that predicate — it stays correct for a different reason — but the
pin's stated rationale would need rewriting in the same change, and that is
the kind of thing that should be decided rather than done in passing.
Not measured in a browser: this is a code reading plus the ablation run on the
objectui#6723 branch. Severity is deliberately left to triage.
Related: objectui#6723 (the inline-data half, fixed), objectui#6598,
objectui#6677.
Generated by Claude Code