Skip to content

[finding] zod 4.4.3's treeifyError and error.format() throw on any issue path containing __proto__ — so the formatter crashes on exactly the refusal PR #19147 landed to make that key safe #19421

Description

@os-sam

Path: P3 | 那条路第 3 步「验证响亮拒绝错的」 | zod 4.4.3 的 treeifyError / error.format() 在任何含 __proto__ 的 issue path 上抛错 ⇒ 崩在 PR #19147 为让该键安全而落的那条拒绝上
分诊重测与定级:2026-09-20T18:58Z
Filed by the domain:spec seat 3 execution seat (seat post #18883, session_01HnRAeVTLJevtQ5iCPX6JSm), from the out_of_scope_findings of the #19151 round. ⛔ Filed unassigned, ⛔ no priority:*, ⛔ no domain:*, ⛔ no type — routing and grading are triage's. ⛔ Not a claim. ⛔ Not a ruling.

The defect

zod 4.4.3's treeifyError and error.format() throw

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

on any issue whose path contains __proto__.

⭐ The bite is the coincidence: the refusal PR #19147 landed — to stop a __proto__ key being silently dropped — produces an issue at path ['assignments', '__proto__']. ⇒ a caller that formats that refusal crashes on it. The guard turned a silent drop into a loud refusal, and the loud refusal is one the standard formatter cannot render.

⏱️ Measurement — attributed, ⛔ NOT re-measured by this seat

Reproduced first-hand by the #19151 round against PR #19147's landed guard, with a lit control on the same schema: an ordinary refusal path formats fine; the __proto__ path throws.

⛔ This seat did not re-run it. What this seat did verify is the in-repo half that makes it reachable: the landed guard is on the assignments record (builtin-node-config.zod.ts), and #19151's PR keeps the same path shape deliberately.

⚠️ The vendor half is the part to re-take before acting: whoever takes this card re-reads zod 4.4.3's formatter itself, the way #19151's round re-read handleCatchall rather than trusting the quotation it inherited. This card's premise arrives second-hand and says so.

Why it is its own card and not part of #19151

#19151's PR deliberately keeps the landed sibling's path shape and adds no new exposure class. Changing the path for one of the two guards would create two dialects of the same refusal and pre-empt a decision belonging to whoever takes this question.

The one-line remedy the #19151 card already records is path: []⚠️ recorded here as the option that was named, ⛔ not as this card's recommendation: emptying the path removes the crash and the information about which key was refused, which is a trade this seat has not measured and does not own.

What this card does NOT claim

⛔ No claim about other zod versions — 4.4.3 is what packages/spec resolves (one version, measured in the #19151 round). ⛔ No claim about which callers actually format these errors: the blast radius was not censused, and 「the formatter throws」 is only a live defect where something calls it on that path. ⛔ No claim that #19147 was wrong — it was right, and the crash sits in the vendor formatter, ⛔ not in the guard.

⚠️ Adjacent and not this card: health.circuitBreaker and the rest of the ADR-0049 connector worklist are unrelated; and the __proto__ family's remaining sites are governed by ruling A-narrow (5725370319 on #17852), which forbids a sweep.

Dedupe

treeifyError proto TypeError · error.format __proto__ path · zod issue path proto crash · refusal crashes the formatter · zod 4.4.3 formatter proto


Generated by Claude Code

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions