Skip to content

fix(object): use own-property check so prototype keys aren't matched as entries (#1523)#1536

Open
sanjibani wants to merge 1 commit into
open-circle:mainfrom
sanjibani:fix/issue-1523-object-prototype-keys
Open

fix(object): use own-property check so prototype keys aren't matched as entries (#1523)#1536
sanjibani wants to merge 1 commit into
open-circle:mainfrom
sanjibani:fix/issue-1523-object-prototype-keys

Conversation

@sanjibani

@sanjibani sanjibani commented Jul 9, 2026

Copy link
Copy Markdown

Fixes #1523.

Bug

`strictObject`, `looseObject`, and `objectWithRest` (and their async variants) checked whether an input key was a defined schema entry with `key in this.entries`. The `in` operator also matches inherited `Object.prototype` members, so an input key named like a prototype member was treated as a defined entry.

Effect:

  • `strictObject` / `strictObjectAsync` failed to reject `proto`, `constructor`, `toString`, `valueOf`, and friends as unknown keys. A `proto` or `constructor` key passed a strict schema silently.
  • `looseObject` / `looseObjectAsync` dropped such keys instead of passing them through.
  • `objectWithRest` / `objectWithRestAsync` skipped such keys instead of validating them against the `rest` schema.

Before the fix:

```ts
v.parse(v.strictObject({ name: v.string() }), { name: 'a', toString: 'b' });
// returns { name: 'a' }; the unknown `toString` key is not reported
```

Fix

Replace each `key in this.entries` membership check with a new `_hasOwnProperty(this.entries, key)` helper - the same `Object.prototype.hasOwnProperty.call` pattern already used by `_isValidObjectKey` for input keys and by the #1522 fixes. A schema that intentionally defines an entry named like a prototype member (`v.object({ toString: v.string() })`) still works, since the own-property check is precise, not name-based.

The new helper lives at `library/src/utils/_hasOwnProperty/` and is re-exported from the utils barrel alongside the existing `_isValidObjectKey` so the two prototype-aware checks sit side by side.

Tests

  • New `_hasOwnProperty` unit suite (3 tests) covering own properties, inherited `Object.prototype` members, and own properties that share a name with a prototype member.
  • New `strictObject` regression test asserting that an input carrying `toString` / `constructor` / `proto` is now rejected as an unknown key. The pre-fix code returned a clean parse for these inputs.

8 files changed, +96/-8.

Summary by CodeRabbit

  • New Features

    • Added an internal own-property utility and re-exported it for shared schema use.
  • Bug Fixes

    • Improved object key handling to treat only direct (own) properties as defined, avoiding prototype-inherited keys being accepted as valid.
    • Tightened strict object validation so prototype-collision keys are correctly rejected as unknown.
  • Tests

    • Added regression tests covering own-property behavior and strict/edge-case prototype-colliding keys.

@dosubot dosubot Bot added size:S This PR changes 10-29 lines, ignoring generated files. bug Something isn't working labels Jul 9, 2026
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds a shared _hasOwnProperty utility that checks own properties with Object.prototype.hasOwnProperty.call, exports it through the utility indexes, and adds tests for own keys, inherited prototype members, and colliding names. Updates loose object, object-with-rest, and strict object schemas, including async variants, to use own-property checks when classifying keys. Adds a strict-object regression test for keys like toString, constructor, and __proto__.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning Core schema changes match the issue, but the requested coverage for loose and rest object paths isn't shown in the PR summary. Add tests for looseObject/looseObjectAsync and objectWithRest/objectWithRestAsync to verify prototype keys pass through or hit rest as required.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main fix: switching object schemas to own-property checks for prototype-colliding keys.
Out of Scope Changes check ✅ Passed All changes support the prototype-key object-schema fix, including helper, re-exports, schema updates, and regression tests.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@library/src/schemas/strictObject/strictObject.test.ts`:
- Line 124: The strictObject test case is using the object-literal prototype
setter instead of creating an own __proto__ property, so it does not exercise
the intended collision path. Update the sample in strictObject.test.ts to use a
computed __proto__ key (or define the property explicitly) within the relevant
test around strictObject so the regression covers an actual own-property
conflict.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0bd9b144-a530-42be-9d86-e79df0581677

📥 Commits

Reviewing files that changed from the base of the PR and between 32247b3 and a6d6f58.

📒 Files selected for processing (4)
  • library/src/schemas/strictObject/strictObject.test.ts
  • library/src/utils/_hasOwnProperty/_hasOwnProperty.test.ts
  • library/src/utils/_hasOwnProperty/_hasOwnProperty.ts
  • library/src/utils/_hasOwnProperty/index.ts

[
{ name: 'foo', toString: 'bar' },
{ name: 'foo', constructor: 1 },
{ name: 'foo', __proto__: 'x' },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Test file outline ==\n'
ast-grep outline library/src/schemas/strictObject/strictObject.test.ts --view expanded || true

printf '\n== Relevant lines ==\n'
nl -ba library/src/schemas/strictObject/strictObject.test.ts | sed -n '90,150p'

printf '\n== strictObject source search ==\n'
fd -a 'strictObject.ts' library/src || true
rg -n "strictObject|__proto__|proto" library/src/schemas/strictObject library/src -g '!**/*.map' || true

printf '\n== JS semantics probe ==\n'
node - <<'JS'
const samples = [
  { expr: "({ name: 'foo', __proto__: 'x' })" },
  { expr: "({ name: 'foo', ['__proto__']: 'x' })" },
  { expr: "Object.defineProperty({ name: 'foo' }, '__proto__', { value: 'x', enumerable: true })" },
  { expr: "({ __proto__: null, a: 1 })" },
];
for (const { expr } of samples) {
  const obj = eval(expr);
  console.log(expr);
  console.log('  own keys:', Object.keys(obj));
  console.log('  hasOwn __proto__:', Object.prototype.hasOwnProperty.call(obj, '__proto__'));
  console.log('  proto is null:', Object.getPrototypeOf(obj) === null);
  console.log('  value of obj.__proto__:', obj.__proto__);
}
JS

Repository: open-circle/valibot

Length of output: 333


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== strictObject directory tree ==\n'
find library/src/schemas/strictObject -maxdepth 2 -type f | sort

printf '\n== strictObject test file ==\n'
nl -ba library/src/schemas/strictObject/strictObject.test.ts | sed -n '1,220p'

printf '\n== strictObject implementation search ==\n'
rg -n "strictObject|createStrictObject|Object.fromEntries|__proto__|prototype" library/src/schemas/strictObject library/src -g '!**/*.map' || true

printf '\n== JS semantics probe ==\n'
node - <<'JS'
const exprs = [
  "({ name: 'foo', __proto__: 'x' })",
  "({ name: 'foo', ['__proto__']: 'x' })",
  "Object.defineProperty({ name: 'foo' }, '__proto__', { value: 'x', enumerable: true })",
  "({ __proto__: null, a: 1 })",
];
for (const expr of exprs) {
  const obj = eval(expr);
  console.log(expr);
  console.log('  own keys:', JSON.stringify(Object.keys(obj)));
  console.log('  hasOwn __proto__:', Object.prototype.hasOwnProperty.call(obj, '__proto__'));
  console.log('  proto is null:', Object.getPrototypeOf(obj) === null);
  console.log('  obj.__proto__:', obj.__proto__);
}
JS

Repository: open-circle/valibot

Length of output: 679


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Minimal probe for object-literal `__proto__` behavior.
node - <<'JS'
const a = { name: 'foo', __proto__: 'x' };
const b = { name: 'foo', ['__proto__']: 'x' };
console.log(JSON.stringify({
  aOwn: Object.prototype.hasOwnProperty.call(a, '__proto__'),
  aKeys: Object.keys(a),
  aProto: Object.getPrototypeOf(a) === Object.prototype,
  aValue: a.__proto__,
  bOwn: Object.prototype.hasOwnProperty.call(b, '__proto__'),
  bKeys: Object.keys(b),
  bProto: Object.getPrototypeOf(b) === Object.prototype,
  bValue: b.__proto__,
}));
JS

Repository: open-circle/valibot

Length of output: 283


Use a computed __proto__ key here{ name: 'foo', __proto__: 'x' } uses the special object-literal prototype setter, so it won’t create an own __proto__ property. Switch this sample to ['__proto__'] or Object.defineProperty so the regression covers the intended collision path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@library/src/schemas/strictObject/strictObject.test.ts` at line 124, The
strictObject test case is using the object-literal prototype setter instead of
creating an own __proto__ property, so it does not exercise the intended
collision path. Update the sample in strictObject.test.ts to use a computed
__proto__ key (or define the property explicitly) within the relevant test
around strictObject so the regression covers an actual own-property conflict.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 4 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="library/src/schemas/strictObject/strictObject.test.ts">

