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.

bridgeListView's mapDensity indexes its table with an unguarded key, so rowHeight: 'toString' returns a function as the density #4442

Closed

Description

@yinlianghui

Found while implementing #4440 (converging @object-ui/core's rowHeightToDensityMode with this bridge). Not fixed there: packages/react/src/spec-bridge/** was outside that card's surface, and this is a different defect class from the coercion #4440 retired.

What

packages/react/src/spec-bridge/bridges/list-view.ts:

function mapDensity(
  rowHeight?: RowHeight,
): 'compact' | 'comfortable' | 'spacious' | undefined {
  if (!rowHeight) return undefined;
  return ROW_HEIGHT_TO_DENSITY[rowHeight];
}

ROW_HEIGHT_TO_DENSITY is a plain object literal, so the index reaches Object.prototype. The parameter is typed RowHeight, but the boundary a host's stored JSON actually crosses is SpecBridge.transformListView, whose parameter is any — that is the untyped entry PR #4439's own pins use.

So:

new SpecBridge().transformListView({ name: 'x', rowHeight: 'toString' }).density
// → [Function toString]

bridgeListView writes if (density) node.density = density, and a function is truthy, so the key IS written: a SchemaNode carrying a function where the renderer expects 'compact' | 'comfortable' | 'spacious'. Same for constructor, valueOf, hasOwnProperty, isPrototypeOf, propertyIsEnumerable, toLocaleString.

Measured directly, on the core twin which had the identical in-based hole before #4440 fixed it:

AssertionError: expected [Function toString] to be undefined
- Expected: undefined
+ Received: [Function toString]

Why it is filed rather than fixed in #4440

#4440's ruling scoped the work to rowHeightToDensityMode and its callers, and named packages/react/src/spec-bridge/** read-only for that dispatch. #4440's agreement pin (packages/react/src/spec-bridge/__tests__/RowHeightDensityAgreement.test.ts) therefore covers the off-spec spellings the card is about and deliberately excludes prototype keys, with a comment pointing here — because on those keys the two surfaces still disagree: core now abstains, the bridge returns a function.

Impact

No user hits this today. It needs a host to author or store the literal string toString (or another Object.prototype member) as a view's rowHeight; the three-repo sweep in PR #4439, re-run at newer HEADs for #4440, found no off-spec rowHeight authored anywhere at all. Grading it is triage's call, not the filer's — an unguarded prototype-chain index is a recognized bug class and the observable result is a function leaking into a schema node, which is why this is filed as a defect rather than as an observation.

The fix, when someone takes it

One line, matching what @object-ui/core now does (packages/core/src/utils/normalize-list-view.ts) and the repo's existing convention (Object.prototype.hasOwnProperty.call(...) — used in core/src/utils/freeze-schema.ts:135, app-shell/src/views/metadata-admin/predicate.ts:305, and six other places):

if (!rowHeight) return undefined;
if (!Object.prototype.hasOwnProperty.call(ROW_HEIGHT_TO_DENSITY, rowHeight)) return undefined;
return ROW_HEIGHT_TO_DENSITY[rowHeight];

Worth extending #4440's agreement pin to the prototype keys in the same PR, since that is exactly the assertion this currently breaks.


Generated by Claude Code

Activity

  1. self-assigned this
    on Aug 12, 2026
  2. yinlianghui commented on Aug 12, 2026

    @yinlianghui
    ContributorAuthor

    CLAIM — session session_017Qqyix2QcnpUC9XeYVDzx3 (objectui whole-repo seat), branch claude/issue-4442-bridge-prototype-guard.

    Ruling (delegated authority, product-long-term-stability; open veto window): dispatched as a straight fix, graded per the #4439/#4447 behavior-on-published precedent.

    1. Fix as the issue proposes: guard mapDensity's index with Object.prototype.hasOwnProperty.call(ROW_HEIGHT_TO_DENSITY, rowHeight) before indexing — the exact convention PR refactor(core): rowHeightToDensityMode abstains for an off-spec rowHeight, matching the spec bridge (#4440) #4447 just applied to the core twin (freeze-schema.ts:135 precedent, eight existing sites). This closes the last disagreement between the two rowHeight surfaces: off-spec input (including prototype-member spellings) now gets the same abstention on both. One resolver, one channel, one answer.
    2. Test half: extend the agreement pin packages/react/src/spec-bridge/__tests__/RowHeightDensityAgreement.test.ts — its two it.each lists deliberately exclude prototype keys with a comment pointing at this issue; per PR refactor(core): rowHeightToDensityMode abstains for an off-spec rowHeight, matching the spec bridge (#4440) #4447's own report, "extending the two it.each lists is the natural test half of that fix". Red-first: assert transformListView({ name: 'x', rowHeight: 'toString' }).density is undefined — pre-fix this returns [Function toString] (measured in the issue body), post-fix it abstains. Cover the other Object.prototype members in the same it.each rows.
    3. Grading: patch-level surface but observable behavior on a published transform moves (function-valued density → absent). Changeset patch unless the emitted .d.ts diff shows surface movement (it should not — the guard is runtime-only); if it does, escalate to minor with the change described in the body per the fix(i18n): retire the reader-less common.search key from the ten locale packs (#4392) #4403/fix(i18n): retire the orphaned report.editor.* namespace, 105 keys across ten packs (#4145) #4177 precedent.

    Sequencing gate: PR #4447 owns the agreement-pin file and is armed for auto-merge — the dev must poll for #4447 in origin/main before creating the worktree, then build the dependency closure.

    Surface (mutual exclusion): packages/react/src/spec-bridge/** (bridge + agreement pin) + one changeset file. No other in-flight card touches spec-bridge: #4045 (lint rule/button types), #4433 (calendar plugin), #4347 (coverage gate), #4325 (fields subpath) are all disjoint.


    Generated by Claude Code


    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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions