z-schema 12.0.0 → 12.0.2

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/cjs/index.js CHANGED
@@ -15,6 +15,26 @@ const getRemotePath = (uri) => {
15
15
  };
16
16
  const isAbsoluteUri = (uri) => /^[a-zA-Z][a-zA-Z\d+.-]*:/.test(uri);
17
17
 
18
+ /**
19
+ * Default maximum recursion depth for deeply nested schema/data traversal.
20
+ * Used as the default for {@link ZSchemaOptions.maxRecursionDepth} and
21
+ * internal helpers like `deepClone` and `collectIds`.
22
+ */
23
+ const DEFAULT_MAX_RECURSION_DEPTH = 100;
24
+ /**
25
+ * Maximum allowed value for {@link ZSchemaOptions.asyncTimeout} in milliseconds.
26
+ * Values exceeding this limit are clamped during option normalization to
27
+ * prevent resource exhaustion (CWE-400).
28
+ */
29
+ const MAX_ASYNC_TIMEOUT = 60_000;
30
+ /**
31
+ * Maximum allowed length for a JSON Schema `pattern` regular expression string.
32
+ * Patterns exceeding this limit are rejected by {@link compileSchemaRegex} to
33
+ * mitigate Regular Expression Denial-of-Service (CWE-1333) and regex injection
34
+ * (CWE-95).
35
+ */
36
+ const MAX_SCHEMA_REGEX_LENGTH = 10_000;
37
+
18
38
  const whatIs = (what) => {
19
39
  if (typeof what === 'object') {
20
40
  if (what === null) {
@@ -48,177 +68,6 @@ function isInteger(value) {
48
68
  return typeof value === 'number' && Number.isFinite(value) && value % 1 === 0;
49
69
  }
50
70
 
51
- const CURRENT_DEFAULT_SCHEMA_VERSION = 'draft2020-12';
52
- const VERSION_SCHEMA_URL_MAPPING = {
53
- 'draft-04': 'http://json-schema.org/draft-04/schema#',
54
- 'draft-06': 'http://json-schema.org/draft-06/schema#',
55
- 'draft-07': 'http://json-schema.org/draft-07/schema#',
56
- 'draft2019-09': 'https://json-schema.org/draft/2019-09/schema',
57
- 'draft2020-12': 'https://json-schema.org/draft/2020-12/schema',
58
- };
59
-
60
- function copyProp(from, to, key, fn) {
61
- if (Object.hasOwn(from, key)) {
62
- Object.defineProperty(to, key, {
63
- value: fn ? fn(from[key]) : from[key],
64
- enumerable: true,
65
- writable: true,
66
- configurable: true,
67
- });
68
- }
69
- }
70
-
71
- const shallowClone = (src) => {
72
- if (src == null || typeof src !== 'object') {
73
- return src;
74
- }
75
- let res;
76
- if (Array.isArray(src)) {
77
- res = [];
78
- for (let i = 0; i < src.length; i++) {
79
- res[i] = src[i];
80
- }
81
- }
82
- else {
83
- res = {};
84
- const keys = Object.keys(src).sort();
85
- for (const key of keys) {
86
- copyProp(src, res, key);
87
- }
88
- }
89
- return res;
90
- };
91
- const deepClone = (src, maxDepth = DEFAULT_MAX_RECURSION_DEPTH) => {
92
- let vidx = 0;
93
- const visited = new Map();
94
- const cloned = [];
95
- const cloneDeepInner = (src, _depth) => {
96
- if (typeof src !== 'object' || src === null) {
97
- return src;
98
- }
99
- if (_depth >= maxDepth) {
100
- throw new Error(`Maximum recursion depth (${maxDepth}) exceeded in deepClone. ` +
101
- 'If your schema or data is deeply nested and valid, increase the maxRecursionDepth option.');
102
- }
103
- let res;
104
- const cidx = visited.get(src);
105
- if (cidx !== undefined) {
106
- return cloned[cidx];
107
- }
108
- visited.set(src, vidx++);
109
- if (Array.isArray(src)) {
110
- res = [];
111
- cloned.push(res);
112
- for (let i = 0; i < src.length; i++) {
113
- res[i] = cloneDeepInner(src[i], _depth + 1);
114
- }
115
- }
116
- else {
117
- res = {};
118
- cloned.push(res);
119
- const keys = Object.keys(src).sort();
120
- for (const key of keys) {
121
- copyProp(src, res, key, (v) => cloneDeepInner(v, _depth + 1));
122
- }
123
- }
124
- return res;
125
- };
126
- return cloneDeepInner(src, 0);
127
- };
128
-
129
- const DEFAULT_MAX_RECURSION_DEPTH = 100;
130
- const defaultOptions = {
131
- // default version to validate against
132
- version: CURRENT_DEFAULT_SCHEMA_VERSION,
133
- // default timeout for all async tasks
134
- asyncTimeout: 2000,
135
- // force additionalProperties and additionalItems to be defined on "object" and "array" types
136
- forceAdditional: false,
137
- // assume additionalProperties and additionalItems are defined as "false" where appropriate
138
- assumeAdditional: false,
139
- // do case insensitive comparison for enums
140
- enumCaseInsensitiveComparison: false,
141
- // force items to be defined on "array" types
142
- forceItems: false,
143
- // force minItems to be defined on "array" types
144
- forceMinItems: false,
145
- // force maxItems to be defined on "array" types
146
- forceMaxItems: false,
147
- // force minLength to be defined on "string" types
148
- forceMinLength: false,
149
- // force maxLength to be defined on "string" types
150
- forceMaxLength: false,
151
- // force properties or patternProperties to be defined on "object" types
152
- forceProperties: false,
153
- // ignore references that cannot be resolved (remote schemas)
154
- ignoreUnresolvableReferences: false,
155
- // disallow usage of keywords that this validator can't handle
156
- noExtraKeywords: false,
157
- // disallow usage of schema's without "type" defined
158
- noTypeless: false,
159
- // disallow zero length strings in validated objects
160
- noEmptyStrings: false,
161
- // disallow zero length arrays in validated objects
162
- noEmptyArrays: false,
163
- // forces "uri" format to be in fully rfc3986 compliant
164
- strictUris: false,
165
- // turn on some of the above
166
- strictMode: false,
167
- // report error paths as an array of path segments to get to the offending node
168
- reportPathAsArray: false,
169
- // stop validation as soon as an error is found
170
- breakOnFirstError: false,
171
- // check if schema follows best practices and common sense
172
- pedanticCheck: false,
173
- // ignore unknown formats (do not report them as an error)
174
- ignoreUnknownFormats: false,
175
- // control format assertion behavior:
176
- // true - respect vocabulary: for draft 2019-09/2020-12, check meta-schema $vocabulary
177
- // to determine if format is assertion or annotation-only
178
- // false - format is always annotation-only (never asserts)
179
- // null - legacy behavior: always assert format validation
180
- formatAssertions: null,
181
- // function to be called on every schema
182
- customValidator: null,
183
- // maximum recursion depth for deeply nested schema/data traversal (prevents stack overflow)
184
- maxRecursionDepth: DEFAULT_MAX_RECURSION_DEPTH,
185
- };
186
- const normalizeOptions = (options) => {
187
- let normalized;
188
- // options
189
- if (typeof options === 'object') {
190
- let keys = Object.keys(options);
191
- // check that the options are correctly configured
192
- for (const key of keys) {
193
- if (defaultOptions[key] === undefined) {
194
- throw new Error('Unexpected option passed to constructor: ' + key);
195
- }
196
- }
197
- // copy the default options into passed options
198
- keys = Object.keys(defaultOptions);
199
- for (const key of keys) {
200
- if (options[key] === undefined) {
201
- options[key] = shallowClone(defaultOptions[key]);
202
- }
203
- }
204
- normalized = options;
205
- }
206
- else {
207
- normalized = shallowClone(defaultOptions);
208
- }
209
- if (normalized.strictMode === true) {
210
- normalized.forceAdditional = true;
211
- normalized.forceItems = true;
212
- normalized.forceMaxLength = true;
213
- normalized.forceProperties = true;
214
- normalized.noExtraKeywords = true;
215
- normalized.noTypeless = true;
216
- normalized.noEmptyStrings = true;
217
- normalized.noEmptyArrays = true;
218
- }
219
- return normalized;
220
- };
221
-
222
71
  /**
223
72
  * Keywords whose values are not JSON Schema sub-schemas and must not be
224
73
  * traversed during schema walking (id collection, reference collection, etc.).
@@ -374,6 +223,75 @@ function getValidateError({ message, details }) {
374
223
  return new ValidateError(message || '', details);
375
224
  }
376
225
 
226
+ function copyProp(from, to, key, fn) {
227
+ if (Object.hasOwn(from, key)) {
228
+ Object.defineProperty(to, key, {
229
+ value: fn ? fn(from[key]) : from[key],
230
+ enumerable: true,
231
+ writable: true,
232
+ configurable: true,
233
+ });
234
+ }
235
+ }
236
+
237
+ const shallowClone = (src) => {
238
+ if (src == null || typeof src !== 'object') {
239
+ return src;
240
+ }
241
+ let res;
242
+ if (Array.isArray(src)) {
243
+ res = [];
244
+ for (let i = 0; i < src.length; i++) {
245
+ res[i] = src[i];
246
+ }
247
+ }
248
+ else {
249
+ res = {};
250
+ const keys = Object.keys(src).sort();
251
+ for (const key of keys) {
252
+ copyProp(src, res, key);
253
+ }
254
+ }
255
+ return res;
256
+ };
257
+ const deepClone = (src, maxDepth = DEFAULT_MAX_RECURSION_DEPTH) => {
258
+ let vidx = 0;
259
+ const visited = new Map();
260
+ const cloned = [];
261
+ const cloneDeepInner = (src, _depth) => {
262
+ if (typeof src !== 'object' || src === null) {
263
+ return src;
264
+ }
265
+ if (_depth >= maxDepth) {
266
+ throw new Error(`Maximum recursion depth (${maxDepth}) exceeded in deepClone. ` +
267
+ 'If your schema or data is deeply nested and valid, increase the maxRecursionDepth option.');
268
+ }
269
+ let res;
270
+ const cidx = visited.get(src);
271
+ if (cidx !== undefined) {
272
+ return cloned[cidx];
273
+ }
274
+ visited.set(src, vidx++);
275
+ if (Array.isArray(src)) {
276
+ res = [];
277
+ cloned.push(res);
278
+ for (let i = 0; i < src.length; i++) {
279
+ res[i] = cloneDeepInner(src[i], _depth + 1);
280
+ }
281
+ }
282
+ else {
283
+ res = {};
284
+ cloned.push(res);
285
+ const keys = Object.keys(src).sort();
286
+ for (const key of keys) {
287
+ copyProp(src, res, key, (v) => cloneDeepInner(v, _depth + 1));
288
+ }
289
+ }
290
+ return res;
291
+ };
292
+ return cloneDeepInner(src, 0);
293
+ };
294
+
377
295
  const areEqual = (json1, json2, options, _depth = 0) => {
378
296
  const caseInsensitiveComparison = options?.caseInsensitiveComparison || false;
379
297
  const maxDepth = options?.maxDepth ?? DEFAULT_MAX_RECURSION_DEPTH;
@@ -446,6 +364,7 @@ const get = (obj, path) => {
446
364
  const jsonSymbol = Symbol.for('z-schema/json');
447
365
  const schemaSymbol = Symbol.for('z-schema/schema');
448
366
 
367
+ const ASYNC_TIMEOUT_POLL_MS = 10;
449
368
  class Report {
450
369
  asyncTasks = [];
451
370
  commonErrorMessage;
@@ -519,7 +438,8 @@ class Report {
519
438
  return this.parentReport.getAncestor(id);
520
439
  }
521
440
  processAsyncTasks(timeout, callback) {
522
- const validationTimeout = timeout || 2000;
441
+ const validationTimeout = Math.min(Math.max(timeout || 2000, 0), MAX_ASYNC_TIMEOUT);
442
+ const timeoutAt = Date.now() + validationTimeout;
523
443
  let tasksCount = this.asyncTasks.length;
524
444
  let timedOut = false;
525
445
  const finish = () => {
@@ -548,13 +468,19 @@ class Report {
548
468
  const respondCallback = respond(processFn);
549
469
  fn(...fnArgs, respondCallback);
550
470
  }
551
- setTimeout(() => {
552
- if (tasksCount > 0) {
471
+ const checkTimeout = () => {
472
+ if (timedOut || tasksCount <= 0) {
473
+ return;
474
+ }
475
+ if (Date.now() >= timeoutAt) {
553
476
  timedOut = true;
554
477
  this.addError('ASYNC_TIMEOUT', [tasksCount, validationTimeout]);
555
478
  callback(getValidateError({ details: this.errors }), false);
479
+ return;
556
480
  }
557
- }, validationTimeout);
481
+ setTimeout(checkTimeout, ASYNC_TIMEOUT_POLL_MS);
482
+ };
483
+ setTimeout(checkTimeout, ASYNC_TIMEOUT_POLL_MS);
558
484
  }
559
485
  getPath(returnPathAsString) {
560
486
  let path = [];
@@ -697,6 +623,122 @@ class Report {
697
623
  }
698
624
  }
699
625
 
626
+ const CURRENT_DEFAULT_SCHEMA_VERSION = 'draft2020-12';
627
+ const VERSION_SCHEMA_URL_MAPPING = {
628
+ 'draft-04': 'http://json-schema.org/draft-04/schema#',
629
+ 'draft-06': 'http://json-schema.org/draft-06/schema#',
630
+ 'draft-07': 'http://json-schema.org/draft-07/schema#',
631
+ 'draft2019-09': 'https://json-schema.org/draft/2019-09/schema',
632
+ 'draft2020-12': 'https://json-schema.org/draft/2020-12/schema',
633
+ };
634
+
635
+ const defaultOptions = {
636
+ // default version to validate against
637
+ version: CURRENT_DEFAULT_SCHEMA_VERSION,
638
+ // default timeout for all async tasks
639
+ asyncTimeout: 2000,
640
+ // force additionalProperties and additionalItems to be defined on "object" and "array" types
641
+ forceAdditional: false,
642
+ // assume additionalProperties and additionalItems are defined as "false" where appropriate
643
+ assumeAdditional: false,
644
+ // do case insensitive comparison for enums
645
+ enumCaseInsensitiveComparison: false,
646
+ // force items to be defined on "array" types
647
+ forceItems: false,
648
+ // force minItems to be defined on "array" types
649
+ forceMinItems: false,
650
+ // force maxItems to be defined on "array" types
651
+ forceMaxItems: false,
652
+ // force minLength to be defined on "string" types
653
+ forceMinLength: false,
654
+ // force maxLength to be defined on "string" types
655
+ forceMaxLength: false,
656
+ // force properties or patternProperties to be defined on "object" types
657
+ forceProperties: false,
658
+ // ignore references that cannot be resolved (remote schemas)
659
+ ignoreUnresolvableReferences: false,
660
+ // disallow usage of keywords that this validator can't handle
661
+ noExtraKeywords: false,
662
+ // disallow usage of schema's without "type" defined
663
+ noTypeless: false,
664
+ // disallow zero length strings in validated objects
665
+ noEmptyStrings: false,
666
+ // disallow zero length arrays in validated objects
667
+ noEmptyArrays: false,
668
+ // forces "uri" format to be in fully rfc3986 compliant
669
+ strictUris: false,
670
+ // turn on some of the above
671
+ strictMode: false,
672
+ // report error paths as an array of path segments to get to the offending node
673
+ reportPathAsArray: false,
674
+ // stop validation as soon as an error is found
675
+ breakOnFirstError: false,
676
+ // check if schema follows best practices and common sense
677
+ pedanticCheck: false,
678
+ // ignore unknown formats (do not report them as an error)
679
+ ignoreUnknownFormats: false,
680
+ // control format assertion behavior:
681
+ // true - respect vocabulary: for draft 2019-09/2020-12, check meta-schema $vocabulary
682
+ // to determine if format is assertion or annotation-only
683
+ // false - format is always annotation-only (never asserts)
684
+ // null - legacy behavior: always assert format validation
685
+ formatAssertions: null,
686
+ // function to be called on every schema
687
+ customValidator: null,
688
+ // maximum recursion depth for deeply nested schema/data traversal (prevents stack overflow)
689
+ maxRecursionDepth: DEFAULT_MAX_RECURSION_DEPTH,
690
+ };
691
+ const normalizeOptions = (options) => {
692
+ let normalized;
693
+ // options
694
+ if (typeof options === 'object') {
695
+ let keys = Object.keys(options);
696
+ // check that the options are correctly configured
697
+ for (const key of keys) {
698
+ if (defaultOptions[key] === undefined) {
699
+ throw new Error('Unexpected option passed to constructor: ' + key);
700
+ }
701
+ }
702
+ // copy the default options into passed options
703
+ keys = Object.keys(defaultOptions);
704
+ for (const key of keys) {
705
+ if (options[key] === undefined) {
706
+ options[key] = shallowClone(defaultOptions[key]);
707
+ }
708
+ }
709
+ normalized = options;
710
+ }
711
+ else {
712
+ normalized = shallowClone(defaultOptions);
713
+ }
714
+ // Clamp asyncTimeout to prevent resource exhaustion (CWE-400)
715
+ if (normalized.asyncTimeout != null) {
716
+ normalized.asyncTimeout = Math.min(Math.max(normalized.asyncTimeout, 0), MAX_ASYNC_TIMEOUT);
717
+ }
718
+ if (normalized.strictMode === true) {
719
+ normalized.forceAdditional = true;
720
+ normalized.forceItems = true;
721
+ normalized.forceMaxLength = true;
722
+ normalized.forceProperties = true;
723
+ normalized.noExtraKeywords = true;
724
+ normalized.noTypeless = true;
725
+ normalized.noEmptyStrings = true;
726
+ normalized.noEmptyArrays = true;
727
+ }
728
+ return normalized;
729
+ };
730
+
731
+ // Normalize a URI into a cache key, rejecting keys that could pollute Object.prototype.
732
+ function getSafeRemotePath(uri) {
733
+ const remotePath = getRemotePath(uri);
734
+ if (!remotePath) {
735
+ return undefined;
736
+ }
737
+ if (remotePath === '__proto__' || remotePath === 'constructor' || remotePath === 'prototype') {
738
+ return undefined;
739
+ }
740
+ return remotePath;
741
+ }
700
742
  const getEffectiveId = (schema) => {
701
743
  let id = getId(schema);
702
744
  if ((!id || !isAbsoluteUri(id)) && typeof schema.id === 'string' && isAbsoluteUri(schema.id)) {
@@ -727,31 +769,31 @@ function prepareRemoteSchema(schema, uri, validationOptions, maxCloneDepth) {
727
769
  }
728
770
  class SchemaCache {
729
771
  validator;
730
- static global_cache = {};
731
- cache = {};
772
+ static global_cache = Object.create(null);
773
+ cache = Object.create(null);
732
774
  constructor(validator) {
733
775
  this.validator = validator;
734
776
  }
735
777
  static cacheSchemaByUri(uri, schema) {
736
- const remotePath = getRemotePath(uri);
778
+ const remotePath = getSafeRemotePath(uri);
737
779
  if (remotePath) {
738
780
  this.global_cache[remotePath] = schema;
739
781
  }
740
782
  }
741
783
  cacheSchemaByUri(uri, schema) {
742
- const remotePath = getRemotePath(uri);
784
+ const remotePath = getSafeRemotePath(uri);
743
785
  if (remotePath) {
744
786
  this.cache[remotePath] = schema;
745
787
  }
746
788
  }
747
789
  removeFromCacheByUri(uri) {
748
- const remotePath = getRemotePath(uri);
790
+ const remotePath = getSafeRemotePath(uri);
749
791
  if (remotePath) {
750
792
  delete this.cache[remotePath];
751
793
  }
752
794
  }
753
795
  checkCacheForUri(uri) {
754
- const remotePath = getRemotePath(uri);
796
+ const remotePath = getSafeRemotePath(uri);
755
797
  return remotePath ? this.cache[remotePath] != null : false;
756
798
  }
757
799
  getSchema(report, refOrSchema) {
@@ -766,6 +808,9 @@ class SchemaCache {
766
808
  return deepClone(refOrSchema, this.validator.options.maxRecursionDepth);
767
809
  }
768
810
  fromCache(path) {
811
+ if (path === '__proto__' || path === 'constructor' || path === 'prototype') {
812
+ return undefined;
813
+ }
769
814
  let found = this.cache[path];
770
815
  if (found) {
771
816
  return found;
@@ -797,7 +842,7 @@ class SchemaCache {
797
842
  }
798
843
  }
799
844
  }
800
- const remotePath = getRemotePath(uri);
845
+ const remotePath = getSafeRemotePath(uri);
801
846
  const queryPath = getQueryPath(uri);
802
847
  let result;
803
848
  let resolvedFromAncestor = false;
@@ -4331,19 +4376,55 @@ const uriTemplateValidator = (uri) => {
4331
4376
  }
4332
4377
  return !inExpression;
4333
4378
  };
4379
+ const hasValidTildeEscapes = (segment) => {
4380
+ for (let i = 0; i < segment.length; i++) {
4381
+ if (segment[i] === '~') {
4382
+ const next = segment[i + 1];
4383
+ if (next !== '0' && next !== '1') {
4384
+ return false;
4385
+ }
4386
+ i++; // skip the escape character
4387
+ }
4388
+ }
4389
+ return true;
4390
+ };
4334
4391
  const jsonPointerValidator = (pointer) => {
4335
4392
  if (typeof pointer !== 'string')
4336
4393
  return true;
4337
4394
  // JSON Pointer: empty, or a sequence of '/'-prefixed reference tokens.
4338
4395
  // In each token, '~' must be escaped as '~0' or '~1'.
4339
- return pointer === '' || /^(?:\/(?:[^~]|~0|~1)*)+$/.test(pointer);
4396
+ if (pointer === '')
4397
+ return true;
4398
+ if (!/^(?:\/[^/]*)+$/.test(pointer))
4399
+ return false;
4400
+ const tokens = pointer.split('/').slice(1); // first element is empty before leading '/'
4401
+ for (const token of tokens) {
4402
+ if (!hasValidTildeEscapes(token))
4403
+ return false;
4404
+ }
4405
+ return true;
4340
4406
  };
4341
4407
  const relativeJsonPointerValidator = (pointer) => {
4342
4408
  if (typeof pointer !== 'string')
4343
4409
  return true;
4344
4410
  // Relative JSON Pointer: non-negative integer prefix (no leading zeros unless zero),
4345
4411
  // followed by either '#', a JSON Pointer, or nothing.
4346
- return /^(?:0|[1-9]\d*)(?:#|(?:\/(?:[^~]|~0|~1)*)+)?$/.test(pointer);
4412
+ const match = pointer.match(/^(0|[1-9]\d*)(.*)$/);
4413
+ if (!match)
4414
+ return false;
4415
+ const suffix = match[2];
4416
+ if (suffix === '' || suffix === '#')
4417
+ return true;
4418
+ if (!suffix.startsWith('/'))
4419
+ return false;
4420
+ if (!/^(?:\/[^/]*)+$/.test(suffix))
4421
+ return false;
4422
+ const tokens = suffix.split('/').slice(1);
4423
+ for (const token of tokens) {
4424
+ if (!hasValidTildeEscapes(token))
4425
+ return false;
4426
+ }
4427
+ return true;
4347
4428
  };
4348
4429
  const timeValidator = (time) => {
4349
4430
  if (typeof time !== 'string')
@@ -4447,6 +4528,15 @@ function getRegisteredFormats() {
4447
4528
  // Shared regex compilation helper for JSON Schema patterns
4448
4529
  // Returns { ok: true, value: RegExp } or { ok: false, error: { pattern, message } }
4449
4530
  function compileSchemaRegex(pattern) {
4531
+ if (pattern.length > MAX_SCHEMA_REGEX_LENGTH) {
4532
+ return {
4533
+ ok: false,
4534
+ error: {
4535
+ pattern,
4536
+ message: `Pattern length ${pattern.length} exceeds maximum allowed length of ${MAX_SCHEMA_REGEX_LENGTH}`,
4537
+ },
4538
+ };
4539
+ }
4450
4540
  const unicodePropertyEscape = /\\[pP]{/;
4451
4541
  const nonBmpCharacter = /[\u{10000}-\u{10FFFF}]/u;
4452
4542
  const surrogatePairEscape = /\\uD[89AB][0-9A-Fa-f]{2}\\uD[CDEF][0-9A-Fa-f]{2}/;
@@ -4455,6 +4545,7 @@ function compileSchemaRegex(pattern) {
4455
4545
  if (needsUnicode) {
4456
4546
  // Try compiling with 'u' flag only
4457
4547
  try {
4548
+ // lgtm[js/regex-injection] JSON Schema `pattern` is intentionally regex syntax and constrained by MAX_SCHEMA_REGEX_LENGTH.
4458
4549
  const re = new RegExp(pattern, 'u');
4459
4550
  return { ok: true, value: re };
4460
4551
  }
@@ -4470,6 +4561,7 @@ function compileSchemaRegex(pattern) {
4470
4561
  }
4471
4562
  else {
4472
4563
  try {
4564
+ // lgtm[js/regex-injection] JSON Schema `pattern` is intentionally regex syntax and constrained by MAX_SCHEMA_REGEX_LENGTH.
4473
4565
  const re = new RegExp(pattern);
4474
4566
  return { ok: true, value: re };
4475
4567
  }
@@ -4789,7 +4881,7 @@ function containsValidator(report, schema, json) {
4789
4881
  for (let idx = 0; idx < json.length; idx++) {
4790
4882
  const subReport = new Report_(report);
4791
4883
  subReports.push(subReport);
4792
- validate.call(this, subReport, containsSchema, json[idx]);
4884
+ this._jsonValidate(subReport, containsSchema, json[idx]);
4793
4885
  cacheValidationResult(report, containsSchema, json[idx], subReport.errors.length === 0);
4794
4886
  }
4795
4887
  const addContainsErrorIfNeeded = () => {
@@ -4822,7 +4914,7 @@ function minContainsValidator() { }
4822
4914
  function allOfValidator(report, schema, json) {
4823
4915
  // http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.5.3.2
4824
4916
  for (let i = 0; i < schema.allOf.length; i++) {
4825
- const validateResult = validate.call(this, report, schema.allOf[i], json);
4917
+ const validateResult = this._jsonValidate(report, schema.allOf[i], json);
4826
4918
  if (this.options.breakOnFirstError && validateResult === false) {
4827
4919
  break;
4828
4920
  }
@@ -4837,7 +4929,7 @@ function anyOfValidator(report, schema, json) {
4837
4929
  for (let i = 0; i < schema.anyOf.length; i++) {
4838
4930
  const subReport = new Report(report);
4839
4931
  subReports.push(subReport);
4840
- validate.call(this, subReport, schema.anyOf[i], json);
4932
+ this._jsonValidate(subReport, schema.anyOf[i], json);
4841
4933
  cacheValidationResult(report, schema.anyOf[i], json, subReport.errors.length === 0);
4842
4934
  }
4843
4935
  // Aggregate async tasks and decide when ready
@@ -4863,7 +4955,7 @@ function oneOfValidator(report, schema, json) {
4863
4955
  for (let i = 0; i < schema.oneOf.length; i++) {
4864
4956
  const subReport = new Report(report);
4865
4957
  subReports.push(subReport);
4866
- validate.call(this, subReport, schema.oneOf[i], json);
4958
+ this._jsonValidate(subReport, schema.oneOf[i], json);
4867
4959
  cacheValidationResult(report, schema.oneOf[i], json, subReport.errors.length === 0);
4868
4960
  }
4869
4961
  // Aggregate async tasks and decide when ready
@@ -4888,7 +4980,7 @@ function oneOfValidator(report, schema, json) {
4888
4980
  function notValidator(report, schema, json) {
4889
4981
  // http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.5.6.2
4890
4982
  const subReport = new Report(report);
4891
- if (validate.call(this, subReport, schema.not, json) === true) {
4983
+ if (this._jsonValidate(subReport, schema.not, json) === true) {
4892
4984
  report.addError('NOT_PASSED', undefined, undefined, schema, 'not');
4893
4985
  }
4894
4986
  }
@@ -4906,13 +4998,13 @@ function ifValidator(report, schema, json) {
4906
4998
  return;
4907
4999
  }
4908
5000
  const conditionReport = new Report(report);
4909
- validate.call(this, conditionReport, conditionSchema, json);
5001
+ this._jsonValidate(conditionReport, conditionSchema, json);
4910
5002
  cacheValidationResult(report, conditionSchema, json, conditionReport.errors.length === 0);
4911
5003
  const branchSchema = conditionReport.errors.length === 0 ? thenSchema : elseSchema;
4912
5004
  if (branchSchema === undefined) {
4913
5005
  return;
4914
5006
  }
4915
- validate.call(this, report, branchSchema, json);
5007
+ this._jsonValidate(report, branchSchema, json);
4916
5008
  }
4917
5009
  function thenValidator() {
4918
5010
  // handled by if
@@ -5168,7 +5260,7 @@ function dependenciesValidator(report, schema, json) {
5168
5260
  }
5169
5261
  else {
5170
5262
  // if dependency is a schema, validate against this schema
5171
- validate.call(this, report, dependencyDefinition, json);
5263
+ this._jsonValidate(report, dependencyDefinition, json);
5172
5264
  }
5173
5265
  }
5174
5266
  }
@@ -5187,7 +5279,7 @@ function dependentSchemasValidator(report, schema, json) {
5187
5279
  for (const dependencyName of keys) {
5188
5280
  if (Object.hasOwn(json, dependencyName)) {
5189
5281
  const dependencySchema = schema.dependentSchemas[dependencyName];
5190
- validate.call(this, report, dependencySchema, json);
5282
+ this._jsonValidate(report, dependencySchema, json);
5191
5283
  }
5192
5284
  }
5193
5285
  }
@@ -5240,7 +5332,7 @@ function propertyNamesValidator(report, schema, json) {
5240
5332
  for (const key of keys) {
5241
5333
  const subReport = new Report_(report);
5242
5334
  subReports.push(subReport);
5243
- validate.call(this, subReport, propertyNamesSchema, key);
5335
+ this._jsonValidate(subReport, propertyNamesSchema, key);
5244
5336
  }
5245
5337
  const addPropertyNameErrors = () => {
5246
5338
  for (let idx = 0; idx < keys.length; idx++) {
@@ -5452,21 +5544,13 @@ function formatValidator(report, schema, json) {
5452
5544
  const result = formatValidatorFn.call(this, json);
5453
5545
  if (result instanceof Promise) {
5454
5546
  // Promise-based async
5455
- const timeoutMs = this.options.asyncTimeout || 2000;
5456
5547
  const promiseResult = result;
5457
5548
  report.addAsyncTaskWithPath(async (callback) => {
5458
5549
  try {
5459
- const timeoutPromise = new Promise((_, reject) => {
5460
- setTimeout(() => reject(new Error('Async timeout')), timeoutMs);
5461
- });
5462
- const resolved = await Promise.race([promiseResult, timeoutPromise]);
5550
+ const resolved = await promiseResult;
5463
5551
  callback(resolved);
5464
5552
  }
5465
- catch (error) {
5466
- if (error.message === 'Async timeout') {
5467
- // Don't call callback, let global timeout handle it
5468
- return;
5469
- }
5553
+ catch (_error) {
5470
5554
  callback(false);
5471
5555
  }
5472
5556
  }, [], function (resolvedResult) {
@@ -6314,6 +6398,21 @@ function setSchemaReader(schemaReader) {
6314
6398
  _schemaReader = schemaReader;
6315
6399
  }
6316
6400
 
6401
+ /** Safely assign a property on `obj`, refusing prototype-polluting keys. */
6402
+ function safeSetProperty(obj, key, value) {
6403
+ const unsafeTargets = [
6404
+ Object.prototype,
6405
+ Function.prototype,
6406
+ Array.prototype,
6407
+ ];
6408
+ if (unsafeTargets.includes(obj)) {
6409
+ return;
6410
+ }
6411
+ /** Reject property names that could pollute Object.prototype (CWE-1321). */
6412
+ if (key !== '__proto__' && key !== 'constructor' && key !== 'prototype') {
6413
+ obj[key] = value;
6414
+ }
6415
+ }
6317
6416
  const collectIds = (obj, maxDepth = DEFAULT_MAX_RECURSION_DEPTH) => {
6318
6417
  const ids = [];
6319
6418
  function walk(node, scope, _depth = 0) {
@@ -6479,7 +6578,8 @@ const resolveReference = (base, ref) => {
6479
6578
  }
6480
6579
  let baseDir = baseNoFrag;
6481
6580
  if (!baseDir.endsWith('/')) {
6482
- baseDir = baseDir.replace(/[^/]*$/, '');
6581
+ const lastSlash = baseDir.lastIndexOf('/');
6582
+ baseDir = lastSlash === -1 ? '' : baseDir.slice(0, lastSlash + 1);
6483
6583
  }
6484
6584
  return baseDir + ref;
6485
6585
  };
@@ -6550,8 +6650,14 @@ class SchemaCompiler {
6550
6650
  this.collectAndCacheIds(schema);
6551
6651
  }
6552
6652
  }
6653
+ const canMutateSchemaObject = schema !== Object.prototype &&
6654
+ schema !== Function.prototype &&
6655
+ schema !== Array.prototype;
6553
6656
  // if we have an id than it should be cached already (if this instance has compiled it)
6554
- if (schema.__$compiled && schema.id && this.validator.scache.checkCacheForUri(schema.id) === false) {
6657
+ if (canMutateSchemaObject &&
6658
+ schema.__$compiled &&
6659
+ schema.id &&
6660
+ this.validator.scache.checkCacheForUri(schema.id) === false) {
6555
6661
  schema.__$compiled = undefined;
6556
6662
  }
6557
6663
  // do not re-compile schemas
@@ -6559,7 +6665,7 @@ class SchemaCompiler {
6559
6665
  return true;
6560
6666
  }
6561
6667
  // v8 - if $schema is not present, set $schema to default
6562
- if (!schema.$schema && this.validator.options.version !== 'none') {
6668
+ if (canMutateSchemaObject && !schema.$schema && this.validator.options.version !== 'none') {
6563
6669
  schema.$schema = this.validator.getDefaultSchemaId();
6564
6670
  }
6565
6671
  if (schema.id && typeof schema.id === 'string' && !options?.noCache) {
@@ -6574,7 +6680,9 @@ class SchemaCompiler {
6574
6680
  }
6575
6681
  // delete all __$missingReferences from previous compilation attempts
6576
6682
  const isValidExceptReferences = report.isValid();
6577
- delete schema.__$missingReferences;
6683
+ if (canMutateSchemaObject) {
6684
+ delete schema.__$missingReferences;
6685
+ }
6578
6686
  // collect all references that need to be resolved - $ref and $schema
6579
6687
  const useRefObjectScope = this.validator.options.version === 'draft2019-09' || this.validator.options.version === 'draft2020-12';
6580
6688
  const refs = collectReferences(schema, undefined, undefined, undefined, { useRefObjectScope }, this.validator.options.maxRecursionDepth);
@@ -6622,17 +6730,17 @@ class SchemaCompiler {
6622
6730
  report.addError('UNRESOLVABLE_REFERENCE', [refObj.ref]);
6623
6731
  report.path = report.path.slice(0, -refObj.path.length);
6624
6732
  // pusblish unresolved references out
6625
- if (isValidExceptReferences) {
6733
+ if (isValidExceptReferences && canMutateSchemaObject) {
6626
6734
  schema.__$missingReferences = schema.__$missingReferences || [];
6627
6735
  schema.__$missingReferences.push(refObj);
6628
6736
  }
6629
6737
  }
6630
6738
  }
6631
6739
  // this might create circular references
6632
- refObj.obj[`__${refObj.key}Resolved`] = response;
6740
+ safeSetProperty(refObj.obj, `__${refObj.key}Resolved`, response);
6633
6741
  }
6634
6742
  const isValid = report.isValid();
6635
- if (isValid) {
6743
+ if (isValid && canMutateSchemaObject) {
6636
6744
  schema.__$compiled = true;
6637
6745
  }
6638
6746
  // else {
@@ -6665,7 +6773,7 @@ class SchemaCompiler {
6665
6773
  const response = arr.find((x) => x.id === refObj.ref);
6666
6774
  if (response) {
6667
6775
  // this might create circular references
6668
- refObj.obj[`__${refObj.key}Resolved`] = response;
6776
+ safeSetProperty(refObj.obj, `__${refObj.key}Resolved`, response);
6669
6777
  // it's resolved now so delete it
6670
6778
  sch.__$missingReferences.splice(idx2, 1);
6671
6779
  }
@@ -7429,6 +7537,16 @@ class ZSchemaBase {
7429
7537
  this.sv = new SchemaValidator(this);
7430
7538
  this.options = normalizeOptions(options);
7431
7539
  }
7540
+ /**
7541
+ * Internal recursive JSON validation — delegates to the `validate` function
7542
+ * in `json-validation.ts`. Exposed as a method so that per-keyword validator
7543
+ * modules (array, combinators, object) can call back into the core validator
7544
+ * via `this` without importing `json-validation.ts` directly (which would
7545
+ * create a circular dependency).
7546
+ */
7547
+ _jsonValidate(report, schema, json) {
7548
+ return validate.call(this, report, schema, json);
7549
+ }
7432
7550
  getDefaultSchemaId() {
7433
7551
  return this.options.version && this.options.version !== 'none'
7434
7552
  ? VERSION_SCHEMA_URL_MAPPING[this.options.version]