Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion library/src/schemas/looseObject/looseObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
import {
_addIssue,
_getStandardProps,
_hasOwnProperty,
_isValidObjectKey,
} from '../../utils/index.ts';
import type { LooseObjectIssue } from './types.ts';
Expand Down Expand Up @@ -205,7 +206,7 @@ export function looseObject(
// Hint: We exclude specific keys for security reasons
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
Contributor

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.

// @ts-expect-error
dataset.value[key] = input[key];
}
Expand Down
3 changes: 2 additions & 1 deletion library/src/schemas/looseObject/looseObjectAsync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
import {
_addIssue,
_getStandardProps,
_hasOwnProperty,
_isValidObjectKey,
} from '../../utils/index.ts';
import type { looseObject } from './looseObject.ts';
Expand Down Expand Up @@ -226,7 +227,7 @@ export function looseObjectAsync(
// Hint: We exclude specific keys for security reasons
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)) {
// @ts-expect-error
dataset.value[key] = input[key];
}
Expand Down
3 changes: 2 additions & 1 deletion library/src/schemas/objectWithRest/objectWithRest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
import {
_addIssue,
_getStandardProps,
_hasOwnProperty,
_isValidObjectKey,
} from '../../utils/index.ts';
import type { ObjectWithRestIssue } from './types.ts';
Expand Down Expand Up @@ -228,7 +229,7 @@ export function objectWithRest(
// Hint: We exclude specific keys for security reasons
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)) {
const valueDataset = this.rest['~run'](
// @ts-expect-error
{ value: input[key] },
Expand Down
3 changes: 2 additions & 1 deletion library/src/schemas/objectWithRest/objectWithRestAsync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
import {
_addIssue,
_getStandardProps,
_hasOwnProperty,
_isValidObjectKey,
} from '../../utils/index.ts';
import type { objectWithRest } from './objectWithRest.ts';
Expand Down Expand Up @@ -179,7 +180,7 @@ export function objectWithRestAsync(
Object.entries(input)
.filter(
([key]) =>
_isValidObjectKey(input, key) && !(key in this.entries)
_isValidObjectKey(input, key) && !_hasOwnProperty(this.entries, key)
)
.map(
async ([key, value]) =>
Expand Down
18 changes: 18 additions & 0 deletions library/src/schemas/strictObject/strictObject.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,24 @@ describe('strictObject', () => {
expectSchemaIssue(schema, baseIssue, [Symbol(), Symbol('foo')]);
});

test('for input with a key that collides with Object.prototype', () => {
// Regression test for #1523. The pre-fix `key in this.entries` check
// matched inherited Object.prototype members, so a strict schema
// silently accepted `__proto__`, `constructor`, `toString`, and
// friends as defined entries. After the fix, those keys are
// reported as unknown and the schema rejects the input.
const strict = strictObject({ name: string() }, 'message');
expectSchemaIssue(
strict,
{ ...baseIssue, expected: 'never' },
[
{ name: 'foo', toString: 'bar' },
{ name: 'foo', constructor: 1 },
{ name: 'foo', __proto__: 'x' },

Copy link
Copy Markdown
Contributor

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.

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' },

]
);
});

// Complex types

// TODO: Enable this test again in case we find a reliable way to check for
Expand Down
4 changes: 2 additions & 2 deletions library/src/schemas/strictObject/strictObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type {
ObjectPathItem,
OutputDataset,
} from '../../types/index.ts';
import { _addIssue, _getStandardProps } from '../../utils/index.ts';
import { _addIssue, _getStandardProps, _hasOwnProperty } from '../../utils/index.ts';
import type { StrictObjectIssue } from './types.ts';

/**
Expand Down Expand Up @@ -200,7 +200,7 @@ export function strictObject(
// Check input for unknown keys if necessary
if (!dataset.issues || !config.abortEarly) {
for (const key in input) {
if (!(key in this.entries)) {
if (!_hasOwnProperty(this.entries, key)) {
_addIssue(this, 'key', dataset, config, {
input: key,
expected: 'never',
Expand Down
4 changes: 2 additions & 2 deletions library/src/schemas/strictObject/strictObjectAsync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type {
ObjectPathItem,
OutputDataset,
} from '../../types/index.ts';
import { _addIssue, _getStandardProps } from '../../utils/index.ts';
import { _addIssue, _getStandardProps, _hasOwnProperty } from '../../utils/index.ts';
import type { strictObject } from './strictObject.ts';
import type { StrictObjectIssue } from './types.ts';

Expand Down Expand Up @@ -221,7 +221,7 @@ export function strictObjectAsync(
// Check input for unknown keys if necessary
if (!dataset.issues || !config.abortEarly) {
for (const key in input) {
if (!(key in this.entries)) {
if (!_hasOwnProperty(this.entries, key)) {
_addIssue(this, 'key', dataset, config, {
input: key,
expected: 'never',
Expand Down
28 changes: 28 additions & 0 deletions library/src/utils/_hasOwnProperty/_hasOwnProperty.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { describe, expect, test } from 'vitest';
import { _hasOwnProperty } from './_hasOwnProperty.ts';

describe('_hasOwnProperty', () => {
test('returns true for own properties', () => {
expect(_hasOwnProperty({ name: 'a' }, 'name')).toBe(true);
});

test('returns false for inherited Object.prototype members', () => {
// Regression coverage for the prototype-pollution class of bug that
// #1523 fixed in the object schemas. Keys that look like they are
// defined entries must not be matched via the `in` operator's
// prototype walk.
expect(_hasOwnProperty({}, 'toString')).toBe(false);
expect(_hasOwnProperty({}, 'valueOf')).toBe(false);
expect(_hasOwnProperty({}, 'hasOwnProperty')).toBe(false);
expect(_hasOwnProperty({}, 'constructor')).toBe(false);
expect(_hasOwnProperty({}, '__proto__')).toBe(false);
});

test('returns true for own properties that share a name with a prototype member', () => {
// A schema that intentionally defines an entry named like a prototype
// member is still recognized as a defined entry. The own-property
// check is precise, not name-based.
expect(_hasOwnProperty({ toString: () => undefined }, 'toString')).toBe(true);
expect(_hasOwnProperty({ constructor: 1 }, 'constructor')).toBe(true);
});
});
18 changes: 18 additions & 0 deletions library/src/utils/_hasOwnProperty/_hasOwnProperty.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Own-property check that survives prototype pollution. Mirrors the call
* pattern already used by `_isValidObjectKey` so the entry-membership
* checks in object schemas can reject keys like `__proto__`,
* `constructor`, and `toString` instead of inheriting them from
* `Object.prototype`.
*
* @param object The object to check.
* @param key The key to check.
*
* @returns Whether the key is an own property of the object.
*
* @internal
*/
// @__NO_SIDE_EFFECTS__
export function _hasOwnProperty(object: object, key: string): boolean {
return Object.prototype.hasOwnProperty.call(object, key);
}
1 change: 1 addition & 0 deletions library/src/utils/_hasOwnProperty/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './_hasOwnProperty.ts';
1 change: 1 addition & 0 deletions library/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export * from './_getGraphemeCount/index.ts';
export * from './_getLastMetadata/index.ts';
export * from './_getStandardProps/index.ts';
export * from './_getWordCount/index.ts';
export * from './_hasOwnProperty/index.ts';
export * from './_isLuhnAlgo/index.ts';
export * from './_isValidObjectKey/index.ts';
export * from './_joinExpects/index.ts';
Expand Down