Last 12 weeks · 31 commits
4 of 6 standards met
, , and all validate by calling and treating "didn't throw" as valid. Step 2 of the WHATWG basic URL parser deletes every ASCII tab, LF and CR from the input before parsing — it flags a validation error but recovers rather than failing — so succeeds on strings containing them and reports on a string that isn't the one it was given. On 4.4.3: The host case is the sharp one: is , so the string Zod validated and the string Zod returned are URLs pointing at different hosts. Tab, LF and CR are the only characters with this property. I enumerated all 32 C0 controls at host, path, query, fragment, leading and trailing positions: every other one is either percent-encoded () or rejected, and U+0009/U+000A/U+000D are the only three that can be injected into the host and still validate. It also breaks a self-consistency invariant — is still emitted as the by , and it rejects these: I scanned every code point 0x0000–0x10FFFF inserted into and : disagrees with on exactly those three characters and nothing else. already handles the identical hazard one section down — "atob ignores whitespace, so reject it up front" — so the shape of the fix is settled by precedent: scan for ahead of each call. Happy to open a PR.
Fixes #6395. Step 2 of the WHATWG basic URL parser deletes every ASCII tab, LF and CR from its input before parsing, rather than failing on them. therefore succeeds and reports on a string that isn't the one it was given, and every format validated through it inherited that. The consequential case is the host: is , so on 4.4.3 and validated one host and returned a string naming a different one. returned and returned . Tab, LF and CR are the only characters with that property. I enumerated all 32 C0 controls at host, path, query, fragment, leading and trailing positions: every other one is percent-encoded () or rejected, and U+0009/U+000A/U+000D are the only three that can be injected into the host and still validate. Separately, scanning every code point 0x0000–0x10FFFF inserted into and , disagrees with on exactly those three and nothing else — after the change they agree everywhere except zone IDs (), which Zod has never accepted and this doesn't touch. 's own pattern rejects these strings, so and the schema Zod publishes for it disagreed on the same input. That's now consistent; is untouched, so no emitted pattern moved. The fix is a scan for ahead of each call, the same shape as rejecting the whitespace ignores. tests the trimmed string, so the deliberate leading/trailing whitespace allowance is untouched — still returns . Scope — what this does not fix.* accepts and returns the other C0 controls (U+0000–U+0008, U+000B, U+000C, U+000E–U+001F) in path, query and fragment, and at the string edges, both before and after this change. A URL string with a leading NUL (U+0000) parses successfully on both revisions and comes back with the NUL still in it. The parser percent-encodes or edge-strips those rather than deleting them, so they don't produce the validated-one-string-returned-another mismatch above, and rejecting them is a larger default-behaviour question I've deliberately left alone. So this narrows a specific inconsistency; it does not make output control-character-free, and shouldn't be read as closing a security boundary. Two behaviour changes worth calling out explicitly: routes through and is equally affected — it accepted and now rejects it. was not affected by the output half of the defect: it returns , so on 4.4.3 it already produced sanitized output ( → ). This PR makes it reject instead, for consistency with the non-normalizing path, and the test asserts that. If you'd rather the normalize path keep silently sanitizing, that's a one-line change. Regression surface. Differential over 41,032 rows — 24 hand-written url/ipv6/cidrv6 seeds crossed with , space, NBSP, , , U+2028, U+2029, NUL, U+0001 and U+001F injected at head/middle/tail, plus 40k seeded-PRNG mutations of the same seeds — run against the built artifact on both revisions: 3,764 accept → reject ( 1237, 1237, 340, 684, 266) 0 reject → accept 0 inputs accepted on both sides whose returned value changed 0 accept → reject flips on an input that does not contain a tab, LF or CR and classic agree on every row, on both revisions Three axes. Load average 5.9–6.8, so under AGENTS.md's ~8 bar but not lab-grade; the numbers below are two independent runs and I've given the spread rather than the prettier of the two. is a single character class with no quantifier, alternation or group, so it can't backtrack, and there's no so no state. Long-input timings, min-of-9: at 1 MB, 0.347 → 0.372 ms, 0.177 → 0.207 ms, 0.196 → 0.223 ms, scaling linearly from 1 KB through 1 MB on both revisions. The guard strictly reduces what reaches , since it rejects before construction. Suite is green on all three CI TypeScript legs (5.5.4, 6.0.3, 7.0.2): 348 files / 3965 tests, no type errors, with no pre-existing failures on either revision. , , , and the resolution and integration workspaces all pass. What I haven't verified:* whether any real caller depends on tab/newline being tolerated. This is a strictness change and I found no test, issue or doc asserting the old behaviour, but it changes a default and the call is yours. The decision above is the part I'd most expect you to want different. I also left zone IDs alone (), which accepts and rejects — that divergence is pre-existing and I haven't judged whether it's intended.
Follow-up to the suggestion on #6351: pulling the shared closures out as their own change, rebased onto current , with nothing else from that branch. What this does Six checks (, , , , , ) each defined the same default inline, so every check instance allocated its own copy of one of two identical closures: They now share two module-level functions, and . The payoff is per-instance memory rather than bundle size, in the same direction as #6318: one shared function instead of one closure per check instance. Retaining 200,000 schemas goes from 3,600.4 to 3,544.4 bytes per schema, a 56 byte drop that is stable across runs since it counts allocations rather than time. Bundle size is not the argument here and does not improve; those numbers are in #6351. Behaviour Check instances now share one function reference where each previously held an identical closure. stays user-overridable through the same , and nothing in the codebase or tests observes closure identity.
Repository: colinhacks/zod. Description: TypeScript-first schema validation with static type inference Stars: 43453, Forks: 2118. Primary language: TypeScript. Languages: TypeScript (89.6%), MDX (9.2%), HTML (0.7%), JavaScript (0.4%), CSS (0.1%). License: MIT. Homepage: https://zod.dev Topics: runtime-validation, schema-validation, static-types, type-inference, typescript. Latest release: v4.4.3 (3mo ago). Open PRs: 100, open issues: 213. Last activity: 5h ago. Community health: 85%. Top contributors: colinhacks, JacobWeisenburger, scotttrinh, jeremyBanks, samchungy, igalklebanov, tmcw, noritaka1166, alexxander, pullfrog[bot] and others.