Upgrade zod from 3.25.76 to 4.6.6 to fix GHSA-mrhx-c7xc-m3w5
Verified. zod 3.25.76 → 4.6.6, with code changes in 2 files. The repo's checks pass before and after the upgrade (4/4 tests).
Why this repo
zod 3.25.76 is a direct dependency and is in the vulnerable range of:
- GHSA-mrhx-c7xc-m3w5 Zod can exhaust memory while validating oversized constrained collections (high). GHSA-mrhx-c7xc-m3w5, CVE-2023-54404. Fixed in 4.6.6.
The advisories name no function to look for, so Teire cannot rule out that your code reaches the vulnerable path.
What changed upstream
Comparing the public API of 3.25.76 and 4.6.6: zod: Zod can exhaust memory while validating oversized constrained collections.
18e71c71Rename the JSON Schemaprocesshelper so bundler polyfills cannot collide (#6541) by @colinhacks— v4.6.0
413cce9afix(v4): make z.properties() a check again (#6594) — removes the standalonez.properties()schema from 4.6.0;z.instanceof().properties()and.check(...z.properties())are unchanged— v4.6.3
Commits between 3.25.76 and 4.6.6:
9c21983ci: stage at verbose so a failed OIDC token exchange says why529f78d4.6.6a0ebf97fix(v4): check array, set and map size bounds before parsing elements (colinhacks/zod#6652)a2ba4a6fix(types): declare record output and input as members, not base type argumen...c42596brelease: hold the run on the release-approval environment until the staged ve...bacad51docs: square the Code for Japan logo to match the other bronze tiles8fb2fafdocs: crop the Code for Japan logo and list it first in bronzeb2ef983docs: add Code for Japan to bronze sponsors74304cerelease: stage npm versions instead of publishing them (colinhacks/zod#6622)34a626fdocs: tighten the z.locales tree-shaking note
Changes
| File | Change |
|---|---|
npm-zod/package.json | 3.25.76 → 4.6.6 |
npm-zod/package-lock.json | 3.25.76 → 4.6.6 |
npm-zod/package-lock.json | regenerated by npm (+4/−4 lines) |
npm-zod/src/schema.ts | edited by NVIDIA Nemotron 3 Ultra |
npm-zod/src/intake.ts | edited by NVIDIA Nemotron 3 Ultra |
Fixed zod v4 breaking changes in two files: 1. schema.ts: z.record() now requires two arguments (keySchema, valueSchema). Changed z.record(z.string()) to z.record(z.string(), z.unknown()) to match the Record<string, unknown> type for the labels field. 2. intake.ts: zod v4 replaced ZodError.errors with ZodError.issues. Updated both intake() and firstProblem() to use parsed.error.issues instead of parsed.error.errors. Each issue has a path property (array of string|number) that works the same way as before.
Code diff
--- a/npm-zod/src/schema.ts
+++ b/npm-zod/src/schema.ts
@@ -12,7 +12,7 @@
email: z.string().email(),
channel: z.nativeEnum(Channel),
subject: z.string().min(1),
- labels: z.record(z.string()).default({}),
+ labels: z.record(z.string(), z.unknown()).default({}),
})
.strict();
--- a/npm-zod/src/intake.ts
+++ b/npm-zod/src/intake.ts
@@ -6,11 +6,11 @@
export function intake(input: unknown): IntakeResult {
const parsed = Ticket.safeParse(input);
if (parsed.success) return { ok: true, ticket: parsed.data };
- return { ok: false, problems: parsed.error.errors.map((issue) => issue.path.join(".") || "(root)") };
+ return { ok: false, problems: parsed.error.issues.map((issue) => issue.path.join(".") || "(root)") };
}
/** The first problem's field, for a one-line error banner. */
export function firstProblem(input: unknown): string | null {
const parsed = Ticket.safeParse(input);
- return parsed.success ? null : (parsed.error.errors[0]?.path.join(".") ?? null);
+ return parsed.success ? null : (parsed.error.issues[0]?.path.join(".") ?? null);
}
Verification
Each side installed its own lockfile in its own sandbox (Token Factory Sandboxes, a microVM per command; the install had the network, the checks had no network interface).
| Check | Before (3.25.76) | After (4.6.6) |
|---|---|---|
typecheck npm run -s typecheck | ✓ exit 0 | ✓ exit 0 |
test npm run -s test -- --reporter=default --reporter=json --outputFile.json=/tmp/tei | ✓ exit 0 · 4/4 passed | ✓ exit 0 · 4/4 passed |
Evidence: verified. typecheck, test pass on the baseline and after; 4/4 tests passed.
Receipt
| Step | Model | Tokens in / out | Cost |
|---|---|---|---|
| Read the release notes | NVIDIA Nemotron 3 Ultra | 4.8k / 2.3k | $0.012 |
| Fix attempt 1: 20 steps → verified | NVIDIA Nemotron 3 Ultra | 221.3k / 3.1k | $0.146 |
| Review of the passing diff: no problems found | NVIDIA Nemotron 3 Ultra | 3.0k / 731 | $0.0052 |
| Sandboxes: install + checks, both sides | Token Factory Sandboxes | — | 28 s |
Model total: $0.163.
Risks not covered
-
z.iso.datetime()andz.iso.datetime({ offset: true })no longer accept minute-precision input like2020-01-01T06:15Z;local: truestill admits2020-01-01T06:15— "RFC 3339 mandates seconds.z.iso.datetime()andz.iso.datetime({ offset: true })no longer accept minute-precision input like2020-01-01T06:15Z.local: truestill admits2020-01-01T06:15, si" (source) -
Chained checks no longer overwrite each other in JSON Schema output; the converter now folds checks as a conjunction, so order no longer changes output (e.g.,
.min()and.max()bounds are preserved even after a format check) — "⚠️ 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 `.m" (source) -
z.emoji()pattern now requires at least one pictograph, regional indicator, or keycap; no longer accepts standalone variation selectors, skin tone modifiers, or joiners like"123","#","*", or lone zero-width joiners — "Unicode'sEmoji_Componentproperty covers the pieces that attach to an emoji, soz.emoji()accepted"123","#","*", and a lone zero-width joiner, variation selector, or skin tone modifier. " (source) -
Numeric enum options no longer include reverse mappings in JSON Schema output;
z.base64()andz.base64url()runtime patterns are now character sets with length/padding enforced in code, not regex, to avoid regex stack overflow on large strings — "⚠️ Numeric enum options no longer include the reverse mappings … The runtime patterns forz.base64()andz.base64url()are the character sets, with length and padding enforced in code, so a multi-" (source) -
A record's key schema now governs only keys that match it (like TypeScript index signatures); intersecting an object with a pattern-keyed record no longer rejects the object's own keys, and
unrecognized_keysissues no longer abort the schema, allowing multiple issues to be reported — "A record's key schema now governs only the keys that match it, the way TypeScript treats an index signature. Intersecting an object with a pattern-keyed record no longer rejects the object's own keys." (source) -
JSON Schema
$defsentries no longer include redundantidfields; this may affect consumers reading those internalidfields directly — "JSON Schema$defsentries no longer include redundantidFixed in #5759. JSON Schema conversion throughz.toJSONSchema()now strips redundantid" (source) -
CUID validation tightened and CUID v1 deprecated;
z.cuid()now uses stricter regex — "CUID validation throughz.cuid()has been tightened, and CUID v1 is now deprecated. Fixed in #5880." (source) -
Empty
z.union([]),z.xor([]), and discriminated unions no longer crash at construction time; they now construct and fail at parse time — "Emptyz.union([]),z.xor([]), and discriminated unions no longer crash at construction time. They construct and fail at parse time. Fixed in #5869." (source) -
Lint, end-to-end and integration suites were not run; dependencies were installed without lifecycle scripts.
Rollback
Revert this pull request: every file listed under Changes returns to zod 3.25.76.
Commands
@teire rebase · @teire recreate · @teire explain · @teire ignore zod
Teire · live run · NVIDIA Nemotron 3 Ultra on Nebius Token Factory