Skip to content

chore(bump): update dependency zod to v4.6.6 - #133

Open
superagent-security[bot] wants to merge 1 commit into
mainfrom
superagent/deps/zod-4.x
Open

superagent-security[bot] wants to merge 1 commit into
mainfrom
superagent/deps/zod-4.x

Conversation

@superagent-security

Copy link
Copy Markdown

Superagent dependency review

Dependency change

Package Change Age
zod 4.4.3 → 4.6.6 21 days
Question Result
Is the update vulnerable or malicious? ✅ No issues found
Is this dependency used? ⚠️ Yes, in production code — Production runtime dependency of apps/api, apps/worker, apps/web and apps/cli and of packages/contracts. Source imports z from zod in apps/api/src/env.ts and app.ts (ZodError), apps/cli/src/config.ts and program.ts, apps/web server actions and worker env. packages/contracts defines array schemas without explicit.max limits, so base 4.4.3 was exposed. Vulnerable code: reachable.

Why this update matters

Base zod 4.4.3 is inside the vulnerable range '< 4.6.6' for GHSA-mrhx-c7xc-m3w5 and its shipped $ZodArray parser accumulates issues for every failing element with no cap or early termination.

Issue details
  • Current version: GHSA-mrhx-c7xc-m3w5 — Affects your code. OSV and the GitHub advisory both state the vulnerable range is < 4.6.6, with 4.6.6 as first patched. The base 4.4.3 archive contains the unfixed handleArrayResult/$ZodArray code and its parse.cjs sets no abortEarly default, matching the advisory's description.
  • Update: GHSA-mrhx-c7xc-m3w5 — Does not affect your code. 4.6.6 is the first patched version for this advisory; candidate's shipped $ZodArray parser defaults abortEarly to true and stops the element loop after an aborted element, removing the unbounded issue accumulation path.
What we checked

Reviewed shipped files and metadata for zod 4.6.6. New./compile entry point is side-effectful and uses new Function for generated validators, but values are hoisted constants and string keys are escaped via JSON.stringify, so no code-injection path was found. No network, child_process, credential access or install-time hooks were observed in shipped files. Build scripts changed to a 'nub exec --node' wrapper and 'sideEffects' changed from false to a three-file list; these are toolchain/packaging changes without established runtime security impact. Package integrity matched registry sha512-4UmzQUF5fxvfcYHlbjBmejU+w+hbDnWFx4aS8kBx7nPBHcO2SqPbpAG8snbdg+Xse4pRefR98hgjFgIMCI/UhA==.

  • zod@4.6.6: reviewed files — done; checked installation — not confirmed; ran the package — not confirmed; compared with the current version — done.
    • Static source review only; no install or runtime execution.
    • Base and candidate reviewed as registry archives; repository node_modules were not built or run.
    • New compile feature was reviewed statically; generated-code behaviour not executed.
    • Repository relevance checks covered manifests, lockfile, imports and Dockerfile; no build/install was run.
  • Current source: 7bb2c963cfb7; update source: d6d6a149d04d.

The AI agent reviewed the Dependabot proposal. The review above describes its findings, evidence and coverage limitations.

Release notes

v4.6.6

Compare Source

Six malicious commits pushed to main on September 21 with a compromised git credential were reverted the same day in #​6620 and have since been removed from the history of main. None were ever published to npm. The npm provenance for 4.6.6 names 284243fb, the pre-rewrite copy of 9c219833 with identical contents.

Commits:

  • 0cd3af5e9f50ad11bbcd4407a46a21b8848ba574 docs: load the en locale directly in Zod Mini
  • 9d5e60f162997b764b40d45aa02481888db50dea docs: import locales from zod/locales, not z.locales
  • 34a626f307871ecbb0123947559328dc52ed5e3e docs: tighten the z.locales tree-shaking note
  • 74304ce5b5796b5edb9a88ceb8e170b6ea3e82df release: stage npm versions instead of publishing them (#​6622)
  • b2ef983e0711ff6517d6020b021071e2e5653efa docs: add Code for Japan to bronze sponsors
  • 8fb2faf0584828a6a027d8279db36f6f52784462 docs: crop the Code for Japan logo and list it first in bronze
  • bacad518c41610d654658172d325874bf0ee6290 docs: square the Code for Japan logo to match the other bronze tiles
  • c42596bd1c87e941d7241d10e012c5c196adcc10 release: hold the run on the release-approval environment until the staged versions are approved (#​6638)
  • a2ba4a67986a3e6b2b131be30084a2f460d9ea67 fix(types): declare record output and input as members, not base type arguments (#​6645)
  • a0ebf970f2d752e5f58527cd2942ff48be4f46bd fix(v4): check array, set and map size bounds before parsing elements (#​6652)
  • 529f78dd9431b07f49647fee3aa0819b3f8b66b7 4.6.6
  • 9c219833c3a3764fa1f1ea9c5ecec8254a63c0b1 ci: stage at verbose so a failed OIDC token exchange says why

v4.6.5

Compare Source

Commits:

  • d2b135cfb7a3582b9eb515756b9166bcb9521f4a docs: add the 4.6.x patch highlights to the 4.6 post
  • f1448f7cee00df9fe1e9ad84a000aa1828cc8bc1 docs: fold the 4.6.x patch highlights into the 4.6 post's own sections
  • de65a5cb39ed22a507fac935788f718fa88d104f docs: lead the properties section with the check and add a Zod Mini tab (#​6598)
  • 56222cd1532c07bcb91b67df529cab4c0a215330 feat(instanceof): key the .properties() shape off the instance type (#​6600)
  • ca0229a404818290e6cdcfefcd7eb2d04bcbb543 Revert "feat: add z.currencyCode() over a vendored ISO 4217 list, refreshed weekly by CI (#​6595)"
  • cc4cd4ee9c52fcaa10964e48cc144541e41a5ed9 Revert "Revert "feat: add z.currencyCode() over a vendored ISO 4217 list, refreshed weekly by CI (#​6595)""
  • 0f3f5ee3ca56c7574bf849e54f79e9a6e02562ee 4.6.5
  • 59bbc03e10c636b9eb3c393dfeb552819774ec21 chore: re-pin the integration peers to the workspace zod after the 4.6.5 bump

v4.6.4

Compare Source

A patch on top of 4.6.3.

  • d6bc1e30 feat: add z.currencyCode() over a vendored ISO 4217 list, refreshed weekly by CI (#6595)
  • ad32d751 perf: z.url() rejects an invalid URL with URL.canParse() instead of a throwing constructor, about 50x faster; fewer allocations on the validation path (#6588)
  • 2bb08717 chore: re-pin the integration peers to the workspace zod after the 4.6.4 bump
  • f6e1701a chore(deps): bump next to 15.5.25 and vite to 7.3.6 (#6153)

v4.6.3

Compare Source

A patch on top of 4.6.2.

  • 413cce9a fix(v4): make z.properties() a check again (#6594) — removes the standalone z.properties() schema from 4.6.0; z.instanceof().properties() and .check(...z.properties()) are unchanged
  • 75d63ee1 docs: show only the .properties() method form in the 4.6 post
  • 46da9572 docs: match the error-message examples to what the parsers emit

v4.6.2

Compare Source

A patch on top of 4.6.1.

v4.6.1

Compare Source

A patch on top of 4.6.0.

v4.6.0

Compare Source

Zod 4.6 is now available.

npm install zod@latest

At a glance:

  • .validate() — checks input validity without building a result (up to 35x faster than .safeParse().success on a compiled schema)
  • z.instanceof().properties() — validates properties of an instance
  • fromJSONSchema() — enforces six validation keywords it used to ignore
  • z.iban() — electronic-format IBAN plus mod-97 checksum
  • z.withParser() — installs a parser generated elsewhere, for environments without new Function
  • Faster CommonJS — drops the getter on every export (~3x faster z.validate() under require)
  • Memory retention in recursive schemas — releases the parsed input, fixing a 4.5 out-of-memory regression
  • @zod/mini — Zod Mini as a standalone package, versioned in lockstep with zod since 4.5

.validate()

Standalone boolean validation, in Zod, Zod Mini, and Zod Core. It answers "is this input valid?" without constructing a ZodError, which makes rejection cheap. The return type is a guard on the schema's input type.

z.validate(z.string(), "hi"); // true
z.validate(z.string(), 42);   // false

It is a method on Zod Classic schemas too. (#6547)

const Player = z.object({
  username: z.string(),
  xp: z.number(),
});

if (Player.validate(data)) {
  data.username; // narrowed
}

In conjunction with z.compile(), this can be up to 35x faster than .safeParse().success on invalid input.

Time per call on invalid input, schemas compiled with z.compile(), safeParse().success as a gray bar with .validate() as a blue bar inside it: a union of 3 objects 28 ns (34.9x faster), an array of 10 strings 23 ns (24.6x), a 3-element tuple 28 ns (16.1x), a 5-key object 21 ns (16.3x), a discriminated union of 3 21 ns (15.7x), z.number() 19 ns (13.8x), z.string() 19 ns (13.2x), z.boolean() 19 ns (13.7x); up to 34.9x faster

Time per call on invalid input, compiled with z.compile() — lower is better (benchmark)

Uncompiled schemas

Without compilation it is up to 5.9x faster. The saving is the result object: .safeParse() allocates one with an accessor pair on every call, and .validate() allocates nothing.

Time per call on invalid input, plain schemas, safeParse().success as a gray bar with .validate() as a blue bar inside it: a union of 3 objects 659 ns (1.5x faster), an array of 10 strings 211 ns (2.3x), a 3-element tuple 166 ns (2.5x), a discriminated union of 3 93 ns (3.3x), a 5-key object 75 ns (3.9x), z.number() 47 ns (5.3x), z.string() 42 ns (5.8x), z.boolean() 42 ns (5.9x); up to 5.9x faster

Time per call on invalid input, plain schemas — lower is better (benchmark)

Both charts measure the failure path. The key feature of .validate() is that it can short-circuit on the first issue it encounters, instead of aggregating a full ZodIssue[] array.

[!NOTE]
Async refinements are covered by .validateAsync().

z.properties()

A new API for validating specific properties of an object. Unlike z.object() it validates in-place, so it plays nice with class instances. (#6536)

const responseLike = z.properties({ status: z.number().min(200).max(299) });

responseLike.parse(new Response("ok", { status: 200 }));  // ✅ a real Response
responseLike.parse({ status: 204 });                      // ✅ a plain object

A corresponding .properties() method has been added to ZodInstanceOf.

Zod

const okResponse = z.instanceof(Response).properties({
  ok: z.literal(true),
  status: z.number().min(200).max(299),
});

Zod Mini

const okResponse = z.instanceof(Response).check(...z.properties({
  ok: z.literal(true),
  status: z.number().check(z.minimum(200), z.maximum(299)),
}));

The input comes back untouched, so the prototype survives and the methods still work. That is the part z.object() cannot do: it would hand back a plain object and the Response would be gone.

const res = await fetch("/api/user");

okResponse.parse(res) === res; // ✅ true

fromJSONSchema()

Six additional JSON Schema keywords are now supported in z.fromJSONSchema(). (#6535)

const schema = z.fromJSONSchema({
  type: "object",
  minProperties: 2,      // also maxProperties
});

schema.parse({ a: 1 });        // ❌ too few properties
schema.parse({ a: 1, b: 2 });  // ✅

Both property bounds count the input's own keys. Array uniqueness is structural, so [{ a: 1 }, { a: 1 }] is a duplicate.

z.fromJSONSchema({ type: "array", uniqueItems: true }).parse([{ a: 1 }, { a: 1 }]); // ❌

z.fromJSONSchema({
  type: "array",
  contains: { type: "number" },   // also minContains and maxContains
  minContains: 2,
}).parse(["a", 2]);               // ❌ only one number

z.iban()

A new string format: an IBAN in electronic format, with a valid ISO 7064 MOD 97-10 checksum. (#6571)

z.iban().parse("DE89370400440532013000"); // ✅
z.iban().parse("DE89370400440532013001"); // ❌ bad checksum

z.withParser()

z.compile() builds its parser with new Function, which a strict Content Security Policy blocks. z.withParser() is that installer on its own: it takes a parser generated somewhere else, at build time or by a native compiler, and installs it under the same contract. (#6575)

const Player = z.object({ username: z.string(), xp: z.number() });

// isPlayer is a type guard your build step generated
const Fast = z.withParser(Player, (input) =>
  isPlayer(input) ? { username: input.username, xp: input.xp } : z.INVALID
);

The supplied parser owns the whole result, so it has to return what the schema would have returned. This one rebuilds the object rather than handing back its input, because z.object() strips unknown keys. Returning z.INVALID hands the input to the runtime, which stays the only source of ZodErrors.

Faster CommonJS

TypeScript compiles a re-export to a getter, and 252 of the 255 exports on Zod 4.5's CommonJS entrypoint were getters. V8 could not see a constant callee behind one, so it could not inline the call. The 4.6 build emits plain properties and freezes the exports object. On a compiled schema, z.validate() under require is about 3x faster than it was in Zod 4.5. (#6564)

const { z } = require("zod");
const CompiledPlayer = z.compile(Player);

z.validate(CompiledPlayer, data); // ~3x faster than Zod 4.5

Only calls through the namespace were affected. A method call like Player.safeParse(data) never reads the exports object, and the ESM build is unchanged.

Memory retention in recursive schemas

A recursive schema held the input and output of its last parse until the next parse replaced it, so one long-lived schema pinned every object it had touched. Zod 4.4 released that input and Zod 4.5 did not, which surfaced as an out-of-memory failure on a repository-wide lint run. The parse state is weak throughout now: one parse of a 29k-node tree retains 2.2 MB where it used to retain 10.1 MB, and recursive parses give up about 6% for it. (#6572)

const Category = z.object({
  name: z.string(),
  get children() {
    return z.array(Category);
  },
});

Bug fixes

⚠️ Error maps run on the first read of error

Because safeParse() now builds its error lazily, error maps — global, locale, and per-schema error — run when result.error is first read, not at parse time. Code that swaps z.config() between the parse and the read gets the newer configuration. (#6519)

const result = schema.safeParse(12);
z.config(z.locales.fr());
result.error.issues[0].message; // French in 4.6, English in 4.5

An error map with a side effect never runs if nothing reads the error. Throwing parses are unaffected — .parse() builds and throws its error immediately, never takes the lazy path, and its stack still points at your call site.

⚠️ z.emoji() rejects component-only strings

Unicode's Emoji_Component property covers the pieces that attach to an emoji, so z.emoji() accepted "123", "#", "*", and a lone zero-width joiner, variation selector, or skin tone modifier. The pattern now requires at least one pictograph, regional indicator, or keycap. (#6532)

z.emoji().parse("😀");   // ✅
z.emoji().parse("1️⃣");   // ✅ the keycap is the anchor
z.emoji().parse("123");  // ❌ was accepted in 4.5

Flags, subdivision flags, skin-tone-modified emoji, and ZWJ sequences are unchanged. Closes #6515.

⚠️ Numeric enum options no longer include the reverse mappings

A numeric TypeScript enum also carries its reverse mapping (0 to "UK") at runtime. The parser already ignored those keys, but .options was read straight off the enum object, so a three-member enum listed six values and three of them failed to parse. (#6542)

enum Country { UK, Germany, France }

z.enum(Country).options; // 4.5: ["UK", "Germany", "France", 0, 1, 2] — 4.6: [0, 1, 2]
⚠️ base64 patterns

The runtime patterns for z.base64() and z.base64url() are the character sets, with length and padding enforced in code, so a multi-megabyte string can no longer overflow the regex stack through a composed schema. The JSON Schema output still emits the exact block forms, so z.toJSONSchema() is unchanged. (#6534, #6527)

Composing z.base64() into a template literal now checks the alphabet but not the length, which is how z.creditCard() already behaves there. The exported z.regexes.base64url is now the length-aware form, so it overflows on a multi-megabyte input the same way z.regexes.base64 does.

⚠️ The email pattern dropped its lookaheads

z.email() opened with two lookaheads, and the second scanned the whole string before the match began. Both are gone, and the rule they enforced — no empty segment in the local part — is expressed structurally instead, so z.email() accepts and rejects exactly what it did before. Valid addresses validate roughly twice as fast. (#6573)

The pattern string is user-visible, and every copy of it changes: z.regexes.email, which has no capture groups now — neither of the two it used to expose held a usable value; issue.pattern on a failed z.email(); and the pattern that z.toJSONSchema() emits, which no longer carries a lookahead, so validators outside ECMAScript can compile it.

Composing an email into a template literal also stops applying its no-consecutive-dots rule to the rest of the string.

z.templateLiteral([z.email(), "|", z.string()]).parse("a@b.cc|a..b");
// 4.5: ❌ — the lookahead reached past the email segment — 4.6: ✅
⚠️ Chained checks no longer overwrite each other in JSON Schema

Each check used to write its own bounds into the schema as it attached, in chain order, so a format check applied after .min() and .max() replaced the tighter values with its own range. The converter folds the checks as a conjunction now. The order they are chained in no longer changes the output. (#6554, #6553)

z.toJSONSchema(z.number().min(0).max(23).int());
// 4.5: { minimum: -9007199254740991, maximum: 9007199254740991 }
// 4.6: { minimum: 0, maximum: 23 }

Runtime parsing enforced the bounds in every version. Only the emitted schema was wrong. The same fold fixes two more cases: a repeated multipleOf kept the first divisor and dropped the rest, so z.number().multipleOf(2).multipleOf(3) emitted a schema that accepts 4, and z.string().min(8).length(5) emitted minLength: 5, widening a bound the runtime still rejected. Closes #6550.

⚠️ Metadata members materialize on first read

Eight members on a Zod Classic schema — .format, .minLength, .maxLength, .minValue, .maxValue, .isInt, .minDate and .maxDate — are computed from the checks now instead of being written onto every instance at construction. Each one is a prototype getter that becomes an own property on first read. (#6554)

const s = z.string().min(3).max(9);

Object.keys(s); // 4.5: ["def", "type", "format", "minLength", "maxLength"] — 4.6: ["def", "type"]
s.minLength;    // 3 in both
Object.keys(s); // 4.6: ["def", "type", "minLength"]

A key is absent until something reads it, and Object.assign({}, schema) copies only the members that have been read. Deleting one restores the getter, and the next read recomputes it.

The values can move too, because the getters read the same fold the JSON Schema converter does. An order-dependent chain reports the tighter bound now instead of whichever check wrote last.

z.string().min(8).length(5).minLength; // 4.5: 5 — 4.6: 8

Commits

Zod 4.6 rolls up 72 commits.

v4.5.4

Compare Source

Commits:

  • 84e416fbf4740527bbc8f319634f4e1b065bb42c fix(v4): stop the cycle walk from firing a default factory (#​6500)
  • e8e206fa33ac5fe7ce20a2beb12d57b1cb3df653 4.5.4

v4.5.3

Compare Source

Commits:

  • e6b6ab347675cd2bd54b1bdbed16f98c59be82a9 docs(blog): widen the z.compile example to a 20-property schema
  • 87d6464418582bb96fc665a01f852ca6da324ad0 fix(docs): drop the OG description when the title wraps past two lines
  • 99fce394a026823e602b9c30d8d5d9f5f1932ce7 bench(v4): z.compile() against zod-compiler (#​6499)
  • e3a695b6bf3f0d591ea682816e3cdaea04b0f967 docs(v4): record the email regex and container output-shape findings under Open
  • 7e24a24288183ce02554f1ded7775d0650a7b7e6 docs(blog): drop the reading time and put a GitHub link in the navbar
  • eab51ff3592b2d11d863f4ee4d5452f31a3de1b6 fix(v4): emit record numeric keys as strings in toJSONSchema (#​6497)

v4.5.2

Compare Source

Commits:

  • a354314ac04fdd5484aa62dd5c3a4b553211a0e4 fix(docs): keep blog posts out of the docs collection (#​6484)
  • d378c42aff6869f0929058a7923cd775880f5c4c ci: drop canary publishing from the release workflow (#​6487)
  • 212b941791e7faae078e17645eb612824fd8f79a fix(v4): let a prototype method getter answer a bare call so vi.spyOn works (#​6488)
  • e7576f542a7bc7ef3cc5eeec237714fd0e6b6e98 docs(blog): let the page show through the navbar in dark mode (#​6489)
  • fedb06fafe33a66ce0b5c236ad2557e0a5a170fe fix(docs): match the blog TOC hover bar to the 2px active indicator
  • 6c932fcb2eea6eb671710ea058ca9fdc382ada89 chore: bump devcontainer image to Node 24 (#​6470)
  • 6635d9dd367a664109de83c021995821f48efa29 docs(blog): soften the "method memoization" attribution
  • 019ae299cc75daa132bf1acf59086a520abf6b85 fix(docs): drop ISR on the docs route so the home page hydrates
  • 652bb438aa4c626c1cd7948c6849c4691239fca7 chore(docs): drop the scroll log from the route-change scroller
  • 571c8e8a3d73b4305f4abfdd6977773cc12f2bf5 fix(docs): render blog tabs with the stock fumadocs tab card
  • 9a193aa24b4efa3b315b91d4c56c8bc385b8513f 4.5.2

@vercel

vercel Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
openmetal Ready Ready Preview Nov 2, 2026 2:42am UTC

Request Review

This branch was successfully deployed

1 active deployment
Preview — d6d6a149 Deployed Nov 2, 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.

0 participants