Skip to content

fix(v4): check array, set and map size bounds before parsing elements - #6652

Merged
colinhacks merged 1 commit into
mainfrom
fix/size-cap-before-walk
Oct 9, 2026
Merged

colinhacks merged 1 commit into
mainfrom
fix/size-cap-before-walk

Conversation

@colinhacks

Copy link
Copy Markdown
Owner

.max(), .length() and .size() on arrays, sets and maps now reject an oversized input before any element is parsed. The bound used to run after the walk, so it bounded nothing: z.array(z.string().uuid()).max(100) given a million numbers built 1,000,001 issues before it reported too_big.

z.array(z.string().uuid()).max(100).safeParse(Array(1_000_000).fill(0));
// one too_big issue in under a millisecond, was 1,000,001 issues in ~450 ms

Only size checks that come before every other check take part, since something like .overwrite() can shrink the value first. The early rejection aborts, so a later .refine() never sees elements that weren't parsed, and an oversized input reports just the too_big issue rather than its element issues too. The compiled path gets the same check ahead of its loop. Unbounded containers are unchanged.

axis result
runtime The check wraps parse only on schemas that carry an upper bound, so an unbounded container runs the same code as before. z.array(z.string()).max(100) over 10 items: 65.9 → 67.4 ns (+1–3%, interleaved, but taken at load 10–20). Construction within noise.
memory Unbounded: unchanged. Bounded: +121 B per schema (3421 → 3542) for the wrapper closure.
bundle zod/mini array fixture +243 B gzipped (3311 → 3554, +7.3%), +227 with a maxLength check. Classic array +215 (+1.0%). Bundles without an array, set or map pay nothing (zod-mini-object unchanged).

Putting the check inside the existing parse closure was smaller (~190 B) but pushed it past V8's inlining budget: z.array(z.string()).max(n) parses went 58 → 71 ns.

Fixes #6643

.max(), .length() and .size() ran after every element was walked, so an
oversized input built one issue per bad element before the bound was
reported. Leading size checks now reject the input first, both in the
interpreter and in the compiled path.

Fixes #6643
Copilot AI balanced review requested due to automatic review settings October 7, 2026 16:21

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@antimatter-super-dev antimatter-super-dev 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.

LGTM

@dokson

dokson commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

One small thing worth flagging: this doesn't cover the unbounded case from the CVE. Making abortEarly public would:

z.array(z.string().uuid()).safeParse(Array(1_000_000).fill(0));
// still 1M issues

z.array(z.string().uuid()).safeParse(Array(1_000_000).fill(0), { abortEarly: true });
// 1 issue

{ abortEarly: true } stops each container at its first error, bound or not. It's opt-in, with no change for anyone who doesn't set it. I have it ready as one commit and can rebase it on top of this PR.

@colinhacks
colinhacks merged commit e06a510 into main Oct 9, 2026
8 checks passed
@colinhacks
colinhacks deleted the fix/size-cap-before-walk branch October 9, 2026 19:50

This branch was successfully deployed

1 active deployment
Preview – zod-v4 — 89faa2f9 Deployed Oct 7, 2026 by vercel[bot]
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.

zod 4.6.5 is affected by CVE-2023-54404

4 participants