Skip to content

fix(config): use Object.hasOwn instead of in operator in unsetConfigValueAtPath#99148

Closed
zenglingbiao wants to merge 4 commits into
openclaw:mainfrom
zenglingbiao:fix/prototype-pollution-config-paths
Closed

fix(config): use Object.hasOwn instead of in operator in unsetConfigValueAtPath#99148
zenglingbiao wants to merge 4 commits into
openclaw:mainfrom
zenglingbiao:fix/prototype-pollution-config-paths

Conversation

@zenglingbiao

@zenglingbiao zenglingbiao commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Use Object.hasOwn() instead of the in operator in unsetConfigValueAtPath() to prevent prototype chain pollution when checking whether a config key exists before deletion.

What Problem This Solves

When calling unsetConfigValueAtPath(root, ["foo", "toString"]) on a config object that does not have a toString key, the function returns true (claiming success) because "toString" in cursor matches Object.prototype.toString.

The function lies to its caller — it reports "key found and deleted" when the key never existed. This can cause config management tools (CLI config unset, config merge logic) to silently skip error handling for non-existent keys.

Code evidence: In src/config/config-paths.ts:63, if (!(leafKey in cursor)) checks the prototype chain. cursor is a plain PathNode object (Record<string, unknown>), and leafKey comes from user-provided dot-notation paths like foo.toString.

Root Cause

  • The in operator checks the entire prototype chain, not just own properties
  • Object.hasOwn() (ES2022+) correctly checks only the object's own properties
  • Object.hasOwn is already used 15+ times across the codebase — this was a missed instance

Why This Fix

