Skip to content

record:related_list: redacting every authored column defeats redaction — the empty array falls through to auto-derived columns no redact list touches #9053

Description

@claude

The measurement

Found while measuring the blast radius of objectui#8793 (which closes the block's
own fail-open branch). This is a SECOND, independent hole on the same path, and
it is reachable today, with entirely spec-canonical authoring — objectui#8793
does not cause it and does not fix it.

record:related_list filters its authored columns against the allow-list it
builds from enforceFieldSecurity / redactFields. When that filter removes
every member, it hands RelatedList an EMPTY array — and RelatedList reads
an empty array as "no columns were authored":

// packages/plugin-detail/src/RelatedList.tsx, effectiveColumns
if (columns && columns.length > 0) {
  const normalized = columns.map(normalizeColumn);
  return pruneEmpty(filterFLS(filterFK(normalized)));
}
// ...falls through to the highlightFields / heuristic auto-derivation

The auto-derived path runs filterFK, pruneEmpty and filterFLS — but the
block's redactFields list is not in scope there at all. It is a block-level
concept that never reaches RelatedList. So the redacted field comes back.

Reproduction (measured, real table, real cells)

Block schema: columns: [{ field: 'salary', label: 'Salary' }],
redactFields: ['salary']; child object schema declares subject and salary;
one row { subject: 'Fix the pump', salary: '90000' }.

Rendered body cells, read from the DOM:

[ 'Fix the pump', '90000' ]

90000 is the value the author redacted. Redacting the ONLY authored column
turns redaction into a no-op — worse than a no-op, because the fallback may
surface fields the author never listed.

Why it is not folded into objectui#8793

That card names one branch — an entry whose identity does not resolve was KEPT —
and its repair strictly narrows the authored array. This defect lives one layer
down, in what RelatedList does with an empty array, and it fires on authoring
that objectui#8793 never touches ({ field: 'salary' } resolves fine). Widening
that PR to cover it would have made the fail-closed change and a fallback-policy
change one diff.

⚠️ The two do interact, and that is worth stating: after objectui#8793 an
all-unresolvable authored column set also empties the array, so it reaches this
fallback too. Measured, same harness, same result — [ 'Fix the pump', '90000' ].

Shape of the fix (not a ruling)

The question is what an emptied-by-security column set MEANS. Two readings, and
they differ for users:

  • A: an empty result after filtering is "the author authored columns and none of
    them survived" — distinct from "no columns authored", so the list should render
    its empty/denied state rather than deriving a replacement set.
  • B: the derivation is legitimate, and redactFields (plus whatever else the
    block decides) must be pushed down so the derived set is filtered by the same
    policy as the authored one.

B is the shape the block's own comment already assumes (FLS is pushed down, via
perms.checkField); redaction simply never was. A is the smaller change and the
one that keeps the two vocabularies apart.

Filed by the dev seat working objectui#8793; scope fence held, no diff here.


Generated by Claude Code

Activity

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

    bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2security

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions