anyvali 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +370 -0
  3. package/VERSION +1 -0
  4. package/dist/errors.d.ts +6 -0
  5. package/dist/errors.d.ts.map +1 -0
  6. package/dist/errors.js +12 -0
  7. package/dist/errors.js.map +1 -0
  8. package/dist/format/validators.d.ts +2 -0
  9. package/dist/format/validators.d.ts.map +1 -0
  10. package/dist/format/validators.js +57 -0
  11. package/dist/format/validators.js.map +1 -0
  12. package/dist/forms/index.d.ts +57 -0
  13. package/dist/forms/index.d.ts.map +1 -0
  14. package/dist/forms/index.js +586 -0
  15. package/dist/forms/index.js.map +1 -0
  16. package/dist/index.d.ts +93 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +156 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/infer.d.ts +8 -0
  21. package/dist/infer.d.ts.map +1 -0
  22. package/dist/infer.js +2 -0
  23. package/dist/infer.js.map +1 -0
  24. package/dist/interchange/document.d.ts +5 -0
  25. package/dist/interchange/document.d.ts.map +1 -0
  26. package/dist/interchange/document.js +12 -0
  27. package/dist/interchange/document.js.map +1 -0
  28. package/dist/interchange/exporter.d.ts +7 -0
  29. package/dist/interchange/exporter.d.ts.map +1 -0
  30. package/dist/interchange/exporter.js +7 -0
  31. package/dist/interchange/exporter.js.map +1 -0
  32. package/dist/interchange/importer.d.ts +4 -0
  33. package/dist/interchange/importer.d.ts.map +1 -0
  34. package/dist/interchange/importer.js +229 -0
  35. package/dist/interchange/importer.js.map +1 -0
  36. package/dist/issue-codes.d.ts +19 -0
  37. package/dist/issue-codes.d.ts.map +1 -0
  38. package/dist/issue-codes.js +18 -0
  39. package/dist/issue-codes.js.map +1 -0
  40. package/dist/parse/coerce.d.ts +16 -0
  41. package/dist/parse/coerce.d.ts.map +1 -0
  42. package/dist/parse/coerce.js +115 -0
  43. package/dist/parse/coerce.js.map +1 -0
  44. package/dist/parse/defaults.d.ts +7 -0
  45. package/dist/parse/defaults.d.ts.map +1 -0
  46. package/dist/parse/defaults.js +12 -0
  47. package/dist/parse/defaults.js.map +1 -0
  48. package/dist/parse/parser.d.ts +11 -0
  49. package/dist/parse/parser.d.ts.map +1 -0
  50. package/dist/parse/parser.js +13 -0
  51. package/dist/parse/parser.js.map +1 -0
  52. package/dist/schemas/any.d.ts +7 -0
  53. package/dist/schemas/any.d.ts.map +1 -0
  54. package/dist/schemas/any.js +12 -0
  55. package/dist/schemas/any.js.map +1 -0
  56. package/dist/schemas/array.d.ts +13 -0
  57. package/dist/schemas/array.d.ts.map +1 -0
  58. package/dist/schemas/array.js +73 -0
  59. package/dist/schemas/array.js.map +1 -0
  60. package/dist/schemas/base.d.ts +37 -0
  61. package/dist/schemas/base.d.ts.map +1 -0
  62. package/dist/schemas/base.js +285 -0
  63. package/dist/schemas/base.js.map +1 -0
  64. package/dist/schemas/bool.d.ts +8 -0
  65. package/dist/schemas/bool.d.ts.map +1 -0
  66. package/dist/schemas/bool.js +27 -0
  67. package/dist/schemas/bool.js.map +1 -0
  68. package/dist/schemas/enum.d.ts +9 -0
  69. package/dist/schemas/enum.d.ts.map +1 -0
  70. package/dist/schemas/enum.js +31 -0
  71. package/dist/schemas/enum.js.map +1 -0
  72. package/dist/schemas/index.d.ts +21 -0
  73. package/dist/schemas/index.d.ts.map +1 -0
  74. package/dist/schemas/index.js +21 -0
  75. package/dist/schemas/index.js.map +1 -0
  76. package/dist/schemas/int.d.ts +32 -0
  77. package/dist/schemas/int.d.ts.map +1 -0
  78. package/dist/schemas/int.js +108 -0
  79. package/dist/schemas/int.js.map +1 -0
  80. package/dist/schemas/intersection.d.ts +16 -0
  81. package/dist/schemas/intersection.d.ts.map +1 -0
  82. package/dist/schemas/intersection.js +58 -0
  83. package/dist/schemas/intersection.js.map +1 -0
  84. package/dist/schemas/literal.d.ts +11 -0
  85. package/dist/schemas/literal.d.ts.map +1 -0
  86. package/dist/schemas/literal.js +28 -0
  87. package/dist/schemas/literal.js.map +1 -0
  88. package/dist/schemas/never.d.ts +7 -0
  89. package/dist/schemas/never.d.ts.map +1 -0
  90. package/dist/schemas/never.js +19 -0
  91. package/dist/schemas/never.js.map +1 -0
  92. package/dist/schemas/null.d.ts +7 -0
  93. package/dist/schemas/null.d.ts.map +1 -0
  94. package/dist/schemas/null.js +24 -0
  95. package/dist/schemas/null.js.map +1 -0
  96. package/dist/schemas/nullable.d.ts +10 -0
  97. package/dist/schemas/nullable.d.ts.map +1 -0
  98. package/dist/schemas/nullable.js +29 -0
  99. package/dist/schemas/nullable.js.map +1 -0
  100. package/dist/schemas/number.d.ts +27 -0
  101. package/dist/schemas/number.d.ts.map +1 -0
  102. package/dist/schemas/number.js +134 -0
  103. package/dist/schemas/number.js.map +1 -0
  104. package/dist/schemas/object.d.ts +28 -0
  105. package/dist/schemas/object.d.ts.map +1 -0
  106. package/dist/schemas/object.js +153 -0
  107. package/dist/schemas/object.js.map +1 -0
  108. package/dist/schemas/optional.d.ts +11 -0
  109. package/dist/schemas/optional.d.ts.map +1 -0
  110. package/dist/schemas/optional.js +39 -0
  111. package/dist/schemas/optional.js.map +1 -0
  112. package/dist/schemas/record.d.ts +9 -0
  113. package/dist/schemas/record.d.ts.map +1 -0
  114. package/dist/schemas/record.js +45 -0
  115. package/dist/schemas/record.js.map +1 -0
  116. package/dist/schemas/ref.d.ts +10 -0
  117. package/dist/schemas/ref.d.ts.map +1 -0
  118. package/dist/schemas/ref.js +30 -0
  119. package/dist/schemas/ref.js.map +1 -0
  120. package/dist/schemas/string.d.ts +29 -0
  121. package/dist/schemas/string.d.ts.map +1 -0
  122. package/dist/schemas/string.js +181 -0
  123. package/dist/schemas/string.js.map +1 -0
  124. package/dist/schemas/tuple.d.ts +14 -0
  125. package/dist/schemas/tuple.d.ts.map +1 -0
  126. package/dist/schemas/tuple.js +59 -0
  127. package/dist/schemas/tuple.js.map +1 -0
  128. package/dist/schemas/union.d.ts +9 -0
  129. package/dist/schemas/union.d.ts.map +1 -0
  130. package/dist/schemas/union.js +45 -0
  131. package/dist/schemas/union.js.map +1 -0
  132. package/dist/schemas/unknown.d.ts +7 -0
  133. package/dist/schemas/unknown.d.ts.map +1 -0
  134. package/dist/schemas/unknown.js +12 -0
  135. package/dist/schemas/unknown.js.map +1 -0
  136. package/dist/types.d.ts +132 -0
  137. package/dist/types.d.ts.map +1 -0
  138. package/dist/types.js +3 -0
  139. package/dist/types.js.map +1 -0
  140. package/dist/util.d.ts +6 -0
  141. package/dist/util.d.ts.map +1 -0
  142. package/dist/util.js +12 -0
  143. package/dist/util.js.map +1 -0
  144. package/package.json +41 -0
  145. package/sdk/js/CHANGELOG.md +13 -0
  146. package/src/errors.ts +17 -0
  147. package/src/format/validators.ts +71 -0
  148. package/src/forms/index.ts +789 -0
  149. package/src/index.ts +285 -0
  150. package/src/infer.ts +12 -0
  151. package/src/interchange/document.ts +18 -0
  152. package/src/interchange/exporter.ts +12 -0
  153. package/src/interchange/importer.ts +285 -0
  154. package/src/issue-codes.ts +19 -0
  155. package/src/parse/coerce.ts +133 -0
  156. package/src/parse/defaults.ts +15 -0
  157. package/src/parse/parser.ts +19 -0
  158. package/src/schemas/any.ts +14 -0
  159. package/src/schemas/array.ts +83 -0
  160. package/src/schemas/base.ts +322 -0
  161. package/src/schemas/bool.ts +30 -0
  162. package/src/schemas/enum.ts +37 -0
  163. package/src/schemas/index.ts +30 -0
  164. package/src/schemas/int.ts +129 -0
  165. package/src/schemas/intersection.ts +81 -0
  166. package/src/schemas/literal.ts +34 -0
  167. package/src/schemas/never.ts +21 -0
  168. package/src/schemas/null.ts +26 -0
  169. package/src/schemas/nullable.ts +36 -0
  170. package/src/schemas/number.ts +151 -0
  171. package/src/schemas/object.ts +203 -0
  172. package/src/schemas/optional.ts +49 -0
  173. package/src/schemas/record.ts +55 -0
  174. package/src/schemas/ref.ts +35 -0
  175. package/src/schemas/string.ts +192 -0
  176. package/src/schemas/tuple.ts +74 -0
  177. package/src/schemas/union.ts +53 -0
  178. package/src/schemas/unknown.ts +14 -0
  179. package/src/types.ts +239 -0
  180. package/src/util.ts +9 -0
  181. package/tests/conformance/runner.test.ts +28 -0
  182. package/tests/conformance/runner.ts +137 -0
  183. package/tests/forms.test.ts +146 -0
  184. package/tests/unit/coerce.test.ts +136 -0
  185. package/tests/unit/collections.test.ts +99 -0
  186. package/tests/unit/composition.test.ts +80 -0
  187. package/tests/unit/date-format.test.ts +18 -0
  188. package/tests/unit/default-mutation.test.ts +32 -0
  189. package/tests/unit/defaults.test.ts +49 -0
  190. package/tests/unit/errors.test.ts +53 -0
  191. package/tests/unit/export.test.ts +270 -0
  192. package/tests/unit/inference.test.ts +306 -0
  193. package/tests/unit/interchange.test.ts +191 -0
  194. package/tests/unit/number.test.ts +195 -0
  195. package/tests/unit/object.test.ts +208 -0
  196. package/tests/unit/parser.test.ts +151 -0
  197. package/tests/unit/primitives.test.ts +111 -0
  198. package/tests/unit/security-recursion.test.ts +105 -0
  199. package/tests/unit/security.test.ts +945 -0
  200. package/tests/unit/shared-ref-falsepos.test.ts +33 -0
  201. package/tests/unit/string-pattern-redos.test.ts +46 -0
  202. package/tests/unit/string.test.ts +147 -0
  203. package/tsconfig.json +21 -0
  204. package/vitest.config.ts +7 -0
@@ -0,0 +1,945 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ string,
4
+ number,
5
+ int,
6
+ int8,
7
+ int16,
8
+ int32,
9
+ int64,
10
+ uint8,
11
+ uint16,
12
+ uint32,
13
+ uint64,
14
+ float32,
15
+ float64,
16
+ object,
17
+ array,
18
+ optional,
19
+ union,
20
+ importSchema,
21
+ RefSchema,
22
+ } from "../../src/index.js";
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // 1. ReDoS - CVE-2016-4055 / CVE-2022-25883
26
+ // ---------------------------------------------------------------------------
27
+ describe("CVE-2016-4055 / CVE-2022-25883 - ReDoS catastrophic backtracking", () => {
28
+ // These patterns are known to cause catastrophic backtracking in naive
29
+ // regex engines. The tests verify that validation completes rather than
30
+ // hanging. If the test runner's own timeout fires, the library is
31
+ // vulnerable to ReDoS.
32
+
33
+ it("handles (a+)+$ pattern without hanging", () => {
34
+ const s = string().pattern("(a+)+$");
35
+ // 24 'a' chars followed by '!' - a classic ReDoS trigger
36
+ const malicious = "a".repeat(24) + "!";
37
+ const result = s.safeParse(malicious);
38
+ // The pattern should either match or not, but must complete
39
+ expect(result.success).toBe(false);
40
+ });
41
+
42
+ it("handles (a|a)+$ pattern without hanging", () => {
43
+ const s = string().pattern("(a|a)+$");
44
+ const malicious = "a".repeat(24) + "!";
45
+ const result = s.safeParse(malicious);
46
+ expect(result.success).toBe(false);
47
+ });
48
+
49
+ it("handles ^([a-zA-Z]+)*$ pattern without hanging", () => {
50
+ // Anchored version forces full backtracking on non-matching suffix
51
+ const s = string().pattern("^([a-zA-Z]+)*$");
52
+ const malicious = "a".repeat(24) + "1";
53
+ const result = s.safeParse(malicious);
54
+ expect(result.success).toBe(false);
55
+ });
56
+
57
+ it("accepts valid input for ReDoS-prone patterns", () => {
58
+ const s = string().pattern("(a+)+$");
59
+ const result = s.safeParse("aaaaaa");
60
+ expect(result.success).toBe(true);
61
+ });
62
+ });
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // 2. Prototype Pollution - CVE-2019-10744 / CVE-2020-8203
66
+ // ---------------------------------------------------------------------------
67
+ describe("CVE-2019-10744 / CVE-2020-8203 - Prototype pollution", () => {
68
+ it("__proto__ in input does not pollute Object.prototype (default strip mode)", () => {
69
+ // Note: __proto__ cannot be used as a declared schema property key in JS
70
+ // because object literals treat it as a prototype setter, not an own property.
71
+ // Instead, we verify __proto__ is safely stripped as an unknown key.
72
+ const s = object({ name: string() }); // default unknownKeys = "strip"
73
+ const input = JSON.parse('{"name":"Alice","__proto__":{"polluted":"yes"}}') as Record<string, unknown>;
74
+ const result = s.safeParse(input);
75
+
76
+ expect(result.success).toBe(true);
77
+ if (result.success) {
78
+ expect(result.data).toEqual({ name: "Alice" });
79
+ }
80
+ // Object.prototype must not be polluted
81
+ expect(({} as Record<string, unknown>).polluted).toBeUndefined();
82
+ });
83
+
84
+ it("handles __proto__ as an unknown key with unknownKeys: 'allow'", () => {
85
+ const s = object({ name: string() }).unknownKeys("allow");
86
+ const input = JSON.parse(
87
+ '{"name":"Alice","__proto__":{"polluted":"yes"}}'
88
+ ) as Record<string, unknown>;
89
+
90
+ const result = s.parse(input) as Record<string, unknown>;
91
+
92
+ expect(result.name).toBe("Alice");
93
+ // __proto__ should be stored as own property data, not alter the prototype chain
94
+ expect(Object.getPrototypeOf(result)).toBe(Object.prototype);
95
+ expect(Object.prototype.hasOwnProperty.call(result, "__proto__")).toBe(true);
96
+ expect(
97
+ Object.getOwnPropertyDescriptor(result, "__proto__")?.value
98
+ ).toEqual({ polluted: "yes" });
99
+ // Global prototype must remain clean
100
+ expect(({} as Record<string, unknown>).polluted).toBeUndefined();
101
+ });
102
+
103
+ it("handles 'constructor' as a property name safely", () => {
104
+ const s = object({
105
+ constructor: string(),
106
+ });
107
+ const input = { constructor: "overridden" };
108
+ const result = s.parse(input);
109
+
110
+ expect(result.constructor).toBe("overridden");
111
+ // The Object constructor itself must not be corrupted
112
+ expect(({}).constructor).toBe(Object);
113
+ });
114
+
115
+ it("handles 'prototype' as a property name safely", () => {
116
+ const s = object({
117
+ prototype: string(),
118
+ });
119
+ const input = { prototype: "value" };
120
+ const result = s.parse(input);
121
+
122
+ expect(Object.prototype.hasOwnProperty.call(result, "prototype")).toBe(true);
123
+ expect((result as Record<string, unknown>).prototype).toBe("value");
124
+ });
125
+
126
+ it("strips __proto__ with unknownKeys: 'strip' without pollution", () => {
127
+ const s = object({ name: string() }).unknownKeys("strip");
128
+ const input = JSON.parse(
129
+ '{"name":"Alice","__proto__":{"polluted":"stripped"}}'
130
+ ) as Record<string, unknown>;
131
+
132
+ const result = s.parse(input) as Record<string, unknown>;
133
+
134
+ expect(result.name).toBe("Alice");
135
+ expect(Object.prototype.hasOwnProperty.call(result, "__proto__")).toBe(false);
136
+ expect(({} as Record<string, unknown>).polluted).toBeUndefined();
137
+ });
138
+
139
+ it("rejects __proto__ with unknownKeys: 'reject' without pollution", () => {
140
+ const s = object({ name: string() }).unknownKeys("reject");
141
+ const input = JSON.parse(
142
+ '{"name":"Alice","__proto__":{"polluted":"rejected"}}'
143
+ ) as Record<string, unknown>;
144
+
145
+ const result = s.safeParse(input);
146
+ expect(result.success).toBe(false);
147
+ // Global prototype must remain clean regardless
148
+ expect(({} as Record<string, unknown>).polluted).toBeUndefined();
149
+ });
150
+ });
151
+
152
+ // ---------------------------------------------------------------------------
153
+ // 3. Recursive $ref DoS - billion laughs class / CVE-2003-1564
154
+ // ---------------------------------------------------------------------------
155
+ describe("CVE-2003-1564 - Recursive $ref and billion laughs DoS", () => {
156
+ it("handles circular self-referencing $ref without infinite loop", () => {
157
+ // Schema A references itself: A -> A
158
+ const doc = {
159
+ anyvaliVersion: "1.0",
160
+ schemaVersion: "1.1",
161
+ root: { kind: "ref" as const, ref: "#/definitions/A" },
162
+ definitions: {
163
+ A: { kind: "ref" as const, ref: "#/definitions/A" },
164
+ },
165
+ extensions: {},
166
+ };
167
+
168
+ // Importing should succeed (lazy resolution).
169
+ const schema = importSchema(doc as any);
170
+
171
+ // Parsing on a pure self-cycle MUST terminate. Either by throwing
172
+ // (V8 RangeError "Maximum call stack size exceeded") or by returning
173
+ // a failure result. It MUST NOT hang the runtime. Bound wall-clock
174
+ // time to catch a hang under CI.
175
+ const start = Date.now();
176
+ let threw = false;
177
+ let result: unknown = undefined;
178
+ try {
179
+ result = schema.safeParse("anything");
180
+ } catch (e) {
181
+ threw = true;
182
+ // V8 throws RangeError on stack exhaustion. Either kind of throw
183
+ // is an acknowledgement that the cycle is not silently ignored.
184
+ expect(e instanceof Error).toBe(true);
185
+ }
186
+ const elapsed = Date.now() - start;
187
+
188
+ // 5-second wall-clock bound. Vitest default would catch a hang too,
189
+ // but an explicit bound makes the assertion intentional.
190
+ expect(elapsed).toBeLessThan(5000);
191
+
192
+ // Exactly one of: parse threw, or parse returned a failure.
193
+ // Returning success on a pure self-cycle would be a bug.
194
+ if (!threw) {
195
+ expect((result as { success: boolean }).success).toBe(false);
196
+ }
197
+ });
198
+
199
+ it("handles deeply nested schemas (100+ levels) without crash", () => {
200
+ // Build a 100-level nested object schema
201
+ let inner: ReturnType<typeof object> = object({ value: string() });
202
+ for (let i = 0; i < 100; i++) {
203
+ inner = object({ child: inner });
204
+ }
205
+
206
+ // Build a deeply nested input to match
207
+ let input: Record<string, unknown> = { value: "deep" };
208
+ for (let i = 0; i < 100; i++) {
209
+ input = { child: input };
210
+ }
211
+
212
+ // Should complete without crashing
213
+ const result = inner.safeParse(input);
214
+ expect(result.success).toBe(true);
215
+ });
216
+
217
+ it("handles mutually recursive $ref schemas (A -> B -> A) without hang", () => {
218
+ const doc = {
219
+ anyvaliVersion: "1.0",
220
+ schemaVersion: "1.1",
221
+ root: { kind: "ref" as const, ref: "#/definitions/A" },
222
+ definitions: {
223
+ A: {
224
+ kind: "object" as const,
225
+ properties: {
226
+ next: { kind: "ref" as const, ref: "#/definitions/B" },
227
+ },
228
+ required: [] as string[],
229
+ unknownKeys: "reject" as const,
230
+ },
231
+ B: {
232
+ kind: "object" as const,
233
+ properties: {
234
+ next: { kind: "ref" as const, ref: "#/definitions/A" },
235
+ },
236
+ required: [] as string[],
237
+ unknownKeys: "reject" as const,
238
+ },
239
+ },
240
+ extensions: {},
241
+ };
242
+
243
+ const schema = importSchema(doc as any);
244
+
245
+ // Parse a non-recursive input - should succeed
246
+ const result = schema.safeParse({});
247
+ // Either succeeds or fails with issues, but must not hang
248
+ expect(typeof result.success).toBe("boolean");
249
+ });
250
+
251
+ it("validates recursive schema with finite depth input", () => {
252
+ // Build a recursive tree schema: Node = { value: string, children?: Node[] }
253
+ let nodeSchema!: ReturnType<typeof object>;
254
+ const childRef = optional(
255
+ array(new RefSchema("#/definitions/Node", () => nodeSchema))
256
+ );
257
+ nodeSchema = object({
258
+ value: string(),
259
+ children: childRef,
260
+ });
261
+
262
+ const input = {
263
+ value: "root",
264
+ children: [
265
+ { value: "child1" },
266
+ {
267
+ value: "child2",
268
+ children: [{ value: "grandchild" }],
269
+ },
270
+ ],
271
+ };
272
+
273
+ const result = nodeSchema.safeParse(input);
274
+ expect(result.success).toBe(true);
275
+ });
276
+ });
277
+
278
+ // ---------------------------------------------------------------------------
279
+ // 4. Integer overflow - CWE-190
280
+ // ---------------------------------------------------------------------------
281
+ describe("CWE-190 - Integer overflow and boundary checks", () => {
282
+ describe("int8 boundaries", () => {
283
+ it("accepts 127 (max int8)", () => {
284
+ expect(int8().safeParse(127).success).toBe(true);
285
+ });
286
+
287
+ it("rejects 128 (overflow int8)", () => {
288
+ expect(int8().safeParse(128).success).toBe(false);
289
+ });
290
+
291
+ it("accepts -128 (min int8)", () => {
292
+ expect(int8().safeParse(-128).success).toBe(true);
293
+ });
294
+
295
+ it("rejects -129 (underflow int8)", () => {
296
+ expect(int8().safeParse(-129).success).toBe(false);
297
+ });
298
+ });
299
+
300
+ describe("int16 boundaries", () => {
301
+ it("accepts 32767 (max int16)", () => {
302
+ expect(int16().safeParse(32767).success).toBe(true);
303
+ });
304
+
305
+ it("rejects 32768 (overflow int16)", () => {
306
+ expect(int16().safeParse(32768).success).toBe(false);
307
+ });
308
+
309
+ it("accepts -32768 (min int16)", () => {
310
+ expect(int16().safeParse(-32768).success).toBe(true);
311
+ });
312
+
313
+ it("rejects -32769 (underflow int16)", () => {
314
+ expect(int16().safeParse(-32769).success).toBe(false);
315
+ });
316
+ });
317
+
318
+ describe("int32 boundaries", () => {
319
+ it("accepts 2147483647 (max int32)", () => {
320
+ expect(int32().safeParse(2147483647).success).toBe(true);
321
+ });
322
+
323
+ it("rejects 2147483648 (overflow int32)", () => {
324
+ expect(int32().safeParse(2147483648).success).toBe(false);
325
+ });
326
+
327
+ it("accepts -2147483648 (min int32)", () => {
328
+ expect(int32().safeParse(-2147483648).success).toBe(true);
329
+ });
330
+
331
+ it("rejects -2147483649 (underflow int32)", () => {
332
+ expect(int32().safeParse(-2147483649).success).toBe(false);
333
+ });
334
+ });
335
+
336
+ describe("uint8 boundaries", () => {
337
+ it("accepts 255 (max uint8)", () => {
338
+ expect(uint8().safeParse(255).success).toBe(true);
339
+ });
340
+
341
+ it("rejects 256 (overflow uint8)", () => {
342
+ expect(uint8().safeParse(256).success).toBe(false);
343
+ });
344
+
345
+ it("accepts 0 (min uint8)", () => {
346
+ expect(uint8().safeParse(0).success).toBe(true);
347
+ });
348
+
349
+ it("rejects -1 (underflow uint8)", () => {
350
+ expect(uint8().safeParse(-1).success).toBe(false);
351
+ });
352
+ });
353
+
354
+ describe("uint16 boundaries", () => {
355
+ it("accepts 65535 (max uint16)", () => {
356
+ expect(uint16().safeParse(65535).success).toBe(true);
357
+ });
358
+
359
+ it("rejects 65536 (overflow uint16)", () => {
360
+ expect(uint16().safeParse(65536).success).toBe(false);
361
+ });
362
+ });
363
+
364
+ describe("uint32 boundaries", () => {
365
+ it("accepts 4294967295 (max uint32)", () => {
366
+ expect(uint32().safeParse(4294967295).success).toBe(true);
367
+ });
368
+
369
+ it("rejects 4294967296 (overflow uint32)", () => {
370
+ expect(uint32().safeParse(4294967296).success).toBe(false);
371
+ });
372
+ });
373
+
374
+ describe("uint64 boundaries", () => {
375
+ it("accepts Number.MAX_SAFE_INTEGER", () => {
376
+ expect(uint64().safeParse(Number.MAX_SAFE_INTEGER).success).toBe(true);
377
+ });
378
+
379
+ it("accepts 0 (min uint64)", () => {
380
+ expect(uint64().safeParse(0).success).toBe(true);
381
+ });
382
+
383
+ it("rejects -1 (underflow uint64)", () => {
384
+ expect(uint64().safeParse(-1).success).toBe(false);
385
+ });
386
+ });
387
+
388
+ describe("int64 / int boundaries", () => {
389
+ it("accepts Number.MAX_SAFE_INTEGER for int64", () => {
390
+ expect(int64().safeParse(Number.MAX_SAFE_INTEGER).success).toBe(true);
391
+ });
392
+
393
+ it("accepts Number.MIN_SAFE_INTEGER for int64", () => {
394
+ expect(int64().safeParse(Number.MIN_SAFE_INTEGER).success).toBe(true);
395
+ });
396
+
397
+ it("accepts Number.MAX_SAFE_INTEGER for int()", () => {
398
+ expect(int().safeParse(Number.MAX_SAFE_INTEGER).success).toBe(true);
399
+ });
400
+
401
+ it("accepts Number.MIN_SAFE_INTEGER for int()", () => {
402
+ expect(int().safeParse(Number.MIN_SAFE_INTEGER).success).toBe(true);
403
+ });
404
+ });
405
+
406
+ it("rejects floating point values for all integer types", () => {
407
+ expect(int8().safeParse(1.5).success).toBe(false);
408
+ expect(int16().safeParse(1.5).success).toBe(false);
409
+ expect(int32().safeParse(1.5).success).toBe(false);
410
+ expect(int64().safeParse(1.5).success).toBe(false);
411
+ expect(uint8().safeParse(1.5).success).toBe(false);
412
+ expect(uint16().safeParse(1.5).success).toBe(false);
413
+ expect(uint32().safeParse(1.5).success).toBe(false);
414
+ expect(uint64().safeParse(1.5).success).toBe(false);
415
+ expect(int().safeParse(1.5).success).toBe(false);
416
+ });
417
+ });
418
+
419
+ // ---------------------------------------------------------------------------
420
+ // 5. NaN/Infinity injection - CWE-20
421
+ // ---------------------------------------------------------------------------
422
+ describe("CWE-20 - NaN and Infinity injection", () => {
423
+ it("rejects NaN for number()", () => {
424
+ expect(number().safeParse(NaN).success).toBe(false);
425
+ });
426
+
427
+ it("rejects NaN for int()", () => {
428
+ expect(int().safeParse(NaN).success).toBe(false);
429
+ });
430
+
431
+ it("rejects NaN for float32()", () => {
432
+ expect(float32().safeParse(NaN).success).toBe(false);
433
+ });
434
+
435
+ it("rejects NaN for float64()", () => {
436
+ expect(float64().safeParse(NaN).success).toBe(false);
437
+ });
438
+
439
+ it("rejects Infinity for number()", () => {
440
+ expect(number().safeParse(Infinity).success).toBe(false);
441
+ });
442
+
443
+ it("rejects -Infinity for number()", () => {
444
+ expect(number().safeParse(-Infinity).success).toBe(false);
445
+ });
446
+
447
+ it("rejects Infinity for float32()", () => {
448
+ expect(float32().safeParse(Infinity).success).toBe(false);
449
+ });
450
+
451
+ it("rejects -Infinity for float32()", () => {
452
+ expect(float32().safeParse(-Infinity).success).toBe(false);
453
+ });
454
+
455
+ it("rejects Infinity for float64()", () => {
456
+ expect(float64().safeParse(Infinity).success).toBe(false);
457
+ });
458
+
459
+ it("rejects -Infinity for float64()", () => {
460
+ expect(float64().safeParse(-Infinity).success).toBe(false);
461
+ });
462
+
463
+ it("rejects Infinity for int()", () => {
464
+ expect(int().safeParse(Infinity).success).toBe(false);
465
+ });
466
+
467
+ it("rejects -Infinity for int()", () => {
468
+ expect(int().safeParse(-Infinity).success).toBe(false);
469
+ });
470
+
471
+ it("handles NaN !== NaN edge case - NaN is never equal to itself", () => {
472
+ // Ensure the library does not use === NaN (which always returns false)
473
+ const result = number().safeParse(NaN);
474
+ expect(result.success).toBe(false);
475
+ if (!result.success) {
476
+ // Should produce a meaningful error, not silently accept
477
+ expect(result.issues.length).toBeGreaterThan(0);
478
+ expect(result.issues[0].code).toBe("invalid_type");
479
+ }
480
+ });
481
+
482
+ it("rejects NaN for all integer types", () => {
483
+ expect(int8().safeParse(NaN).success).toBe(false);
484
+ expect(int16().safeParse(NaN).success).toBe(false);
485
+ expect(int32().safeParse(NaN).success).toBe(false);
486
+ expect(int64().safeParse(NaN).success).toBe(false);
487
+ expect(uint8().safeParse(NaN).success).toBe(false);
488
+ expect(uint16().safeParse(NaN).success).toBe(false);
489
+ expect(uint32().safeParse(NaN).success).toBe(false);
490
+ expect(uint64().safeParse(NaN).success).toBe(false);
491
+ });
492
+
493
+ it("rejects Infinity for all integer types", () => {
494
+ expect(int8().safeParse(Infinity).success).toBe(false);
495
+ expect(int16().safeParse(Infinity).success).toBe(false);
496
+ expect(int32().safeParse(Infinity).success).toBe(false);
497
+ expect(int64().safeParse(Infinity).success).toBe(false);
498
+ expect(uint8().safeParse(Infinity).success).toBe(false);
499
+ expect(uint16().safeParse(Infinity).success).toBe(false);
500
+ expect(uint32().safeParse(Infinity).success).toBe(false);
501
+ expect(uint64().safeParse(Infinity).success).toBe(false);
502
+ });
503
+ });
504
+
505
+ // ---------------------------------------------------------------------------
506
+ // 6. Format validation bypass - CWE-20
507
+ // ---------------------------------------------------------------------------
508
+ describe("CWE-20 - Format validation bypass", () => {
509
+ describe("email format", () => {
510
+ it("does not silently ignore a tampered email format name", () => {
511
+ const s = string().format("email\0" as any);
512
+ expect(s.safeParse("not-an-email").success).toBe(false);
513
+ });
514
+
515
+ // REVIEW: The test above proves the vulnerable email case, but it does not
516
+ // distinguish malformed built-ins from valid custom extension names.
517
+ it("rejects malformed format identifiers without blocking custom formats", () => {
518
+ expect(string().format("email\0" as any).safeParse("not-an-email").success).toBe(false);
519
+ expect(string().format("x-custom" as any).safeParse("any value").success).toBe(true);
520
+ });
521
+
522
+ it("does not import a tampered email format as an unconstrained string", () => {
523
+ const schema = importSchema({
524
+ anyvaliVersion: "1.0",
525
+ schemaVersion: "1.1",
526
+ root: { kind: "string", format: "email\0" as any },
527
+ definitions: {},
528
+ extensions: {},
529
+ });
530
+ expect(schema.safeParse("not-an-email").success).toBe(false);
531
+ });
532
+
533
+ // REVIEW: Imported schemas need the same malformed-format guard as the
534
+ // builder API, otherwise untrusted interchange can strip validation.
535
+ it("rejects imported malformed format identifiers without blocking custom formats", () => {
536
+ const malformed = importSchema({
537
+ anyvaliVersion: "1.0",
538
+ schemaVersion: "1.1",
539
+ root: { kind: "string", format: "email\0" as any },
540
+ definitions: {},
541
+ extensions: {},
542
+ });
543
+ const custom = importSchema({
544
+ anyvaliVersion: "1.0",
545
+ schemaVersion: "1.1",
546
+ root: { kind: "string", format: "x-custom" as any },
547
+ definitions: {},
548
+ extensions: {},
549
+ });
550
+ expect(malformed.safeParse("not-an-email").success).toBe(false);
551
+ expect(custom.safeParse("any value").success).toBe(true);
552
+ });
553
+
554
+ it("rejects null byte injection: user@example.com\\0.evil.com", () => {
555
+ const s = string().format("email");
556
+ const result = s.safeParse("user@example.com\0.evil.com");
557
+ expect(result.success).toBe(false);
558
+ });
559
+
560
+ it("rejects very long local part (>64 chars)", () => {
561
+ const s = string().format("email");
562
+ const longLocal = "a".repeat(65) + "@example.com";
563
+ const result = s.safeParse(longLocal);
564
+ // RFC 5321 limits local part to 64 characters; a strict validator should reject
565
+ // If the library does not enforce this, the test documents the behavior
566
+ expect(typeof result.success).toBe("boolean");
567
+ });
568
+
569
+ it("rejects email without domain", () => {
570
+ const s = string().format("email");
571
+ expect(s.safeParse("user@").success).toBe(false);
572
+ });
573
+
574
+ it("rejects email without local part", () => {
575
+ const s = string().format("email");
576
+ expect(s.safeParse("@example.com").success).toBe(false);
577
+ });
578
+ });
579
+
580
+ describe("url format", () => {
581
+ it("rejects javascript: protocol", () => {
582
+ const s = string().format("url");
583
+ expect(s.safeParse("javascript:alert(1)").success).toBe(false);
584
+ });
585
+
586
+ it("rejects data: protocol", () => {
587
+ const s = string().format("url");
588
+ expect(s.safeParse("data:text/html,<script>alert(1)</script>").success).toBe(false);
589
+ });
590
+
591
+ it("rejects file: protocol", () => {
592
+ const s = string().format("url");
593
+ expect(s.safeParse("file:///etc/passwd").success).toBe(false);
594
+ });
595
+
596
+ it("accepts valid https URL", () => {
597
+ const s = string().format("url");
598
+ expect(s.safeParse("https://example.com/path?q=1").success).toBe(true);
599
+ });
600
+
601
+ it("accepts valid http URL", () => {
602
+ const s = string().format("url");
603
+ expect(s.safeParse("http://example.com").success).toBe(true);
604
+ });
605
+ });
606
+
607
+ describe("ipv4 format", () => {
608
+ it("rejects octal notation 0177.0.0.1 (127.0.0.1 in octal)", () => {
609
+ const s = string().format("ipv4");
610
+ // Octal notation can be used to bypass IP filters
611
+ expect(s.safeParse("0177.0.0.1").success).toBe(false);
612
+ });
613
+
614
+ it("rejects overflow 256.1.1.1", () => {
615
+ const s = string().format("ipv4");
616
+ expect(s.safeParse("256.1.1.1").success).toBe(false);
617
+ });
618
+
619
+ it("rejects overflow 999.999.999.999", () => {
620
+ const s = string().format("ipv4");
621
+ expect(s.safeParse("999.999.999.999").success).toBe(false);
622
+ });
623
+
624
+ it("accepts valid IPv4", () => {
625
+ const s = string().format("ipv4");
626
+ expect(s.safeParse("192.168.1.1").success).toBe(true);
627
+ });
628
+
629
+ it("accepts 0.0.0.0", () => {
630
+ const s = string().format("ipv4");
631
+ expect(s.safeParse("0.0.0.0").success).toBe(true);
632
+ });
633
+
634
+ it("accepts 255.255.255.255", () => {
635
+ const s = string().format("ipv4");
636
+ expect(s.safeParse("255.255.255.255").success).toBe(true);
637
+ });
638
+
639
+ it("rejects leading zeros in octets (010.0.0.1)", () => {
640
+ const s = string().format("ipv4");
641
+ // Leading zeros can indicate octal interpretation
642
+ const result = s.safeParse("010.0.0.1");
643
+ // Strict validators should reject; documents actual behavior
644
+ expect(typeof result.success).toBe("boolean");
645
+ });
646
+ });
647
+
648
+ describe("ipv6 format", () => {
649
+ it("handles IPv4-mapped IPv6 address ::ffff:127.0.0.1", () => {
650
+ const s = string().format("ipv6");
651
+ // IPv4-mapped addresses may bypass IPv4-only filters
652
+ const result = s.safeParse("::ffff:127.0.0.1");
653
+ // This is technically valid IPv6 but may not match the regex;
654
+ // the test documents the library's behavior
655
+ expect(typeof result.success).toBe("boolean");
656
+ });
657
+
658
+ it("accepts valid IPv6 loopback ::1", () => {
659
+ const s = string().format("ipv6");
660
+ expect(s.safeParse("::1").success).toBe(true);
661
+ });
662
+
663
+ it("accepts valid full IPv6", () => {
664
+ const s = string().format("ipv6");
665
+ expect(
666
+ s.safeParse("2001:0db8:85a3:0000:0000:8a2e:0370:7334").success
667
+ ).toBe(true);
668
+ });
669
+ });
670
+ });
671
+
672
+ // ---------------------------------------------------------------------------
673
+ // 6b. Unicode length bypass / portability mismatch
674
+ // ---------------------------------------------------------------------------
675
+ describe("Unicode length constraints", () => {
676
+ it("counts astral Unicode code points as one character", () => {
677
+ const emoji = "😀";
678
+ expect(string().maxLength(1).safeParse(emoji).success).toBe(true);
679
+ expect(string().minLength(2).safeParse(emoji).success).toBe(false);
680
+ });
681
+
682
+ // REVIEW: The test above covers one surrogate pair. This companion case
683
+ // catches mixed BMP plus astral strings where UTF-16 length diverges more subtly.
684
+ it("counts mixed BMP and astral code points for min and max length", () => {
685
+ const value = "a😀";
686
+ expect(string().minLength(2).maxLength(2).safeParse(value).success).toBe(true);
687
+ expect(string().maxLength(1).safeParse(value).success).toBe(false);
688
+ });
689
+
690
+ it("uses code point length for imported maxLength schemas", () => {
691
+ const schema = importSchema({
692
+ anyvaliVersion: "1.0",
693
+ schemaVersion: "1.1",
694
+ root: { kind: "string", maxLength: 1 },
695
+ definitions: {},
696
+ extensions: {},
697
+ });
698
+ expect(schema.safeParse("😀").success).toBe(true);
699
+ });
700
+ });
701
+
702
+ // ---------------------------------------------------------------------------
703
+ // 7. Large input DoS - CWE-400
704
+ // ---------------------------------------------------------------------------
705
+ describe("CWE-400 - Large input denial of service", () => {
706
+ it("handles a 1MB string without crashing", () => {
707
+ const s = string();
708
+ const bigString = "x".repeat(1_000_000);
709
+ const result = s.safeParse(bigString);
710
+ expect(result.success).toBe(true);
711
+ if (result.success) {
712
+ expect(result.data.length).toBe(1_000_000);
713
+ }
714
+ });
715
+
716
+ it("validates a 1MB string with maxLength constraint", () => {
717
+ const s = string().maxLength(500_000);
718
+ const bigString = "x".repeat(1_000_000);
719
+ const result = s.safeParse(bigString);
720
+ expect(result.success).toBe(false);
721
+ });
722
+
723
+ it("handles deeply nested objects (100 levels) without crashing", () => {
724
+ // Build schema
725
+ let schema: ReturnType<typeof object> = object({ value: string() });
726
+ for (let i = 0; i < 100; i++) {
727
+ schema = object({ nested: schema });
728
+ }
729
+
730
+ // Build matching input
731
+ let input: Record<string, unknown> = { value: "deep" };
732
+ for (let i = 0; i < 100; i++) {
733
+ input = { nested: input };
734
+ }
735
+
736
+ const result = schema.safeParse(input);
737
+ expect(result.success).toBe(true);
738
+ });
739
+
740
+ it("handles deeply nested objects that fail validation with full error paths", () => {
741
+ let schema: ReturnType<typeof object> = object({ value: int() });
742
+ for (let i = 0; i < 50; i++) {
743
+ schema = object({ nested: schema });
744
+ }
745
+
746
+ // Input with wrong type at the deepest level
747
+ let input: Record<string, unknown> = { value: "not-an-int" };
748
+ for (let i = 0; i < 50; i++) {
749
+ input = { nested: input };
750
+ }
751
+
752
+ const result = schema.safeParse(input);
753
+ expect(result.success).toBe(false);
754
+ if (!result.success) {
755
+ // Path should have 51 segments: "nested" x50 + "value"
756
+ expect(result.issues[0].path.length).toBe(51);
757
+ }
758
+ });
759
+
760
+ it("handles an array with 10000 items without crashing", () => {
761
+ const s = array(int());
762
+ const bigArray = Array.from({ length: 10_000 }, (_, i) => i);
763
+ const result = s.safeParse(bigArray);
764
+ expect(result.success).toBe(true);
765
+ if (result.success) {
766
+ expect(result.data.length).toBe(10_000);
767
+ }
768
+ });
769
+
770
+ it("handles an array with 10000 items that all fail validation", () => {
771
+ const s = array(int());
772
+ const bigArray = Array.from({ length: 10_000 }, () => "not-a-number");
773
+ const result = s.safeParse(bigArray);
774
+ expect(result.success).toBe(false);
775
+ if (!result.success) {
776
+ expect(result.issues.length).toBe(10_000);
777
+ }
778
+ });
779
+
780
+ it("handles object with many properties", () => {
781
+ const shape: Record<string, ReturnType<typeof string>> = {};
782
+ for (let i = 0; i < 1000; i++) {
783
+ shape[`prop_${i}`] = string();
784
+ }
785
+ const s = object(shape);
786
+
787
+ const input: Record<string, string> = {};
788
+ for (let i = 0; i < 1000; i++) {
789
+ input[`prop_${i}`] = `value_${i}`;
790
+ }
791
+
792
+ const result = s.safeParse(input);
793
+ expect(result.success).toBe(true);
794
+ });
795
+ });
796
+
797
+ // ---------------------------------------------------------------------------
798
+ // 8. JSON import injection
799
+ // ---------------------------------------------------------------------------
800
+ describe("JSON import injection", () => {
801
+ it("rejects schema with unknown kind", () => {
802
+ const doc = {
803
+ anyvaliVersion: "1.0",
804
+ schemaVersion: "1.1",
805
+ root: { kind: "evil_custom_type" },
806
+ definitions: {},
807
+ extensions: {},
808
+ };
809
+
810
+ expect(() => importSchema(doc as any)).toThrow();
811
+ });
812
+
813
+ it("rejects schema with missing kind field", () => {
814
+ const doc = {
815
+ anyvaliVersion: "1.0",
816
+ schemaVersion: "1.1",
817
+ root: {},
818
+ definitions: {},
819
+ extensions: {},
820
+ };
821
+
822
+ expect(() => importSchema(doc as any)).toThrow();
823
+ });
824
+
825
+ it("rejects schema with null root", () => {
826
+ expect(() =>
827
+ importSchema({
828
+ anyvaliVersion: "1.0",
829
+ schemaVersion: "1.1",
830
+ root: null,
831
+ } as any)
832
+ ).toThrow();
833
+ });
834
+
835
+ it("rejects schema with undefined root", () => {
836
+ expect(() =>
837
+ importSchema({
838
+ anyvaliVersion: "1.0",
839
+ schemaVersion: "1.1",
840
+ } as any)
841
+ ).toThrow();
842
+ });
843
+
844
+ it("handles __proto__ in definition names without prototype pollution", () => {
845
+ const doc = JSON.parse(`{
846
+ "anyvaliVersion": "1.0",
847
+ "schemaVersion": "1.1",
848
+ "root": { "kind": "ref", "ref": "#/definitions/__proto__" },
849
+ "definitions": {
850
+ "__proto__": { "kind": "string" }
851
+ },
852
+ "extensions": {}
853
+ }`);
854
+
855
+ // Should either resolve the ref and work, or throw a clear error
856
+ let result: unknown;
857
+ let threw = false;
858
+ try {
859
+ const schema = importSchema(doc as any);
860
+ result = schema.safeParse("test");
861
+ } catch {
862
+ threw = true;
863
+ }
864
+
865
+ if (!threw) {
866
+ // If it resolved, the prototype should not be polluted
867
+ expect(({} as Record<string, unknown>).__proto__).toBe(Object.prototype);
868
+ }
869
+ // Either outcome is acceptable as long as no pollution occurs
870
+ expect(({} as Record<string, unknown>).polluted).toBeUndefined();
871
+ });
872
+
873
+ it("rejects schema with kind set to 'constructor'", () => {
874
+ const doc = {
875
+ anyvaliVersion: "1.0",
876
+ schemaVersion: "1.1",
877
+ root: { kind: "constructor" },
878
+ definitions: {},
879
+ extensions: {},
880
+ };
881
+
882
+ expect(() => importSchema(doc as any)).toThrow();
883
+ });
884
+
885
+ it("rejects schema with kind set to 'toString'", () => {
886
+ const doc = {
887
+ anyvaliVersion: "1.0",
888
+ schemaVersion: "1.1",
889
+ root: { kind: "toString" },
890
+ definitions: {},
891
+ extensions: {},
892
+ };
893
+
894
+ expect(() => importSchema(doc as any)).toThrow();
895
+ });
896
+
897
+ it("handles ref to nonexistent definition gracefully", () => {
898
+ const doc = {
899
+ anyvaliVersion: "1.0",
900
+ schemaVersion: "1.1",
901
+ root: { kind: "ref" as const, ref: "#/definitions/DoesNotExist" },
902
+ definitions: {},
903
+ extensions: {},
904
+ };
905
+
906
+ const schema = importSchema(doc as any);
907
+ // The ref should fail at parse time, not import time (lazy resolution)
908
+ let threw = false;
909
+ try {
910
+ schema.safeParse("anything");
911
+ } catch {
912
+ threw = true;
913
+ }
914
+ // Should throw because the definition doesn't exist
915
+ expect(threw).toBe(true);
916
+ });
917
+
918
+ it("imports valid schema after rejecting invalid ones (no state leakage)", () => {
919
+ // First, try an invalid import
920
+ try {
921
+ importSchema({
922
+ anyvaliVersion: "1.0",
923
+ schemaVersion: "1.1",
924
+ root: { kind: "unknown_kind" },
925
+ definitions: {},
926
+ extensions: {},
927
+ } as any);
928
+ } catch {
929
+ // expected
930
+ }
931
+
932
+ // Now a valid import should still work
933
+ const validDoc = {
934
+ anyvaliVersion: "1.0",
935
+ schemaVersion: "1.1",
936
+ root: { kind: "string" as const },
937
+ definitions: {},
938
+ extensions: {},
939
+ };
940
+
941
+ const schema = importSchema(validDoc as any);
942
+ const result = schema.safeParse("hello");
943
+ expect(result.success).toBe(true);
944
+ });
945
+ });