Skip to content

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

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions