Skip to content

fix(v4): prevent formatError from throwing on Object.prototype keys in path#6083

Open
mahitha-ada wants to merge 1 commit into
colinhacks:mainfrom
mahitha-ada:fix/formaterror-prototype-keys
Open

fix(v4): prevent formatError from throwing on Object.prototype keys in path#6083
mahitha-ada wants to merge 1 commit into
colinhacks:mainfrom
mahitha-ada:fix/formaterror-prototype-keys

Conversation

@mahitha-ada

Copy link
Copy Markdown

fix(v4): prevent formatError from throwing on Object.prototype keys in path

Problem

formatError builds its nested error tree using a plain object as a dictionary, initializing each node with:

curr[el] = curr[el] || { _errors: [] };

When a validation issue's path segment matches a member inherited from Object.prototype (e.g. toString, constructor, valueOf, hasOwnProperty), curr[el] resolves to the inherited member instead of undefined. The || short-circuits, curr is assigned that member (a function, with no _errors array), and the subsequent curr[el]._errors.push(...) throws:

TypeError: Cannot read properties of undefined (reading 'push')

Reproduction

import { z } from "zod";

const schema = z.object({ toString: z.string() });
const result = schema.safeParse({}); // success: false

if (!result.success) {
  z.formatError(result.error); // 💥 throws TypeError
}

This is the same class of bug as the reported treeifyError issue (#6070) and the previously-fixed flatten() issue (#5265#5266). #6070's fix addresses treeifyError's properties dictionary; formatError has the identical vulnerability and is not covered by that fix.

Fix

Guard node initialization with Object.prototype.hasOwnProperty.call(...) before assigning, so inherited members are never mistaken for existing nodes — mirroring the guard used in treeifyError and the Object.create(null) approach used for flatten():

if (!Object.prototype.hasOwnProperty.call(curr, el)) {
  curr[el] = { _errors: [] };
}
if (terminal) {
  curr[el]._errors.push(mapper(issue));
}

Behavior is unchanged for all non-prototype keys.

Tests

Adds a regression test (formatError does not throw on Object.prototype keys in path) that parses a schema with toString, constructor, and valueOf fields and asserts formatError returns the populated tree instead of throwing. The test fails on the current code (TypeError) and passes with this change.

Verified locally:

  • vitest run error.test118 passed, 0 type errors (with fix)
  • Reverting the fix makes the new test fail with the exact TypeError above (confirming it's a true regression test)
  • biome check clean

Relationship to #6070 / #6071 — complementary, not a duplicate. The open treeifyError fixes (#6071, and #6081) address only treeifyError. This PR covers the sibling formatError path, which those leave unaddressed. The two can land independently.

…n path

formatError builds its nested tree using a plain object as a dictionary
and initializes each node with `curr[el] = curr[el] || { _errors: [] }`.
When a validation issue's path segment matches a member inherited from
Object.prototype (e.g. "toString", "constructor", "valueOf"), `curr[el]`
resolves to the inherited member instead of undefined, so the `||`
short-circuits, `curr` is assigned that member (which has no `_errors`
array), and the subsequent `curr[el]._errors.push(...)` throws a
TypeError.

Guard node initialization with Object.prototype.hasOwnProperty.call,
mirroring the existing fix in flattenError (colinhacks#5265) and treeifyError.

Adds a regression test that fails without this change.

@pullfrog pullfrog Bot left a comment

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.

✅ No new issues found.

Reviewed changes — guards formatError against Object.prototype key collisions in path segments, matching the approach used in treeifyError and flatten().

  • Guard node initialization with hasOwnProperty.call — replacing the previous curr[el] || { _errors: [] } pattern that would resolve inherited members (e.g. toString) to functions, causing a TypeError on _errors.push().
  • Regression testtoString, constructor, and valueOf path segments in formatError.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

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.

1 participant