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