fix(v4): reject whitespace in z.base64() to close atob bypass#5888
Conversation
atob() strips ASCII whitespace before validating, which let strings like "123 " sneak past the length-mod-4 short-circuit in isValidBase64. Reject whitespace explicitly before the length check so the runtime matches the strict regex we already advertise via bag.patterns / contentEncoding. Refs #5865.
|
TL;DR — Fixes a whitespace bypass in Key changes
Summary | 3 files | 1 commit | base:
The fix is a single linear scan (
|
There was a problem hiding this comment.
Reviewed — no issues found.
Task list (5/5 completed)
- Read the full diff to understand the changes
- Read the affected source file (schemas.ts) for context around the change
- Review the regex change for correctness and completeness
- Review tests for adequate coverage
- Submit review
Claude Opus | 𝕏
|
Landed in Zod 4.4 |
Summary
Closes the whitespace bypass in
isValidBase64()raised in #5865.atob()strips ASCII whitespace before decoding, which lets strings like"123 "(length 4, mod-4 passes,atobstrips and parses"123") sneak past the existing length check. Fix: reject whitespace explicitly before the length check so the runtime matches the strict regex we already advertise viabag.patterns/contentEncoding.Why not just enforce
regexes.base64at runtime?That's what the validator originally did. It was replaced with the
atob-based check specifically for perf on large inputs in #4386 (commitec7c89d9), which also added thebig base64 and base64urlregression test inpackages/zod/src/v4/classic/tests/string.test.tsthat round-trips 10MB throughz.base64().parse(). The/\s/.test()short-circuit is a single deterministic linear scan with no backtracking, so it fits inside that perf envelope without reintroducing the regex.Why keep the strict default at all?
Discussed in detail at #5865 (comment). Short version: zod's string formats are strict by default, the unpadded base64url niche is already served by
z.base64url()/z.jwt(), and strict-vs-lenient is asymmetric — strict can be loosened later via opt-in (z.base64({ padding: "optional" })) non-breakingly, while lenient cannot be tightened without a breaking change.Test plan
pnpm vitest run packages/zod/src/v4/classic/tests/string.test.ts -t "base64"— passespnpm vitest run packages/zod/src/v4— 2640 tests passingpnpm vitest run(via pre-push hook) — 3689 tests passing, no type errorspackages/zod/src/v4/classic/tests/string.test.tscovering the bypass ("123 ") plus leading/trailing/internal space, newline, and tabpackages/zod/src/v4/mini/tests/string.test.tsRefs #5865.