<violation number="1" location="library/src/schemas/strictObject/strictObject.test.ts:124">
P2: The object literal `{ name: 'foo', __proto__: 'x' }` uses JavaScript's special prototype-setter syntax rather than creating an own property named `__proto__`. This means the resulting object only has `name` as an own key, so this test case doesn't actually exercise the `__proto__` collision path.

Use a computed key to create a true own property:
```js
{ name: 'foo', ['__proto__']: 'x' }
```</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

[
{ name: 'foo', toString: 'bar' },
{ name: 'foo', constructor: 1 },
{ name: 'foo', __proto__: 'x' },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The object literal { name: 'foo', __proto__: 'x' } uses JavaScript's special prototype-setter syntax rather than creating an own property named __proto__. This means the resulting object only has name as an own key, so this test case doesn't actually exercise the __proto__ collision path.

Use a computed key to create a true own property:

{ name: 'foo', ['__proto__']: 'x' }
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At library/src/schemas/strictObject/strictObject.test.ts, line 124:

<comment>The object literal `{ name: 'foo', __proto__: 'x' }` uses JavaScript's special prototype-setter syntax rather than creating an own property named `__proto__`. This means the resulting object only has `name` as an own key, so this test case doesn't actually exercise the `__proto__` collision path.

Use a computed key to create a true own property:
```js
{ name: 'foo', ['__proto__']: 'x' }
```</comment>

<file context>
@@ -108,6 +108,24 @@ describe('strictObject', () => {
+        [
+          { name: 'foo', toString: 'bar' },
+          { name: 'foo', constructor: 1 },
+          { name: 'foo', __proto__: 'x' },
+        ]
+      );
</file context>
Suggested change
{ name: 'foo', __proto__: 'x' },
{ name: 'foo', ['__proto__']: 'x' },

…as entries

`strictObject`, `looseObject`, and `objectWithRest` (and their
async variants) check whether an input key is a defined schema entry
with `key in this.entries`. The `in` operator also matches
inherited `Object.prototype` members, so an input key named like a
prototype member was treated as a defined entry.

Effect:

- `strictObject` / `strictObjectAsync` failed to reject
  `__proto__`, `constructor`, `toString`, `valueOf`, and friends
  as unknown keys. A `__proto__` or `constructor` key passed a
  strict schema silently.
- `looseObject` / `looseObjectAsync` dropped such keys instead of
  passing them through.
- `objectWithRest` / `objectWithRestAsync` skipped such keys
  instead of validating them against the `rest` schema.

Replace each `key in this.entries` membership check with a new
`_hasOwnProperty(this.entries, key)` helper - the same
`Object.prototype.hasOwnProperty.call` pattern already used by
`_isValidObjectKey` for input keys and by the open-circle#1522 fixes. A
schema that intentionally defines an entry named like a prototype
member (`v.object({ toString: v.string() })`) still works, since
the own-property check is precise.

The new helper lives at `library/src/utils/_hasOwnProperty/` and is
re-exported from the utils barrel alongside the existing
`_isValidObjectKey` so the two prototype-aware checks sit side by
side.

Tests:

- New `_hasOwnProperty` unit suite covering own properties,
  inherited `Object.prototype` members, and own properties that
  share a name with a prototype member.
- New `strictObject` regression test asserting that an input
  carrying `toString` / `constructor` / `__proto__` is now
  rejected as an unknown key. The pre-fix code returned a clean
  parse for these inputs.

Closes open-circle#1523
@sanjibani
sanjibani force-pushed the fix/issue-1523-object-prototype-keys branch from a6d6f58 to 7e2ebb6 Compare July 9, 2026 09:40
@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. and removed size:S This PR changes 10-29 lines, ignoring generated files. labels Jul 9, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@library/src/schemas/looseObject/looseObject.ts`:
- Line 209: Add regression coverage in the `looseObject` and `objectWithRest`
test suites for prototype-collision keys. In `looseObject.test.ts`, add direct
cases showing `LooseObject` accepts and preserves `__proto__`, `constructor`,
`toString`, and `valueOf` on `this.entries` without treating them as collisions.
In `objectWithRest.test.ts`, add corresponding `ObjectWithRest` cases that
validate those keys against `rest` behavior, using the existing
`looseObject`/`objectWithRest` helpers and assertions to mirror the
`strictObject.test.ts` regression coverage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2d9eb938-8eb3-4805-8ec3-8455981d9a6a

📥 Commits

Reviewing files that changed from the base of the PR and between a6d6f58 and 7e2ebb6.

📒 Files selected for processing (11)
  • library/src/schemas/looseObject/looseObject.ts
  • library/src/schemas/looseObject/looseObjectAsync.ts
  • library/src/schemas/objectWithRest/objectWithRest.ts
  • library/src/schemas/objectWithRest/objectWithRestAsync.ts
  • library/src/schemas/strictObject/strictObject.test.ts
  • library/src/schemas/strictObject/strictObject.ts
  • library/src/schemas/strictObject/strictObjectAsync.ts
  • library/src/utils/_hasOwnProperty/_hasOwnProperty.test.ts
  • library/src/utils/_hasOwnProperty/_hasOwnProperty.ts
  • library/src/utils/_hasOwnProperty/index.ts
  • library/src/utils/index.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • library/src/utils/_hasOwnProperty/_hasOwnProperty.test.ts
  • library/src/utils/_hasOwnProperty/index.ts
  • library/src/utils/_hasOwnProperty/_hasOwnProperty.ts
  • library/src/schemas/strictObject/strictObject.test.ts

if (!dataset.issues || !config.abortEarly) {
for (const key in input) {
if (_isValidObjectKey(input, key) && !(key in this.entries)) {
if (_isValidObjectKey(input, key) && !_hasOwnProperty(this.entries, key)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for prototype-collision test coverage in looseObject and objectWithRest test files
rg -n '__proto__\|constructor\|toString\|valueOf\|prototype' \
  library/src/schemas/looseObject/looseObject.test.ts \
  library/src/schemas/looseObject/looseObjectAsync.test.ts \
  library/src/schemas/objectWithRest/objectWithRest.test.ts \
  library/src/schemas/objectWithRest/objectWithRestAsync.test.ts \
  library/src/schemas/looseObject/looseObjectAsync.test.ts 2>/dev/null || echo "No matches found"

Repository: open-circle/valibot

Length of output: 174


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== test file locations =="
fd -a 'looseObject*.test.ts' library/src || true
fd -a 'objectWithRest*.test.ts' library/src || true

echo
echo "== repository-wide prototype-collision references =="
rg -n '__proto__|constructor|toString|valueOf|prototype' library/src --glob '*test.ts' --glob '*test.tsx' || true

echo
echo "== schema test names around looseObject/objectWithRest =="
rg -n 'looseObject|objectWithRest' library/src --glob '*test.ts' || true

Repository: open-circle/valibot

Length of output: 28050


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file sizes =="
wc -l library/src/schemas/looseObject/looseObject.test.ts library/src/schemas/objectWithRest/objectWithRest.test.ts

echo
echo "== looseObject test outline =="
ast-grep outline library/src/schemas/looseObject/looseObject.test.ts --view expanded || true

echo
echo "== objectWithRest test outline =="
ast-grep outline library/src/schemas/objectWithRest/objectWithRest.test.ts --view expanded || true

Repository: open-circle/valibot

Length of output: 584


Add prototype-collision coverage to looseObject and objectWithRest

strictObject.test.ts covers this regression, but library/src/schemas/looseObject/looseObject.test.ts and library/src/schemas/objectWithRest/objectWithRest.test.ts still lack cases for __proto__, constructor, toString, and valueOf. Add direct tests for loose objects passing these keys through and object-with-rest validating them against rest.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@library/src/schemas/looseObject/looseObject.ts` at line 209, Add regression
coverage in the `looseObject` and `objectWithRest` test suites for
prototype-collision keys. In `looseObject.test.ts`, add direct cases showing
`LooseObject` accepts and preserves `__proto__`, `constructor`, `toString`, and
`valueOf` on `this.entries` without treating them as collisions. In
`objectWithRest.test.ts`, add corresponding `ObjectWithRest` cases that validate
those keys against `rest` behavior, using the existing
`looseObject`/`objectWithRest` helpers and assertions to mirror the
`strictObject.test.ts` regression coverage.

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

Labels

bug Something isn't working size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant