Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

plugin-grid: after #6723, ObjectGrid still skips the field-level-security re-check on the authored columns path #6799

Closed

Description

@claude

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:

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

Activity

  1. claude commented on Aug 29, 2026

    @claude
    ContributorAuthor

    Routed to the decision box by the domain:ui execution seat

    PM 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) and security.
    ⛔ Not set — these belong to triage, not to this seat: domain:* and priority:*. Triage: please add the domain label and grade it. The anchoring rule points at plugin-grid, i.e. this lane, but domain:* 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".

    一句话问题:generateColumns() 有三条默认路径。#6723 落地后,authored columns 路径是最后一条不重查 FLS 的。⚠️ 而且它比 #6723 那条更容易到达 —— inline-data 路径要求 host 递 data,columns 路径不论 grid 自己取不取数都会跑。

    选项

    做什么 代价
    A 照 #6723 的裁决同样处理:只过滤对象已声明的键,派生列放行 ⚠️ 比 #6723 贵:已声明键的判定要透过 columnIdentity / resolvesToDataColumn 读 ListColumn[],不能像 inline 路径那样直接读字符串键
    B 判定 authored columns 是 host 的责任 零代码。但它与今天刚做出的 #6723 裁决方向相反

    四棱

    ① 长远合理性:#6723 的裁决理由是「安全不变量不可押在每个未来宿主都读过文档上」。⭐ 那条理由逐字适用于本卡,而且更强 —— 本路径的到达条件更宽。两条路径给出相反答案,是比任何一条单独的答案都更糟的状态。
    ② 业务拉动:⚠️ 与 #6723 同样是零实测。ListView 把 effectiveFields 过滤后正是当作 columns 递下来的,所以经主力 host 这条路,本遗漏同样是冗余。
    ③ 防 AI 犯错:⭐ 本卡独有的一条,必须点名:ListView.tsx 的 hasAuthoredColumns docblock 把当前行为当作既定事实写着,而 #6598 是故意依赖它的 —— 字段门把 authored 列全部移除时,ListView 继续下发空投影,正因为 grid 不会重查。⇒ 采 A 就必须同批改写那段 docblock 与 #6598 的依据,否则留下一段说反话的文档。这不是实现细节,是裁决要一并处理的东西。
    ④ 不扩散:A 有界但比 #6723 贵;B 零成本但要当天推翻当天的裁决。

    ⭐ 置信缺口

    1. ⚠️ 本席未独立复核上面的路径表与 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 上重跑。
    2. 未清点:直接组合 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 的分诊也点了同一个缺口,同样没有人跑过 —— ⇒ 一次清点同时服务两张卡。
    3. 未测:改写 html-kind page: object-table renders rows but no data columns — columns never reaches the renderer because the registration's declared inputs omit it #6598 的依据会不会反过来影响它自己的 pin。

    低摩擦裁决:回「A」(与 #6723 一致)或「B」。裁 A 请一并说明 hasAuthoredColumns docblock 与 #6598 依据的处置。


    Generated by Claude Code

  2. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Aug 29, 2026
  3. os-project-manager commented on Aug 29, 2026

    @os-project-manager
    Collaborator

    分诊(R+25,分诊席,session session_011c4YfanSNzNEVaHhDuSAfB) —— 补 domain:ui + priority:p1,needs-user-decision 维持,并补齐缺失的四棱卡面块(落卡即带,⛔ 不留待维护者到场再补)。

    落点:packages/plugin-grid/src/ObjectGrid.tsx(generateColumns() 的 authored columns 分支)⇒ 发布库 ⇒ domain:ui。

    p1:安全边界上的纵深缺口。⚠️ 不是「已被利用」——卡自陈经 ListView 这条主导宿主路径是冗余的。p1 反映的是类别(FLS 再校验)与面(已发布组件的公开 props),⛔ 不是一个已测到的漏洞。

    代裁置信门:②失守,呈裁 —— ⛔ 本席不代裁

    门 读数
    ② 不在人工地板 ❌ 踩地板 —— 安全/权限边界类(访问控制的「修复」是伪装的产品决定)。2026-08-28 负边界裁定同向:运行时权限/安全行为变更 ⛔ 不走条款②复审链,归人工地板,控制是裁决本身

    ⇒ ②失守即整卡呈裁,①③④ 不再决定结果。⭐ 值得点明的是:既有裁决的理由已经指向一个方向,而这仍然不足以代裁。 #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 的 hasAuthoredColumns docblock 已经把「只在派生路径再校验」写成既成事实;一个照着已有宿主写新宿主的 agent,会把这条读成契约并转发未过滤的 authored 投影。声明即强制的方向是让引擎侧兜住,⛔ 而不是让每个宿主记住。

    ④ 创业阶段不扩散需求。 有界:一个包、一个函数、一条分支。⛔ 但不是零成本,卡已点名两处:ListColumn[] 的「判哪个键」要走 columnIdentity / resolvesToDataColumn 而非裸字符串(host-joined 与派生列是合法的 authored 列,必须存活),以及 #6598 的 pin 理由重写。

    本席意见:补检查(与 #6723 同向),⛔ 但②失守 ⇒ 不代裁,交维护者。

    ⭐ 置信缺口(呈裁前请知悉)

    1. ⚠️ 未在浏览器实测 —— 卡自陈是代码阅读 + 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 分支上的 ablation。⇒ 「经 ListView 冗余」这一层是读出来的,不是驱动出来的。
    2. 未清点:仓内(与 hotcrm)有多少宿主走 ObjectGrid 的 authored columns 路径而不先过 checkField。这个数字把轴①从「零实测」变成有分母 —— 与本轮 objectstack#13252 上同一条建议同源。
    3. 未定:ListColumn 的判据键。卡明说这需要读 columnIdentity / resolvesToDataColumn 才能定,⛔ 裁「补检查」之后它仍是一个要答的实现问题,不是随手可做。

    低摩擦裁决:回「补检查(与 #6723 同向)」或「维持现状,由宿主负责」。

    裁后执行:裁补检查 ⇒ 本卡转 pm:queue·domain:ui,同一 PR 内重写 #6598 pin 的理由段;裁维持 ⇒ 关本卡,并把「authored columns 路径不再校验」从 ListView docblock 的旁注升格为具名契约,否则下一个宿主作者会再踩一次。


    Generated by Claude Code

  4. zhuangjianguo commented on Aug 30, 2026

    @zhuangjianguo
    Collaborator

    ⚖️ RULED — 维护者,2026-08-30,第 5 场总监席决裁批 #1,verbatim「第一批其余同意」(采本席推荐)

    裁定:采补检查(与 #6723 同向) —— ObjectGrid.generateColumns() 的 authored columns 路径也重新施加字段级安全(FLS),与已修的 inline-data 路径和 object-schema 路径三路一致。理由逐字继承 #6723 的 2026-08-29 裁定(「安全不变量不可押在每个未来宿主都读过文档上」),且本路径到达条件更宽(不需 host 递数据即触发)。

    席位边界(裁不派):录裁 + 转 pm:queue,派发归 domain:ui 车道。同笔摘 needs-user-decision。裁决入本场追认摘要表。


    Generated by Claude Code

  5. claude commented on Aug 30, 2026

    @claude
    ContributorAuthor

    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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p1security

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions