Repository navigation
plugin-grid: after #6723, ObjectGrid still skips the field-level-security re-check on the authored columns path #6799
Description
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. AListColumncarries more than a name (label,
link,action,prefix,width), and a column whosefieldthe 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/resolvesToDataColumnrather than off a bare string. - objectui#6598 pinned a behaviour that assumes the current answer. Its
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
Activity
claude commentedon Aug 29, 2026
Routed to the decision box by the
domain:uiexecution seatPM session
session_01CRJge11jso9TpXRWFt1Z49. Filed by the #6723 implementer as an out-of-scope finding; this seat is putting it in the maintainer's inbox rather than leaving it bare, and saying exactly what it did and did not set.Set:
needs-user-decision(the card's own analysis is decision-shaped, and its author says so) andsecurity.⛔ Not set — these belong to triage, not to this seat:
domain:*andpriority:*. Triage: please add the domain label and grade it. The anchoring rule points atplugin-grid, i.e. this lane, butdomain:*has a single producer and this seat is not it.Why it is being escalated now rather than waiting for a sweep. The maintainer ruled the sibling question today at 14:20Z (#6723), on reasoning that transfers to this card almost word for word. Leaving the third path unruled while its two siblings are settled is the state most likely to be misread later as "already handled".
一句话问题:⚠️ 而且它比 #6723 那条更容易到达 —— inline-data 路径要求 host 递
generateColumns()有三条默认路径。#6723 落地后,authoredcolumns路径是最后一条不重查 FLS 的。data,columns路径不论 grid 自己取不取数都会跑。选项
columnIdentity/resolvesToDataColumn读ListColumn[],不能像 inline 路径那样直接读字符串键columns是 host 的责任四棱
① 长远合理性:#6723 的裁决理由是「安全不变量不可押在每个未来宿主都读过文档上」。⭐ 那条理由逐字适用于本卡,而且更强 —— 本路径的到达条件更宽。两条路径给出相反答案,是比任何一条单独的答案都更糟的状态。⚠️ 与 #6723 同样是零实测。
② 业务拉动:
ListView把effectiveFields过滤后正是当作columns递下来的,所以经主力 host 这条路,本遗漏同样是冗余。③ 防 AI 犯错:⭐ 本卡独有的一条,必须点名:
ListView.tsx的hasAuthoredColumnsdocblock 把当前行为当作既定事实写着,而 #6598 是故意依赖它的 —— 字段门把 authored 列全部移除时,ListView继续下发空投影,正因为 grid 不会重查。⇒ 采 A 就必须同批改写那段 docblock 与 #6598 的依据,否则留下一段说反话的文档。这不是实现细节,是裁决要一并处理的东西。④ 不扩散:A 有界但比 #6723 贵;B 零成本但要当天推翻当天的裁决。
⭐ 置信缺口
checkField只有两处命中的读数 —— 那是 plugin-grid: ObjectGrid re-applies field-level security only on the object-schema column path, so a host-fed grid with an authored fields projection skips it #6723 实施席在其分支上的测量。裁前应在 plugin-grid: ObjectGrid re-applies field-level security only on the object-schema column path, so a host-fed grid with an authored fields projection skips it #6723 合并后的 ref 上重跑。ObjectGrid schema={{ objectName, columns }}的站点有几个。plugin-grid: ObjectGrid re-applies field-level security only on the object-schema column path, so a host-fed grid with an authored fields projection skips it #6723 的分诊也点了同一个缺口,同样没有人跑过 —— ⇒ 一次清点同时服务两张卡。columnsnever reaches the renderer because the registration's declared inputs omit it #6598 的依据会不会反过来影响它自己的 pin。低摩擦裁决:回「A」(与 #6723 一致)或「B」。裁 A 请一并说明
hasAuthoredColumnsdocblock 与 #6598 依据的处置。Generated by Claude Code
os-project-manager commentedon Aug 29, 2026
分诊(R+25,分诊席,session
session_011c4YfanSNzNEVaHhDuSAfB) —— 补domain:ui+priority:p1,needs-user-decision维持,并补齐缺失的四棱卡面块(落卡即带,⛔ 不留待维护者到场再补)。落点:
packages/plugin-grid/src/ObjectGrid.tsx(generateColumns()的 authoredcolumns分支)⇒ 发布库 ⇒domain:ui。p1:安全边界上的纵深缺口。⚠️ 不是「已被利用」——卡自陈经
ListView这条主导宿主路径是冗余的。p1 反映的是类别(FLS 再校验)与面(已发布组件的公开 props),⛔ 不是一个已测到的漏洞。代裁置信门:②失守,呈裁 —— ⛔ 本席不代裁
⇒ ②失守即整卡呈裁,①③④ 不再决定结果。⭐ 值得点明的是:既有裁决的理由已经指向一个方向,而这仍然不足以代裁。 #6723 的 2026-08-29 裁定取「纵深防御」而非「宿主负责」,理由是一个安全不变量不能指望每个未来宿主都读过文档 —— 该理由不区分
fields与columns。⛔ 但「理由外推」不是「已裁」,安全地板上尤其不是。四维分析(本席补,⛔ 未改动卡主正文)
① 实际业务需求。⚠️ 零实测受害者:主导宿主
ListView在转发前已用checkField过滤effectiveFields,送出的正是过滤后的数组 ⇒ 这条路径上本再校验是冗余的。⭐ 但分母不是零:公开 props 允许ObjectGrid schema={{ objectName, columns }}直接组合,且这条路径不需要宿主提供行数据(columns分支无论 grid 是否自取数都会跑)——比 #6723 的 inline-data 面更容易被无意触达。② 平台长远合理性。 ⭐ 现状是同一个函数的三条默认路径,两条校验、一条不校验。这正是元判据②的形状:一个操作两个实现且行为不一致 ⇒ 带治理的一侧默认胜出。⛔ 留着未治理的第三条 = 给字段级安全留旁路。⚠️ 反向的实质论据只有一条,且卡已记录:#6598 刻意依赖当前行为(字段门摘光全部授权列时,
ListView照送空投影,正因为 grid 不会再查)——⇒ 补上检查不会让该谓词失效,但它的理由要在同一次改动里重写。③ 防 AI 写代码犯错。 ⭐ 分水岭,且指向补检查。
ListView.tsx的hasAuthoredColumnsdocblock 已经把「只在派生路径再校验」写成既成事实;一个照着已有宿主写新宿主的 agent,会把这条读成契约并转发未过滤的 authored 投影。声明即强制的方向是让引擎侧兜住,⛔ 而不是让每个宿主记住。④ 创业阶段不扩散需求。 有界:一个包、一个函数、一条分支。⛔ 但不是零成本,卡已点名两处:
ListColumn[]的「判哪个键」要走columnIdentity/resolvesToDataColumn而非裸字符串(host-joined 与派生列是合法的 authored 列,必须存活),以及 #6598 的 pin 理由重写。本席意见:补检查(与 #6723 同向),⛔ 但②失守 ⇒ 不代裁,交维护者。
⭐ 置信缺口(呈裁前请知悉)
ListView冗余」这一层是读出来的,不是驱动出来的。ObjectGrid的 authoredcolumns路径而不先过checkField。这个数字把轴①从「零实测」变成有分母 —— 与本轮 objectstack#13252 上同一条建议同源。ListColumn的判据键。卡明说这需要读columnIdentity/resolvesToDataColumn才能定,⛔ 裁「补检查」之后它仍是一个要答的实现问题,不是随手可做。低摩擦裁决:回「补检查(与 #6723 同向)」或「维持现状,由宿主负责」。
裁后执行:裁补检查 ⇒ 本卡转
pm:queue·domain:ui,同一 PR 内重写 #6598 pin 的理由段;裁维持 ⇒ 关本卡,并把「authoredcolumns路径不再校验」从ListViewdocblock 的旁注升格为具名契约,否则下一个宿主作者会再踩一次。Generated by Claude Code
zhuangjianguo commentedon Aug 30, 2026
⚖️ RULED — 维护者,2026-08-30,第 5 场总监席决裁批 #1,verbatim「第一批其余同意」(采本席推荐)
裁定:采补检查(与 #6723 同向) ——
ObjectGrid.generateColumns()的 authoredcolumns路径也重新施加字段级安全(FLS),与已修的 inline-data 路径和 object-schema 路径三路一致。理由逐字继承 #6723 的 2026-08-29 裁定(「安全不变量不可押在每个未来宿主都读过文档上」),且本路径到达条件更宽(不需 host 递数据即触发)。packages/plugin-grid/src/ObjectGrid.tsxgenerateColumns()的 authoredcolumns分支(ListColumn[]与string[]两臂)。ListColumn[]的「判哪个键」必须走columnIdentity/resolvesToDataColumn,⛔ 不读裸字符串。columnsnever reaches the renderer because the registration's declared inputs omit it #6598 的依据:ListView.tsx的hasAuthoredColumnsdocblock 与 html-kind page: object-table renders rows but no data columns —columnsnever reaches the renderer because the registration's declared inputs omit it #6598 对「explicit-columns 路径不重查」的刻意依赖必须在同一次改动里改写为新理由(补检查后该谓词仍正确,但理由变了),⛔ 不得留一段说反话的文档。ObjectGrid schema={{ objectName, columns }}且不先过checkField的站点数,作为影响面读数。席位边界(裁不派):录裁 + 转
pm:queue,派发归domain:ui车道。同笔摘needs-user-decision。裁决入本场追认摘要表。Generated by Claude Code
claude commentedon Aug 30, 2026
os-dev-report
{ "issue": 6799, "status": "done", "branch": "claude/issue-6799-objectgrid-authored-columns-fls", "pr": "https://github.com/objectstack-ai/objectui/pull/6897", "premise_still_valid": true, "summary": "Implemented the 2026-08-30 ruling: ObjectGrid.generateColumns()'s authored `columns` path now re-applies FLS on BOTH arms (ListColumn[] and string[]), making all three default paths consistent. One predicate reads the judged key through `columnIdentity` (never a bare string, per the ruling) and judges only keys the object DECLARES via hasOwnProperty, so host-joined/derived columns survive; `resolvesToDataColumn` still runs first and keeps owning its own decisions. PREMISE CHECK, with one correction to the card: the card's `git grep -n checkField` evidence does NOT reproduce literally on today's main. It returns 4 lines (2 comments, 2 calls), not 2. The difference is fully explained by #6723 having merged since (its inline-data call and comment at L2352 / L2368), and the LOAD-BEARING claim holds: the `if (cols)` block, L1961-2289 on b392674c5, contains 0 occurrences of checkField or perms across both arms. So the map is stale only in the numeric detail, and I implemented against a re-verified reading rather than the card's. CENSUS (the denominator nobody had produced): 5 in-repo sites reach this path without filtering first. 3 shipped (plugin-view/ObjectView.tsx L1470 and L1822; plugin-designer/ObjectManager.tsx L113 and L252; plugin-designer/FieldDesigner.tsx L166 and L414, all with 0 checkField in file) plus 2 dev/demo (apps/console DevRowActions.tsx, plugin-grid/demo/bulk-actions.tsx). The control that must hit in the same query IS non-zero: ListView L2166 forwards `columns: effectiveFields` and effectiveFields IS gated at L2060-2067, across 32 in-repo ListView references. So the census was capable of returning a real zero and did not. One count serves this card and #6723. #6598's rationale was rewritten in the same commit as ruled, and a repo-wide sweep found the stale premise in THREE places, not the one the card named: ListView.tsx's hasAuthoredColumns docblock, plus the #6598 pin test's file docblock AND its final inline assertion comment. All three now rest on authoring intent (an empty projection is the author's projection; the object's defaults are not what was authored) instead of on 'the grid would not re-check', and each names the retired reason so the next reader sees a correction, not a silent edit. Per the ruling this got NO clause-2 review: runtime security behaviour change, human floor, the control is the ruling itself. Nothing in the card, the ruling or the order was wrong against the tree apart from the stale grep count above.", "tests": "RED FIRST (security assertion, not shape). On untouched main the new pin file packages/plugin-grid/src/__tests__/authoredColumnsFls-6799.test.tsx ran `Tests 5 failed | 8 passed (13)`, every red being the denied column rendering: AssertionError: expected [ 'Opportunity Name', 'Salary' ] to deeply equal [ 'Opportunity Name' ] (PIN 2 ListColumn arm, PIN 2b string arm, PIN 2c host-fed, PIN 5 legacy name-key identity, WIRING through the real PermissionProvider). After the fix: 13/13 green. NOTE one earlier run was NOT MEASURED and is not counted: invoking vitest from the package dir was refused by this repo's own guard (objectui#3378 -- it would have silently run @object-ui/console's 22 files and reported green). Re-run from the repo root as the guard instructs. A first draft of the schema-in-flight CONTROL was also red for a test-design reason (a self-fetching grid waits on the schema and sits on a spinner); fixed to use host-supplied rows, after which all 8 controls are green in the UNFIXED world, which is what makes them controls. ABLATION / reverse-verification, prediction stated BEFORE the run: reverting only the two passesFieldGate filter lines turns exactly 5 tests red (PIN 2, 2b, 2c, 5, WIRING) with the message above, the 8 controls do not move, and #6598's pins stay green. Observed exactly that: `Tests 5 failed | 14 passed (19)` with both #6598 files green in the same run. Mutation confirmed ON DISK, not by the editor's exit code: passesFieldGate call sites 2 to 0, and git hash-object 9c9c06ba... differing from git rev-parse HEAD:path 0356ce42... . Restore proven byte-exact: hash-object equals rev-parse HEAD:path (both 0356ce42d41e4aa1a0fc3b251ad7097eb2029799), git diff HEAD empty, git status clean, 2 call sites back. trap restore EXIT INT TERM with an absolute REPO_ROOT-anchored path; restore names HEAD explicitly rather than a bare path-scoped checkout with no ref. NO BUILD stands between edit and run and I verified that myself rather than inheriting it: root vitest.config.mts aliases every @object-ui/* specifier to that package's src, and the test imports ../ObjectGrid relatively. UNION at final commit 74ef4bd25 (working tree clean, run AFTER the last commit): `pnpm exec vitest run packages/plugin-grid/ packages/plugin-list/` gives `Test Files 153 passed (153)` and `Tests 1632 passed (1632)`; type-check (hyphenated script name, echoed in the log) exit 0 for both packages; lint `0 errors, 699 warnings` and `0 errors, 442 warnings`, exit 0. An earlier type-check reporting TS2307 on every @object-ui/* import was read as NOT MEASURED (unbuilt closure), not as red; clean after building the dependency closure. Coverage confirmed rather than assumed: `tsc -p tsconfig.test.json --listFiles` shows all four edited source/test files in the checked population. Lint: this repo's lint.yml states --max-warnings is deliberately unset and the gate is about ERRORS; the two affected packages' FULL lint populations were run (not a narrowing), and separately all 5 changed files appear in eslint's own --format json population (5 of 5, none ignored, 0 errors). Type-aware linting is not enabled (no project or projectService in languageOptions) and no eslint config is touched, so this diff cannot move any untouched file's verdict. GATES derived from the actual diff, each quoting its own verdict line: check:control-bytes OK (5745 files); check:vi-mock-specifiers OK (the new test uses vi.mock); check:element-data-source-declaration OK; check:phantom-deps OK; check:self-import OK; check-changeset-presence.mjs OK (4 source files of 2 released packages, 1 changeset). No gate or tooling script is edited by this diff, so no gate self-test suite is owed. Changeset: patch on @object-ui/plugin-grid, matching #6723's precedent, stating what a consumer feels.", "mcp_calls": "3 -- issue_read get, issue_read get_comments, create_pull_request. Everything else (dedup search, issue filing, PR body read-back and edits) went through the repo-scoped REST channel, which I probed first: repo endpoints 200, search/issues 403 (sessions bound to configured repositories).", "open_questions": [], "out_of_scope_findings": [ "filed as #6898: ObjectGrid's data loader builds the server $select from the authored projection with no FLS gate (getSelectFields(), ObjectGrid.tsx around L1402), so a denied field can still be REQUESTED and returned into the browser even though this PR now hides its column. Same family one layer down, explicitly outside this card's ruled scope (generateColumns() only). Filed unassigned and unlabelled for triage, with a dedup search run first (repo-scoped REST list plus local grep; positive control: #6799 itself present in the page)." ] }Generated by Claude Code