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