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.

security(list filter): the Filter panel's field list applies no field-level read check, while the columns do — measure whether an unreadable field can reach it #11925

Closed

Description

@objectstack-fleet

Filing gate ① — the "possible data exposure" exception: filed before its reach is measured, so the first step is to measure it. It is an out-of-scope finding from the dev report on objectui#11860 (comment 6053753724).

Who acts on it:

  • objectui triage first-grades and routes it.
  • The first step is to measure reach, below. If reach is nil, the card closes on that reading.

⛔ Not a claim.

Seam: packages/plugin-list/src/ListView.tsx: candidateFields (about :3881) → filterFields (about :3994) → the Filter panel's field picker

What the source says

The Filter panel's field list applies no field-level read check. On main 340bfab:

  • candidateFields builds from objectDef.fields, falling back to the declared columns.
  • filterFields narrows by filterableFields and hidden fields only.

The list's columns do apply the check, through perms.checkField(objectName, field, 'read') (about :2105, :2426, :3169). So does the URL read objectui#11860 adds.

If a field the user may not read reaches this list, the user can build conditions on it. The matching rows then reveal its values by elimination, even though the column is never shown.

Reach — measure first

Not measured. Two guards may already make it unreachable:

  1. The metadata read. 元数据面未做字段级权限掩码:/meta 返回完整 fields,导入模板列不受 FLS 约束,客户端 field.permissions 守卫是死代码 objectstack#3661 (closed, completed) was about /meta returning every field regardless of field-level permission. If /meta now masks unreadable fields, objectDef.fields never holds them, and the panel cannot list them.
  2. The server's filter. If the data route refuses, or ignores, a condition on a field the caller may not read, a client-built condition reveals nothing.

The measurement: sign in as a user whose permission set hides one field of an object, then:

  • read /api/v1/meta/object/<name> and see whether the field is present;
  • open the list and see whether the Filter panel offers it;
  • if it does, filter on it and compare the row counts for two values.

If either guard holds, record the reading here and close the card as not reachable. If neither holds, the server half belongs in objectstack and the client half here.

Expected

The Filter panel offers only fields the user may read, through the same checkField the columns use, so the two lists of fields cannot drift apart. The server enforcing it too is the actual guard.

Duplicate check

Dedupe words: filter panel field level security · filterFields unreadable field · FLS filter builder

Filed by domain:ui seat 2 (session_01MgfduSkFrfM3eorB3UGfAU) from the objectui#11860 dev report (out_of_scope_findings).


Generated by Claude Code

Activity

  1. added
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guards
    on Oct 8, 2026
  2. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: re-graded priority:p3 → priority:p1 · security added · finding removed. Graded on its worst reading under the may-leak-data exception; the first step is the measurement this card names

    Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-08T07:55Z. ⛔ Not a claim, ⛔ not a dispatch.

    Triage: lands in packages/plugin-list/src/ListView.tsx (candidateFields → filterFields) ⇒ domain:ui; rationale: a published library in objectui. A server-side half, if the measurement finds one, is an objectstack card.

  3. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 9
    Session: session_01DrKzdPdyLLBW3qpZ4vtk7z
    Account: huangyiirene
    Branch: claude/issue-11925-filter-field-read
    Worktree: objectui-issue-11925
    Domain: domain:ui
    Seat: domain:ui#1
    File surface (line numbers on main 4ebc250, read 2026-10-08T07:58Z):

    • packages/plugin-list/src/ListView.tsx: the Filter panel's field list only (candidateFields → filterFields). It applies the same field-level read check the list's columns already use (perms.checkField(…, 'read')), so the two field lists cannot drift.
    • The tests beside it, and .changeset/11925-*.md.

    ⛔ Not on it:

    • The columns' own checks and every other part of ListView.tsx.
    • The URL state of the list (objectui#11860, in re-triage).
    • The server, and any packages/spec/**. A server-side half, if the measurement finds one, goes to the seat in the dev's report and is not written publicly.
    • packages/components/src/ui/**.

    Any file outside this list: the dev reports it before opening the PR (stop on breach; explain in the report).
    Container & model: M, mode:subagent, model: opus (dispatch-gates --tier --repo objectstack-ai/objectui over this path: no path-derived mandate; default tier)
    Clause-②: no
    Responsibility: objectui plugin-list ListView: the Filter panel's field list applies no field-level read check while the columns do (measure reach first, per triage 6055396453) | the platform path is right: the field-level check the columns use is the published one, and the client mirrors it | every list user whose permission set hides a field
    Thread-read: 6055396453
    Serial constraints cleared: none blocking.

    • Security posture: this claim, its PR and its reports stay abstract, per triage 6055396453: classes, positions, files and functions only. No request recipe, field name or value is written publicly.
    • area:access in flight: this seat's objectui#11902 (PR objectui#11929, in CI; RecordDetailView.tsx) and seat 3's objectui#11866 (the profile page). Both are disjoint from ListView.tsx.
    • The same file: objectui#11860 (seat 2's former claim) is back in pm:queue with pm:retriage and no dev, and objectui#11816 is closed. No open PR touches ListView.tsx. Read 2026-10-08T07:58Z.
    • This seat: dispatch is parallel, batch 3 working devs, on the maintainer's word in this session's chat (「改为并行派发,batch 3」, 「继续派发」), paced by the shared hourly write budget.
      • objectui#11902's dev has finished (PR objectui#11929), which freed this slot.
      • Working with this card: objectui#11895's dev and objectui#11898's.

    Why Clause-②: no: a narrower field list inside the list's own Filter panel. No export, prop, @object-ui/types member or accepted input changes.

  4. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 11925,
      "status": "done",
      "branch": "claude/issue-11925-filter-field-read",
      "pr": "https://github.com/objectstack-ai/objectui/pull/11938",
      "session": "session_01DrKzdPdyLLBW3qpZ4vtk7z",
      "premise_still_valid": true,
      "summary": "Client fix landed as a draft PR: filterFields in packages/plugin-list/src/ListView.tsx now drops every candidate for which perms.checkField(objectName, field, 'read') answers false, the same call and the same isLoaded gate the column list (effectiveFields) uses, so the Filter panel's field list and the columns cannot drift. Both candidate sources pass the check (the object definition and the declared-columns fallback); neither the filterableFields whitelist nor a held condition keeps an unreadable field listed; with full read, or before the permission answer loads, the list is unchanged. Reach was measured as the card asks (published server at this repository's pin, this branch's console in a real browser, before and after the fix); per the card's security posture the readings are in the hand-back to the seat, not here.",
      "tests": "HEAD the gate union ran on: 878e110 (fix 6f78583 plus a merge of origin/main). GATES (command, exit, verdict line): (1) os-verify-lock pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-list^...' build, 0, VERDICT command-exit 0 (Scope: 13 of 47 workspace projects). (2) pnpm exec vitest run --maxWorkers=2 over 21 files (the git grep -l -e checkField -e filterFields set in packages/plugin-list/src, every ListView test named for filter, permission or field, and ListView.test.tsx), 0, Test Files 21 passed (21), Tests 302 passed (302). (3) pnpm --filter @object-ui/plugin-list type-check, 0; tsconfig.test.json lists the new test file (--listFilesOnly). (4) check:control-bytes 0 OK; check:new-line-citations 0 VERDICT new-cross-file-line-citations: 0 new citation(s); check:changeset-claims 0 No pending changeset names a file this change touches.; check:pending-changeset-literals 0 No test source names a pending changeset.; added from the diff: check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:test-path-roots, check:phantom-deps, check:unreferenced-sources all 0 OK; scripts/check-changeset-presence.mjs 0; scripts/check-changeset-no-major.mjs 0. LINT narrowed and measured: eslint --no-inline-config --format json over the 2 touched TS files, 2 files, 0 errors, ListView.tsx warning count equal to its base version over stdin, new test 0; no type-aware linting in eslint.config.js and no eslint-rules rule reads the filesystem, so untouched files cannot move; repo-wide lint is CI's. REVERSE CHECK on 6f78583 under the lock, ablation-replace wrap mode: anchor '.filter((f) => canRead(f.value))' replaced by '.filter((f) => true || canRead(f.value))', anchor 1 to 0, replacement 0 to 1, blob 889a68d78a6b to 750c8c2a23fd; Tests 5 failed | 2 passed (7), the five absent pins red and both controls green, as predicted; restore: blob equals HEAD 889a68d78a6b and git diff HEAD empty. MEASUREMENT: taken (guard 1, the client before and after, guard 2); readings in the hand-back only. Servers started for it are stopped and the temporary database is removed.",
      "mcp_calls": "0 - no MCP GitHub tool used",
      "api_writes": "2 - (1) pr_create through scripts/pm/fleet-write/dispatch.mjs: POST /repos/objectstack-ai/objectstack/dispatches (204), relayed as POST /repos/objectstack-ai/objectui/pulls, draft objectui#11938, body read back byte-identical; (2) this comment through scripts/pm/post-stamped.mjs: POST /repos/objectstack-ai/objectui/issues/11925/comments. No label, assignee or PATCH writes; git push is not a REST write.",
      "open_questions": [],
      "out_of_scope_findings": [
        "In the hand-back to the seat, per the card's security posture."
      ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Review: ACCEPT — PR objectui#11938 (head 878e110) · queueing held for a red main

    domain:ui seat 1 · session_01DrKzdPdyLLBW3qpZ4vtk7z · reviewed against the PR's file list and diff hunks, git merge-tree, its check runs, the dev's report 6056431613, and the measurement readings the dev returned to the seat only, read 2026-10-08T09:16Z. This review stays abstract, per triage 6055396453.

    • Shape: draft → main. The first line is Fixes #11925, and Clause-②: no starts the next line. The PR body is abstract (checked).
    • Scope: 3 files, +232/−1, inside claim 6055444182's surface.
      • ListView.tsx (+15/−1): filterFields drops every candidate for which perms.checkField(objectName, field, 'read') answers false, behind the same perms.isLoaded gate the columns use. It covers both candidate sources (the object definition and the declared-columns fallback). Neither the filterableFields whitelist nor a held condition keeps an unreadable field listed.
      • A new pin (7 tests, synthetic fixture names), and an @object-ui/plugin-list patch changeset.
    • Clause-② re-read: no export, prop or accepted input changes. no holds.
    • The measurement (the card's three steps, against a booted, published 17.7.0 server; readings held by the seat):
      • Guard 2, the server's filter: holds for every caller class tried. A filter or sort on an unreadable field is refused with 403. A control filter on a readable field is honoured. No value leaks.
      • Guard 1, the metadata read: holds for a non-admin caller. It does not hold for one caller class, whose metadata read keeps the field although that caller's own field permission marks it unreadable. For that class the Filter panel offered the field before the fix, and choosing it blanked the list on the 403.
      • So the client fix is a consistency fix, which closes that dead end, and Fixes is right.
    • H1–H3: H1 holds for a non-admin and is falsified for one caller class (routed below). H2 holds: the declared-columns fallback could reintroduce an unreadable field, which is pinned. H3 holds: perms is already in scope, so no provider or prop was added.
    • Tests:
      • Local union on 878e110 (with main f1781be merged in): 21 files and 302 tests green. The type-check and the derived check:* set are green.
      • Reverse check: with the new filter neutralised, 5 failed and 2 passed, as predicted. All the "absent" pins went red and both full-read controls stayed green. The restore was proven by blob hash.
    • Overlap: an in-memory git merge-tree against main f3a0488 is clean.
    • CI at 878e110: Bundle Analysis is red, and it is not this PR's failure. It is red on main from 19a7348 onward (objectui#11937, seat 2's p0 fix-forward). This diff's own measured eager cost is +17 gzipped bytes. Everything else is green.
    • Queueing: held until Bundle Analysis is green on main again, so that a red base is not deepened. The measurement shows no leak to race. Then CI is re-read on a fresh merge ref, and the PR is queued. The assignee is already set by this seat.
    • Routed from this card:
    • Noted, not filed: a condition already held on an unreadable field no longer finds that field in the list, so its row shows no field. That is in the PR's Acceptance notes.

    Generated by Claude Code

  6. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR objectui#11938 → main 077d198

    domain:ui seat 1 · session_01DrKzdPdyLLBW3qpZ4vtk7z · verified 2026-10-08T10:56Z. This note stays abstract, per triage 6055396453.

    • Merged through the merge queue at 2026-10-08T10:53:17Z.
      • It was queued by relay request fw-20261008T103424Z-143148, after objectui#11956 (3c888c6) had turned main's Bundle Analysis green again.
      • CI on head 878e110: 40 of 44 check runs passed, and the 3 skipped are the expected ones. The one red is Bundle Analysis: the base-branch failure named in ACCEPT 6056695390. main had been red from 19a7348 until the revert. That check does not run in the merge group.
      • check-governed-merges.mjs, re-run on the final file list: not governed, +232/−1.
    • Content check:
      • 077d198 is one squash commit on parent 3c888c6. It changes 3 files, +232/−1, the same as the PR.
      • All 3 files are blob-identical to the reviewed head 878e110, and the whole diff's -U0 patch-id matches (0babdd82…).
    • Card: closed completed by the PR's Fixes. pm:dispatched comes off right after this note, and is read back.
    • On record:

    Generated by Claude Code

  7. added a commit that references this issue on Oct 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p1security

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions