fix(v4): skip __proto__ key in object catchall#5898
Conversation
|
New pull request. Leaping into action...
|
|
TL;DR — Fixes a security issue where Key changes
Summary | 2 files | 1 commit | base: Prototype-safe catchall iteration
The fix is intentionally minimal — one conditional inside the hot path.
|
In handleCatchall, an unrecognized __proto__ key in the input was
being assigned directly to the fresh {} result object via
(final.value)[key] = result.value. That assignment goes through the
__proto__ setter and replaces the prototype of the parsed result,
giving the validated object attacker-controlled inherited properties
without polluting Object.prototype.
Skip __proto__ before the catchall runs. Mirrors the existing guard
in $ZodRecord and the v3 object-assembly path. No other behavior
change — for..in iteration of input is preserved.
Reported in:
- GHSA-r34p-xfmx-58wv
- GHSA-84jv-fqfx-wxhr
7f658ed to
5b506b8
Compare
|
Landed in Zod 4.4 |

Summary
In
handleCatchall(), an unrecognized__proto__key in the input is currently assigned directly to a fresh{}result object via(final.value)[key] = result.value. Because that goes through the__proto__setter on a plain object, the assignment replaces the prototype of the parsed result, giving the validated object attacker-controlled inherited properties.Object.prototypeitself is not affected.PoC on
4.3.6:Same shape reproduces with
passthrough()andcatchall(...), sync/async, and{ jitless: true }(the JIT fastpass falls through to the samehandleCatchallfor unknown keys).Fix
One-line guard in the catchall loop:
Mirrors the existing
__proto__skip in$ZodRecord(schemas.ts:2867) and the v3 object-assembly guard atparseUtil.ts:144. Deliberately conservative —for..initeration of input is preserved, so behavior for any input other than one with a__proto__own key is unchanged. Inherited prototype-chain enumeration (the separate concern in GHSA-9hmj-h52h-mxcf) is not addressed here; that's a narrower attack model and a noisier behavior change worth handling separately.Reported in:
Test plan
object.test.tscoveringlooseObject/passthrough/catchall(unknown)/safeParseAsync+{ jitless: true }/strict(), assertingObject.getPrototypeOf(parsed) === Object.prototypeand that the injected key is not visible on the result.pnpm vitest run— 3753 passed across 333 files, no regressions.