Skip to content

fix(ui): tighten clipboard prefix matching to prevent sibling row lea…#16742

Open
dawndarkness wants to merge 1 commit into
payloadcms:mainfrom
dawndarkness:fix/clipboard-prefix-collision
Open

fix(ui): tighten clipboard prefix matching to prevent sibling row lea…#16742
dawndarkness wants to merge 1 commit into
payloadcms:mainfrom
dawndarkness:fix/clipboard-prefix-collision

Conversation

@dawndarkness
Copy link
Copy Markdown

@dawndarkness dawndarkness commented May 26, 2026

What?

mergeFormStateFromClipboard and its helper reduceFormStateByPath use key.startsWith(prefix) at three callsites without enforcing a path boundary. This leaks unrelated form-state keys into the clipboard on copy and into the merge target on paste.

Most visible failure mode: array fields with 10 or more rows. Copying row 1 silently bundles rows 10, 11, 12 into the clipboard (their paths all start with children.1). On paste, those leaked rows get rewritten to out-of-bounds indices like .50, .51, .52, surfacing as phantom rows the editor cannot remove. The field then fails validation and blocks save.

The same loose match also lets textual sibling fields cross-contaminate (children vs childrenOther) in the fromRowToField cleanup branch and in reduceFormStateByPath.

Why?

'children.1'.startsWith('children.1') matches 'children.10', 'children.11', etc. 'children'.startsWith('children') matches 'childrenOther'. Three callsites are affected:

  • reduceFormStateByPath's filter loop. Builds the clipboard payload, so the leak originates here.
  • mergeFormStateFromClipboard's paste loop predicate. Should drop unrelated keys; instead rewrites them to out-of-bounds paths.
  • mergeFormStateFromClipboard's fromRowToField cleanup branch. Can delete unrelated fields whose name shares a textual prefix.

How?

Added a small private helper requiring the candidate path to either equal the prefix or be followed by .:

function isStrictPathPrefix(key: string, prefix: string): boolean {
  return key === prefix || key.startsWith(`${prefix}.`)
}

Replaced startsWith at all three callsites. No behavior change for non-colliding paths.

Tests

New describe('prefix collision with multi-digit sibling indices') block in mergeFormStateFromClipboard.spec.ts covers:

  • reduceFormStateByPath with a 13-row array and rowIndex: 1: leaked siblings (.10, .11, .12) must be filtered out.
  • reduceFormStateByPath with childrenOther adjacent to children: textual sibling field must be filtered out.
  • mergeFormStateFromClipboard end-to-end with a pre-leaked clipboard payload: pasting row 1 into row 5 must not create .50, .51, .52.
  • mergeFormStateFromClipboard fromRowToField cleanup: must not delete unrelated childrenOther.* paths.

All four fail on main, all pass after the fix. Existing tests in the spec are unchanged and continue to pass.

Manual repro

  1. Collection with an array field permitting at least 13 rows.
  2. Populate 13 rows with distinctive titles.
  3. Row Copy on row 1.
  4. Paste into any other row.
  5. Phantom empty rows appear at out-of-bounds positions; field fails validation.

After the fix: paste updates only the target row.

Fixes #16741

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: clipboard copy/paste creates phantom out-of-bounds rows when array has 10+ siblings

1 participant