Evidence

  • Before: unsetConfigValueAtPath({foo:{bar:"v"}}, ["foo","toString"]) returns true (wrong — key doesn't exist)
  • After: returns false (correct — key is not an own property)
  • All 3 prototype keys tested: toString, constructor, valueOf — all correctly return false
  • Existing non-prototype key deletion: unaffected
  • Parent pruning: unaffected

Real Behavior Proof

Before (on main)

$ node --import tsx proof.mjs
FAIL non-existent "toString" returns false (got: true, owns "toString": false)
FAIL non-existent "constructor" returns false (got: true, owns "constructor": false)
FAIL non-existent "valueOf" returns false (got: true, owns "valueOf": false)
FAIL non-existent "hasOwnProperty" returns false (got: true, owns "hasOwnProperty": false)
PASS existing "toString" deleted successfully
PASS "toString" no longer own property
PASS sibling key "bar" preserved
PASS parent "foo" preserved after no-op unset
PASS sibling "bar" preserved after no-op unset

4 FAILED

After (with fix)

$ node --import tsx proof.mjs
PASS non-existent "toString" returns false (got: false, owns "toString": false)
PASS non-existent "constructor" returns false (got: false, owns "constructor": false)
PASS non-existent "valueOf" returns false (got: false, owns "valueOf": false)
PASS non-existent "hasOwnProperty" returns false (got: false, owns "hasOwnProperty": false)
PASS existing "toString" deleted successfully
PASS "toString" no longer own property
PASS sibling key "bar" preserved
PASS parent "foo" preserved after no-op unset
PASS sibling "bar" preserved after no-op unset

ALL PASS

Tests and Validation

  • Regression test added: src/config/config-paths.test.ts covers 3 prototype keys + normal deletion + parent pruning
  • Fix verified with standalone node --import tsx proof script
  • Diff: 1 line changed in source + regression test
  • Proof: 4 prototype keys (toString/constructor/valueOf/hasOwnProperty) all change from FAIL → PASS, regression tests pass

@clawsweeper

clawsweeper Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Thanks for the context here. I swept through the related work, and this is now duplicate or superseded.

Keep open: current main still has the inherited-property false positive, while this PR is a focused helper/test fix with sufficient terminal proof and no blocking findings.

Root-cause cluster
Relationship: canonical
Canonical: #99148
Summary: This PR is the canonical item for the unsetConfigValueAtPath own-property fix; related PRs cover adjacent helpers with the same JavaScript pattern.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Canonical path: Close this PR as superseded by #99152.

So I’m closing this here and keeping the remaining discussion on #99152.

Review details

Best possible solution:

Close this PR as superseded by #99152.

Do we have a high-confidence way to reproduce the issue?

Yes. Source inspection shows current main reaches leafKey in cursor, so unsetConfigValueAtPath({ foo: { bar: 'value' } }, ['foo', 'toString']) can report a missing own key as present; I did not execute tests in this read-only checkout.

Is this the best way to solve the issue?

Yes. Fixing the shared helper's leaf existence check is narrower and cleaner than adding caller-side special cases, and it matches sibling config mutation code that already uses own-property checks.

Security review:

Security review cleared: The diff narrows an object-key existence check and adds tests; it does not change dependencies, workflows, credentials, or other supply-chain surfaces.

AGENTS.md: found and applied where relevant.

What I checked:

  • linked superseding PR: fix(config): use Object.hasOwn instead of in operator in restoreOriginalValueOrThrow #99152 (fix(config): use Object.hasOwn instead of in operator in restoreOriginalValueOrThrow) is merged at 2026-07-03T11:32:26Z.
  • cluster evidence: the durable review links that PR in the work cluster or recommended risk path.
  • no human follow-up: live comments and timeline hydrated by apply contain no non-automation activity after the ClawSweeper review.

Likely related people:

  • steipete: GitHub commit metadata maps the original /config chat config update and later blocked-key guard unification touching src/config/config-paths.ts to this login. (role: introduced behavior and adjacent owner; confidence: high; commits: 8b579c91a555, 08e020881d6e; files: src/config/config-paths.ts, src/auto-reply/reply/config-mutations.ts, src/auto-reply/reply/commands-config.ts)
  • RomneyDa: The current shallow checkout blames the helper lines to merged refactor commit 80d129212cf8, so this is a recent-history routing signal rather than root-cause ownership. (role: recent area contributor; confidence: medium; commits: 80d129212cf8; files: src/config/config-paths.ts, src/auto-reply/reply/config-mutations.ts)
  • zenglingbiao: The contributor authored merged PR fix(config): use Object.hasOwn instead of in operator in restoreOriginalValueOrThrow #99152, which fixed the same own-property pattern in restoreOriginalValueOrThrow on current main. (role: recent adjacent config contributor; confidence: medium; commits: 459b93e3ddaf; files: src/config/redact-snapshot.ts, src/config/redact-snapshot.restore.test.ts)

Codex review notes: model internal, reasoning high; reviewed against d0b84a955cff.

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. P2 Normal backlog priority with limited blast radius. labels Jul 2, 2026
@zenglingbiao

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@clawsweeper clawsweeper Bot added rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. labels Jul 2, 2026
@zenglingbiao
zenglingbiao force-pushed the fix/prototype-pollution-config-paths branch from bb05c0a to c7bfa66 Compare July 4, 2026 04:59
@zenglingbiao
zenglingbiao force-pushed the fix/prototype-pollution-config-paths branch from c7bfa66 to e302b10 Compare July 4, 2026 05:01
@clawsweeper clawsweeper Bot removed proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. labels Jul 4, 2026
@zenglingbiao

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper could not start a re-review for this item.

Reason: re-review requires an open issue or PR.

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. labels Jul 4, 2026
@zenglingbiao

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper clawsweeper Bot closed this Jul 4, 2026
@clawsweeper

clawsweeper Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

🦞✅
ClawSweeper autoclose is complete.

Reason: structured ClawSweeper close marker: close-required (sha=e302b106bda7371ee7975443a61c11d072d19109)

Closed:

@zenglingbiao

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review — This PR fixes unsetConfigValueAtPath in src/config/config-paths.ts. The merged #99152 only fixed restoreOriginalValueOrThrow in src/config/redact-snapshot.ts. These are different functions in different files — current main still has on config-paths.ts:63. This PR is not superseded by #99152.

@vincentkoc

Copy link
Copy Markdown
Member

Reopening: the prior supersession close was incorrect. #99152 changed a different function and does not fix this behavior; current main still reports inherited keys as present.

The reviewed invariant is slightly broader than this fork branch: config path reads, writes, and unsets must traverse own serialized data only. Because this fork has maintainer edits disabled, I cannot push the complete repair to the contributor branch. I am preparing a maintainer-owned replacement from this commit, preserving @zenglingbiao's authorship, and will link the exact landed commit here before closing this PR.

@vincentkoc

Copy link
Copy Markdown
Member

The maintainer-owned replacement discussed above has landed:

The final repair covers own-property reads, writes, and removals, including inherited parent/leaf data properties, accessors, and non-writable descriptors. Final proof was 106 focused tests, Testbox pnpm check:changed, clean autoreview, and exact-head hosted CI with zero failures.

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

Labels

P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. size: XS status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants