受発注や見積もりの業務システムで、APIが受け取る明細行に「最大100行」のような上限を Zod の .max() で付けている場合を考えます(想定の場面)。上限を超えた送信が来れば、エラーを返して終わり、と考えるのが自然です。
ところが Zod 4.6.5 までは、上限を超えた配列でもまず全部の要素を検証し、そのあとで件数の上限を判定していました。10万行の不正な明細が届くと、10万行分の検証が走り、行ごとのエラーが積み上がってから「上限超過」が報告されます。
2026年10月11日に公開された 4.6.6 で、この順序が逆になりました。4.6.5 と 4.6.7 を手元で動かして比べた結果と、更新の前に確かめておきたい点をまとめます。
4.6.6で変わったこと
Zod のリポジトリ(colinhacks/zod)にある修正のコミット(#6652)には、次の説明があります。
.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.
要点は次のとおりです。
- 対象は Zod 4 の配列(
z.array)・z.set・z.mapの、上限を決めるチェック(.max()・.length()・.size()) - 上限を超えた入力は、要素を検証する前にエラーになる
- 通常の検証と、事前コンパイル(
z.compile())した検証の両方に入った
この修正は、脆弱性 CVE-2023-54404 の修正として扱われています。CVE を登録した VulnCheck の公開情報(2026年10月1日公開)は、影響を受ける版を「zod <= 4.6.5」、修正を今回のコミット(履歴の書き換え前の ID は e06a510)とし、深刻度を CVSS v4.0 で 8.2(High)としています。内容は、件数の上限のない配列スキーマに大きな配列を送ると、要素ごとのエラーが積み上がってメモリを使い果たし、プロセスが落ちるおそれがある、というものです。修正のきっかけになった Issue #6643 も、この CVE を指して 4.6.5 が影響を受けると報告しています。
ただし、4.6.6 に上げるだけで守られるわけではありません。メンテナーは関連する Issue #1872 を閉じる際に、外部から入力を受けるアプリはリクエスト本体の大きさの制限か .max() のようなスキーマの上限で入力を抑える必要があり、#6652 以降は .max() を付けた配列が要素を見る前に拒否される、と説明しています。.max() などの上限を付けていない配列は、4.6.6 以降も要素の数だけ検証とエラーが積み上がります。
npm の記録では、4.6.6 が 2026年10月11日、4.6.7 が10月12日(いずれもUTC)の公開です。リポジトリの履歴を見ると、4.6.6 から 4.6.7 への差分は版番号・公開用のスクリプト・開発用のロックファイルだけで、検証のコードは同じです。4.6.5 の公開は9月13日で、npm の latest は10月13日時点で 4.6.7 を指しています。
なお、下限の .min()・.nonempty() は先に判定する対象ではありません。下限を満たさない配列は、これまでどおり要素のエラーと下限のエラーの両方が返ります。
4.6.5と4.6.7で同じ入力を比べた
Node.js 22.22.0(Linux)で、4.6.5・4.6.6・4.6.7 をそれぞれ別のディレクトリに入れ、同じスクリプトを実行しました。スキーマは、明細の配列に .max(100) を付けたものです。
const Item = z.object({ sku: z.string(), qty: z.number().int().positive() });
const Body = z.object({ items: z.array(Item).max(100) });
入力は、{ sku: 1, qty: -1 }(2項目とも不正)を10万件並べた配列です。各版で5回ずつ safeParse を実行しました。

実行ログの抜粋です。
zod 4.6.5
A 100k invalid items, .max(100): issues=200001 errorJSONbytes=27877921 median_ms=368.728
B 100k valid items, .max(100): issues=1 elemRefineCalls/run=100000 median_ms=45.529
zod 4.6.7
A 100k invalid items, .max(100): issues=1 errorJSONbytes=141 median_ms=0.038
B 100k valid items, .max(100): issues=1 elemRefineCalls/run=0 median_ms=0.335
読み取れることは3つです。
- 4.6.5 ではエラーの量が入力に比例した。 10万件×2項目の 200,000件に、上限超過の1件を加えた 200,001件のエラーができ、
JSON.stringifyすると約27.9MBになりました。エラーをそのままログやレスポンスに出していると、その分の容量と処理が上乗せされます - 4.6.7 では上限超過の1件だけになった。 エラーの内容は
code: "too_big"、path: ["items"]です - 正しい要素でも、4.6.5 は全部を検証していた。 要素の
.refine()に回数を数える処理を入れると、4.6.5 は1回の検証で10万回呼ばれ、4.6.7 は0回でした
時間はこの環境で5回測った中央値で、条件が変われば値も変わります。比べてほしいのは桁の違いのほうです。z.compile() したスキーマでも同じ入力で比べ、4.6.5 はエラー200,001件、4.6.7 は1件でした。
先に判定されない書き方
この変更はスキーマの書き方によっては効きません。手元で確かめた条件をまとめます(要素数1,000件、.max(100) などの上限を超えた入力で、4.6.7 の要素の検証回数を数えた結果)。
| 書き方 | 4.6.7 の要素の検証 |
|---|---|
z.array(el).max(100)、.min(1).max(100)、.nonempty().max(100) | 0回(先に判定) |
z.array(el).length(10)、Zod Mini の .check(z.maxLength(100)) | 0回(先に判定) |
z.array(el).max(100).refine(...) / .transform(...)、.optional() を付けたもの | 0回(先に判定) |
z.array(el).refine(...).max(100) | 1,000回(判定は後) |
import { z } from "zod/v3" の z.array(el).max(100) | 1,000回(判定は後) |
ポイントはチェックの並び順です。ソースでは、配列に付けたチェックを先頭から見ていき、件数のチェック以外(.refine() など)が出てきたところで先に判定する対象を打ち切っています。値を書き換えるチェック(.overwrite())のあとに件数を数えると結果が変わりうるためで、テストにもその例が入っています。
もう1つは、互換用の zod/v3 の入り口です。今回の修正は Zod 4 のコードだけを変えていて、zod/v3 から読み込んだスキーマは 4.6.7 でも要素を全部検証してから上限を判定しました。
そもそも .max() を付けていない配列は、どの版でも件数の制限がありません。前述の CVE への備えとしても、APIの入力の配列には上限を付け、.refine() より前に置く、というのが今回の結果から言える書き方です。HTTPサーバー側でリクエストの大きさを制限しているかどうかも、あわせて確かめておく価値があります(サーバーの設定は今回は試していません)。
画面とテストで起きる変化
処理が軽くなるだけでなく、返ってくるエラーの中身が変わります。上限を少し超えた入力で比べました。.max(2) の配列に、不正な明細を3件入れた場合です。
4.6.5: items.0.sku / items.0.qty / items.1.sku / items.1.qty / items.2.sku / items.2.qty / items(too_big) の7件
4.6.7: items(too_big) の1件
ここから、次の影響が考えられます(編集部の整理)。
- フォームの行ごとのエラー表示。 行数の上限を超えた状態で送信すると、4.6.5 では各行の入力ミスと「行数が多すぎる」が同時に出ていましたが、4.6.6 以降は行数のエラーだけになります。行を減らしてから再送すると、その時点で各行のエラーが初めて表示されます。画面の文言や操作の案内を、この順序に合わせるかどうかを決めておきます
- エラーの形を固定したテスト。 上限を超えた入力でエラーの件数や一覧をスナップショットで比べているテストは、4.6.6 に上げると失敗します。仕様どおりの変化なので、期待値を更新します
- 要素の検証に処理を入れている場合。 要素の
.refine()や.transform()で、マスタの照会やログ出力などをしていると、上限を超えた入力ではそれが呼ばれなくなります。件数を数える処理や副作用を要素側に置いている場合は確認が必要です
Zod でフォームの入力をサーバー側でもう一度検証する構成は選択で項目が変わる申込フォームの作り方、Hono の validator で Zod を使う構成はHono でバックエンドAPIを作るときのベストプラクティスで扱っています。
更新するときの確認手順
- 使っている版を確かめる。
npm ls zodで、アプリ本体と依存パッケージが読み込んでいる版をすべて見ます。依存パッケージが別の版の Zod を持っていることがあります - 配列・Set・Map のスキーマを洗い出し、
.max()・.length()・.size()が付いているか、.refine()より前にあるかを見る zod/v3から読み込んでいるスキーマがあれば、Zod 4 の書き方へ移すかどうかを決める- 上限を超えた入力のテストを足すか、既存のテストの期待値を更新する
- フォームで行数の上限を設けている画面は、上限を超えたときの表示を確かめる
洗い出しには、たとえば次の検索が使えます。
npm ls zod
grep -rnE "z\.(array|set|map)\(" src | grep -vE "\.(max|length|size)\("
grep -rn "from \"zod/v3\"\|from 'zod/v3'" src
1行目で版を、2行目で上限のない配列を、3行目で zod/v3 の読み込みを探します。2行目は1行に書いたスキーマしか拾わないため、複数行にまたがる定義は目で確かめてください。APIの型と検証をまとめて管理する考え方はOpenAPI から oRPC へも参考になります。
確認した範囲
2026年10月13日に、colinhacks/zod のリポジトリ(修正のコミット #6652、v4.6.5〜v4.6.7 の履歴、テスト、docs の api.mdx・compile.mdx)と npm の公開日時を確認しました(資料調査)。あわせて Linux・Node.js 22.22.0 で zod 4.6.5・4.6.6・4.6.7 を入れ、同じスクリプトでエラー件数・要素の検証回数・処理時間を比べました(実機検証。時間は1台・5回の中央値)。HTTPサーバーやフォームのライブラリと組み合わせた動作、
z.record()・z.tuple()は確認していません。2026年10月16日に、GitHub のリリースノート(v4.6.6)、PR #6652、Issue #6643・#1872、CVE-2023-54404 の公開情報(NVD・VulnCheck・OSV)を確認しました。
Zod を使ったAPIや業務システムの入力検証の見直しは、開発・AI・自動化のご相談からお問い合わせください。
参考資料
- colinhacks/zod — fix(v4): check array, set and map size bounds before parsing elements (#6652)
- colinhacks/zod — packages/zod/src/v4/core/schemas.ts(v4.6.7)
- colinhacks/zod — packages/docs/content/api.mdx(Arrays)
- npm — zod(各版の公開日時)
- colinhacks/zod — Release v4.6.6
- colinhacks/zod — Issue #6643(zod 4.6.5 is affected by CVE-2023-54404)
- colinhacks/zod — Issue #1872(Denial of service with array validations?)
- NVD — CVE-2023-54404
- VulnCheck — Zod 4.6.5 Uncontrolled Resource Consumption via Array Validation









