z-schema 12.0.1 → 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
@@ -364,6 +364,7 @@ const get = (obj, path) => {
364
364
  const jsonSymbol = Symbol.for('z-schema/json');
365
365
  const schemaSymbol = Symbol.for('z-schema/schema');
366
366
 
367
+ const ASYNC_TIMEOUT_POLL_MS = 10;
367
368
  class Report {
368
369
  asyncTasks = [];
369
370
  commonErrorMessage;
@@ -438,6 +439,7 @@ class Report {
438
439
  }
439
440
  processAsyncTasks(timeout, callback) {
440
441
  const validationTimeout = Math.min(Math.max(timeout || 2000, 0), MAX_ASYNC_TIMEOUT);
442
+ const timeoutAt = Date.now() + validationTimeout;
441
443
  let tasksCount = this.asyncTasks.length;
442
444
  let timedOut = false;
443
445
  const finish = () => {
@@ -466,13 +468,19 @@ class Report {
466
468
  const respondCallback = respond(processFn);
467
469
  fn(...fnArgs, respondCallback);
468
470
  }
469
- setTimeout(() => {
470
- if (tasksCount > 0) {
471
+ const checkTimeout = () => {
472
+ if (timedOut || tasksCount <= 0) {
473
+ return;
474
+ }
475
+ if (Date.now() >= timeoutAt) {
471
476
  timedOut = true;
472
477
  this.addError('ASYNC_TIMEOUT', [tasksCount, validationTimeout]);
473
478
  callback(getValidateError({ details: this.errors }), false);
479
+ return;
474
480
  }
475
- }, validationTimeout);
481
+ setTimeout(checkTimeout, ASYNC_TIMEOUT_POLL_MS);
482
+ };
483
+ setTimeout(checkTimeout, ASYNC_TIMEOUT_POLL_MS);
476
484
  }
477
485
  getPath(returnPathAsString) {
478
486
  let path = [];
@@ -720,6 +728,17 @@ const normalizeOptions = (options) => {
720
728
  return normalized;
721
729
  };
722
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
+ }
723
742
  const getEffectiveId = (schema) => {
724
743
  let id = getId(schema);
725
744
  if ((!id || !isAbsoluteUri(id)) && typeof schema.id === 'string' && isAbsoluteUri(schema.id)) {
@@ -750,31 +769,31 @@ function prepareRemoteSchema(schema, uri, validationOptions, maxCloneDepth) {
750
769
  }
751
770
  class SchemaCache {
752
771
  validator;
753
- static global_cache = {};
754
- cache = {};
772
+ static global_cache = Object.create(null);
773
+ cache = Object.create(null);
755
774
  constructor(validator) {
756
775
  this.validator = validator;
757
776
  }
758
777
  static cacheSchemaByUri(uri, schema) {
759
- const remotePath = getRemotePath(uri);
778
+ const remotePath = getSafeRemotePath(uri);
760
779
  if (remotePath) {
761
780
  this.global_cache[remotePath] = schema;
762
781
  }
763
782
  }
764
783
  cacheSchemaByUri(uri, schema) {
765
- const remotePath = getRemotePath(uri);
784
+ const remotePath = getSafeRemotePath(uri);
766
785
  if (remotePath) {
767
786
  this.cache[remotePath] = schema;
768
787
  }
769
788
  }
770
789
  removeFromCacheByUri(uri) {
771
- const remotePath = getRemotePath(uri);
790
+ const remotePath = getSafeRemotePath(uri);
772
791
  if (remotePath) {
773
792
  delete this.cache[remotePath];
774
793
  }
775
794
  }
776
795
  checkCacheForUri(uri) {
777
- const remotePath = getRemotePath(uri);
796
+ const remotePath = getSafeRemotePath(uri);
778
797
  return remotePath ? this.cache[remotePath] != null : false;
779
798
  }
780
799
  getSchema(report, refOrSchema) {
@@ -789,6 +808,9 @@ class SchemaCache {
789
808
  return deepClone(refOrSchema, this.validator.options.maxRecursionDepth);
790
809
  }
791
810
  fromCache(path) {
811
+ if (path === '__proto__' || path === 'constructor' || path === 'prototype') {
812
+ return undefined;
813
+ }
792
814
  let found = this.cache[path];
793
815
  if (found) {
794
816
  return found;
@@ -820,7 +842,7 @@ class SchemaCache {
820
842
  }
821
843
  }
822
844
  }
823
- const remotePath = getRemotePath(uri);
845
+ const remotePath = getSafeRemotePath(uri);
824
846
  const queryPath = getQueryPath(uri);
825
847
  let result;
826
848
  let resolvedFromAncestor = false;
@@ -4523,6 +4545,7 @@ function compileSchemaRegex(pattern) {
4523
4545
  if (needsUnicode) {
4524
4546
  // Try compiling with 'u' flag only
4525
4547
  try {
4548
+ // lgtm[js/regex-injection] JSON Schema `pattern` is intentionally regex syntax and constrained by MAX_SCHEMA_REGEX_LENGTH.
4526
4549
  const re = new RegExp(pattern, 'u');
4527
4550
  return { ok: true, value: re };
4528
4551
  }
@@ -4538,6 +4561,7 @@ function compileSchemaRegex(pattern) {
4538
4561
  }
4539
4562
  else {
4540
4563
  try {
4564
+ // lgtm[js/regex-injection] JSON Schema `pattern` is intentionally regex syntax and constrained by MAX_SCHEMA_REGEX_LENGTH.
4541
4565
  const re = new RegExp(pattern);
4542
4566
  return { ok: true, value: re };
4543
4567
  }
@@ -5520,21 +5544,13 @@ function formatValidator(report, schema, json) {
5520
5544
  const result = formatValidatorFn.call(this, json);
5521
5545
  if (result instanceof Promise) {
5522
5546
  // Promise-based async
5523
- const timeoutMs = Math.min(Math.max(this.options.asyncTimeout || 2000, 0), MAX_ASYNC_TIMEOUT);
5524
5547
  const promiseResult = result;
5525
5548
  report.addAsyncTaskWithPath(async (callback) => {
5526
5549
  try {
5527
- const timeoutPromise = new Promise((_, reject) => {
5528
- setTimeout(() => reject(new Error('Async timeout')), timeoutMs);
5529
- });
5530
- const resolved = await Promise.race([promiseResult, timeoutPromise]);
5550
+ const resolved = await promiseResult;
5531
5551
  callback(resolved);
5532
5552
  }
5533
- catch (error) {
5534
- if (error.message === 'Async timeout') {
5535
- // Don't call callback, let global timeout handle it
5536
- return;
5537
- }
5553
+ catch (_error) {
5538
5554
  callback(false);
5539
5555
  }
5540
5556
  }, [], function (resolvedResult) {
@@ -6384,6 +6400,14 @@ function setSchemaReader(schemaReader) {
6384
6400
 
6385
6401
  /** Safely assign a property on `obj`, refusing prototype-polluting keys. */
6386
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
+ }
6387
6411
  /** Reject property names that could pollute Object.prototype (CWE-1321). */
6388
6412
  if (key !== '__proto__' && key !== 'constructor' && key !== 'prototype') {
6389
6413
  obj[key] = value;
@@ -6626,8 +6650,14 @@ class SchemaCompiler {
6626
6650
  this.collectAndCacheIds(schema);
6627
6651
  }
6628
6652
  }
6653
+ const canMutateSchemaObject = schema !== Object.prototype &&
6654
+ schema !== Function.prototype &&
6655
+ schema !== Array.prototype;
6629
6656
  // if we have an id than it should be cached already (if this instance has compiled it)
6630
- 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) {
6631
6661
  schema.__$compiled = undefined;
6632
6662
  }
6633
6663
  // do not re-compile schemas
@@ -6635,7 +6665,7 @@ class SchemaCompiler {
6635
6665
  return true;
6636
6666
  }
6637
6667
  // v8 - if $schema is not present, set $schema to default
6638
- if (!schema.$schema && this.validator.options.version !== 'none') {
6668
+ if (canMutateSchemaObject && !schema.$schema && this.validator.options.version !== 'none') {
6639
6669
  schema.$schema = this.validator.getDefaultSchemaId();
6640
6670
  }
6641
6671
  if (schema.id && typeof schema.id === 'string' && !options?.noCache) {
@@ -6650,7 +6680,9 @@ class SchemaCompiler {
6650
6680
  }
6651
6681
  // delete all __$missingReferences from previous compilation attempts
6652
6682
  const isValidExceptReferences = report.isValid();
6653
- delete schema.__$missingReferences;
6683
+ if (canMutateSchemaObject) {
6684
+ delete schema.__$missingReferences;
6685
+ }
6654
6686
  // collect all references that need to be resolved - $ref and $schema
6655
6687
  const useRefObjectScope = this.validator.options.version === 'draft2019-09' || this.validator.options.version === 'draft2020-12';
6656
6688
  const refs = collectReferences(schema, undefined, undefined, undefined, { useRefObjectScope }, this.validator.options.maxRecursionDepth);
@@ -6698,7 +6730,7 @@ class SchemaCompiler {
6698
6730
  report.addError('UNRESOLVABLE_REFERENCE', [refObj.ref]);
6699
6731
  report.path = report.path.slice(0, -refObj.path.length);
6700
6732
  // pusblish unresolved references out
6701
- if (isValidExceptReferences) {
6733
+ if (isValidExceptReferences && canMutateSchemaObject) {
6702
6734
  schema.__$missingReferences = schema.__$missingReferences || [];
6703
6735
  schema.__$missingReferences.push(refObj);
6704
6736
  }
@@ -6708,7 +6740,7 @@ class SchemaCompiler {
6708
6740
  safeSetProperty(refObj.obj, `__${refObj.key}Resolved`, response);
6709
6741
  }
6710
6742
  const isValid = report.isValid();
6711
- if (isValid) {
6743
+ if (isValid && canMutateSchemaObject) {
6712
6744
  schema.__$compiled = true;
6713
6745
  }
6714
6746
  // else {
package/dist/report.js CHANGED
@@ -5,6 +5,7 @@ import { get } from './utils/json.js';
5
5
  import { jsonSymbol, schemaSymbol } from './utils/symbols.js';
6
6
  import { isAbsoluteUri } from './utils/uri.js';
7
7
  import { isObject } from './utils/what-is.js';
8
+ const ASYNC_TIMEOUT_POLL_MS = 10;
8
9
  export class Report {
9
10
  asyncTasks = [];
10
11
  commonErrorMessage;
@@ -79,6 +80,7 @@ export class Report {
79
80
  }
80
81
  processAsyncTasks(timeout, callback) {
81
82
  const validationTimeout = Math.min(Math.max(timeout || 2000, 0), MAX_ASYNC_TIMEOUT);
83
+ const timeoutAt = Date.now() + validationTimeout;
82
84
  let tasksCount = this.asyncTasks.length;
83
85
  let timedOut = false;
84
86
  const finish = () => {
@@ -107,13 +109,19 @@ export class Report {
107
109
  const respondCallback = respond(processFn);
108
110
  fn(...fnArgs, respondCallback);
109
111
  }
110
- setTimeout(() => {
111
- if (tasksCount > 0) {
112
+ const checkTimeout = () => {
113
+ if (timedOut || tasksCount <= 0) {
114
+ return;
115
+ }
116
+ if (Date.now() >= timeoutAt) {
112
117
  timedOut = true;
113
118
  this.addError('ASYNC_TIMEOUT', [tasksCount, validationTimeout]);
114
119
  callback(getValidateError({ details: this.errors }), false);
120
+ return;
115
121
  }
116
- }, validationTimeout);
122
+ setTimeout(checkTimeout, ASYNC_TIMEOUT_POLL_MS);
123
+ };
124
+ setTimeout(checkTimeout, ASYNC_TIMEOUT_POLL_MS);
117
125
  }
118
126
  getPath(returnPathAsString) {
119
127
  let path = [];
@@ -4,6 +4,17 @@ import { deepClone } from './utils/clone.js';
4
4
  import { decodeJSONPointer } from './utils/json.js';
5
5
  import { getQueryPath, getRemotePath, isAbsoluteUri } from './utils/uri.js';
6
6
  import { normalizeOptions } from './z-schema-options.js';
7
+ // Normalize a URI into a cache key, rejecting keys that could pollute Object.prototype.
8
+ function getSafeRemotePath(uri) {
9
+ const remotePath = getRemotePath(uri);
10
+ if (!remotePath) {
11
+ return undefined;
12
+ }
13
+ if (remotePath === '__proto__' || remotePath === 'constructor' || remotePath === 'prototype') {
14
+ return undefined;
15
+ }
16
+ return remotePath;
17
+ }
7
18
  const getEffectiveId = (schema) => {
8
19
  let id = getId(schema);
9
20
  if ((!id || !isAbsoluteUri(id)) && typeof schema.id === 'string' && isAbsoluteUri(schema.id)) {
@@ -34,31 +45,31 @@ export function prepareRemoteSchema(schema, uri, validationOptions, maxCloneDept
34
45
  }
35
46
  export class SchemaCache {
36
47
  validator;
37
- static global_cache = {};
38
- cache = {};
48
+ static global_cache = Object.create(null);
49
+ cache = Object.create(null);
39
50
  constructor(validator) {
40
51
  this.validator = validator;
41
52
  }
42
53
  static cacheSchemaByUri(uri, schema) {
43
- const remotePath = getRemotePath(uri);
54
+ const remotePath = getSafeRemotePath(uri);
44
55
  if (remotePath) {
45
56
  this.global_cache[remotePath] = schema;
46
57
  }
47
58
  }
48
59
  cacheSchemaByUri(uri, schema) {
49
- const remotePath = getRemotePath(uri);
60
+ const remotePath = getSafeRemotePath(uri);
50
61
  if (remotePath) {
51
62
  this.cache[remotePath] = schema;
52
63
  }
53
64
  }
54
65
  removeFromCacheByUri(uri) {
55
- const remotePath = getRemotePath(uri);
66
+ const remotePath = getSafeRemotePath(uri);
56
67
  if (remotePath) {
57
68
  delete this.cache[remotePath];
58
69
  }
59
70
  }
60
71
  checkCacheForUri(uri) {
61
- const remotePath = getRemotePath(uri);
72
+ const remotePath = getSafeRemotePath(uri);
62
73
  return remotePath ? this.cache[remotePath] != null : false;
63
74
  }
64
75
  getSchema(report, refOrSchema) {
@@ -73,6 +84,9 @@ export class SchemaCache {
73
84
  return deepClone(refOrSchema, this.validator.options.maxRecursionDepth);
74
85
  }
75
86
  fromCache(path) {
87
+ if (path === '__proto__' || path === 'constructor' || path === 'prototype') {
88
+ return undefined;
89
+ }
76
90
  let found = this.cache[path];
77
91
  if (found) {
78
92
  return found;
@@ -104,7 +118,7 @@ export class SchemaCache {
104
118
  }
105
119
  }
106
120
  }
107
- const remotePath = getRemotePath(uri);
121
+ const remotePath = getSafeRemotePath(uri);
108
122
  const queryPath = getQueryPath(uri);
109
123
  let result;
110
124
  let resolvedFromAncestor = false;
@@ -5,6 +5,14 @@ import { getRemotePath, isAbsoluteUri } from './utils/uri.js';
5
5
  import { getSchemaReader } from './z-schema-reader.js';
6
6
  /** Safely assign a property on `obj`, refusing prototype-polluting keys. */
7
7
  function safeSetProperty(obj, key, value) {
8
+ const unsafeTargets = [
9
+ Object.prototype,
10
+ Function.prototype,
11
+ Array.prototype,
12
+ ];
13
+ if (unsafeTargets.includes(obj)) {
14
+ return;
15
+ }
8
16
  /** Reject property names that could pollute Object.prototype (CWE-1321). */
9
17
  if (key !== '__proto__' && key !== 'constructor' && key !== 'prototype') {
10
18
  obj[key] = value;
@@ -247,8 +255,14 @@ export class SchemaCompiler {
247
255
  this.collectAndCacheIds(schema);
248
256
  }
249
257
  }
258
+ const canMutateSchemaObject = schema !== Object.prototype &&
259
+ schema !== Function.prototype &&
260
+ schema !== Array.prototype;
250
261
  // if we have an id than it should be cached already (if this instance has compiled it)
251
- if (schema.__$compiled && schema.id && this.validator.scache.checkCacheForUri(schema.id) === false) {
262
+ if (canMutateSchemaObject &&
263
+ schema.__$compiled &&
264
+ schema.id &&
265
+ this.validator.scache.checkCacheForUri(schema.id) === false) {
252
266
  schema.__$compiled = undefined;
253
267
  }
254
268
  // do not re-compile schemas
@@ -256,7 +270,7 @@ export class SchemaCompiler {
256
270
  return true;
257
271
  }
258
272
  // v8 - if $schema is not present, set $schema to default
259
- if (!schema.$schema && this.validator.options.version !== 'none') {
273
+ if (canMutateSchemaObject && !schema.$schema && this.validator.options.version !== 'none') {
260
274
  schema.$schema = this.validator.getDefaultSchemaId();
261
275
  }
262
276
  if (schema.id && typeof schema.id === 'string' && !options?.noCache) {
@@ -271,7 +285,9 @@ export class SchemaCompiler {
271
285
  }
272
286
  // delete all __$missingReferences from previous compilation attempts
273
287
  const isValidExceptReferences = report.isValid();
274
- delete schema.__$missingReferences;
288
+ if (canMutateSchemaObject) {
289
+ delete schema.__$missingReferences;
290
+ }
275
291
  // collect all references that need to be resolved - $ref and $schema
276
292
  const useRefObjectScope = this.validator.options.version === 'draft2019-09' || this.validator.options.version === 'draft2020-12';
277
293
  const refs = collectReferences(schema, undefined, undefined, undefined, { useRefObjectScope }, this.validator.options.maxRecursionDepth);
@@ -325,7 +341,7 @@ export class SchemaCompiler {
325
341
  report.addError('UNRESOLVABLE_REFERENCE', [refObj.ref]);
326
342
  report.path = report.path.slice(0, -refObj.path.length);
327
343
  // pusblish unresolved references out
328
- if (isValidExceptReferences) {
344
+ if (isValidExceptReferences && canMutateSchemaObject) {
329
345
  schema.__$missingReferences = schema.__$missingReferences || [];
330
346
  schema.__$missingReferences.push(refObj);
331
347
  }
@@ -335,7 +351,7 @@ export class SchemaCompiler {
335
351
  safeSetProperty(refObj.obj, `__${refObj.key}Resolved`, response);
336
352
  }
337
353
  const isValid = report.isValid();
338
- if (isValid) {
354
+ if (isValid && canMutateSchemaObject) {
339
355
  schema.__$compiled = true;
340
356
  }
341
357
  // else {
@@ -19,6 +19,7 @@ export function compileSchemaRegex(pattern) {
19
19
  if (needsUnicode) {
20
20
  // Try compiling with 'u' flag only
21
21
  try {
22
+ // lgtm[js/regex-injection] JSON Schema `pattern` is intentionally regex syntax and constrained by MAX_SCHEMA_REGEX_LENGTH.
22
23
  const re = new RegExp(pattern, 'u');
23
24
  return { ok: true, value: re };
24
25
  }
@@ -34,6 +35,7 @@ export function compileSchemaRegex(pattern) {
34
35
  }
35
36
  else {
36
37
  try {
38
+ // lgtm[js/regex-injection] JSON Schema `pattern` is intentionally regex syntax and constrained by MAX_SCHEMA_REGEX_LENGTH.
37
39
  const re = new RegExp(pattern);
38
40
  return { ok: true, value: re };
39
41
  }
@@ -1,6 +1,5 @@
1
1
  import { getFormatValidators } from '../format-validators.js';
2
2
  import { decodeBase64, isValidBase64 } from '../utils/base64.js';
3
- import { MAX_ASYNC_TIMEOUT } from '../utils/constants.js';
4
3
  import { compileSchemaRegex } from '../utils/schema-regex.js';
5
4
  import { unicodeLength } from '../utils/unicode.js';
6
5
  import { whatIs } from '../utils/what-is.js';
@@ -94,21 +93,13 @@ export function formatValidator(report, schema, json) {
94
93
  const result = formatValidatorFn.call(this, json);
95
94
  if (result instanceof Promise) {
96
95
  // Promise-based async
97
- const timeoutMs = Math.min(Math.max(this.options.asyncTimeout || 2000, 0), MAX_ASYNC_TIMEOUT);
98
96
  const promiseResult = result;
99
97
  report.addAsyncTaskWithPath(async (callback) => {
100
98
  try {
101
- const timeoutPromise = new Promise((_, reject) => {
102
- setTimeout(() => reject(new Error('Async timeout')), timeoutMs);
103
- });
104
- const resolved = await Promise.race([promiseResult, timeoutPromise]);
99
+ const resolved = await promiseResult;
105
100
  callback(resolved);
106
101
  }
107
- catch (error) {
108
- if (error.message === 'Async timeout') {
109
- // Don't call callback, let global timeout handle it
110
- return;
111
- }
102
+ catch (_error) {
112
103
  callback(false);
113
104
  }
114
105
  }, [], function (resolvedResult) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "z-schema",
3
- "version": "12.0.1",
3
+ "version": "12.0.2",
4
4
  "engines": {
5
5
  "node": ">=22.0.0"
6
6
  },
package/src/report.ts CHANGED
@@ -11,6 +11,8 @@ import { jsonSymbol, schemaSymbol } from './utils/symbols.js';
11
11
  import { isAbsoluteUri } from './utils/uri.js';
12
12
  import { isObject } from './utils/what-is.js';
13
13
 
14
+ const ASYNC_TIMEOUT_POLL_MS = 10;
15
+
14
16
  export interface SchemaErrorDetail {
15
17
  /**
16
18
  * Example: "Expected type string but found type array"
@@ -160,6 +162,7 @@ export class Report {
160
162
 
161
163
  processAsyncTasks(timeout: number | undefined, callback: ValidateCallback) {
162
164
  const validationTimeout = Math.min(Math.max(timeout || 2000, 0), MAX_ASYNC_TIMEOUT);
165
+ const timeoutAt = Date.now() + validationTimeout;
163
166
  let tasksCount = this.asyncTasks.length;
164
167
  let timedOut = false;
165
168
 
@@ -193,13 +196,19 @@ export class Report {
193
196
  fn(...fnArgs, respondCallback);
194
197
  }
195
198
 
196
- setTimeout(() => {
197
- if (tasksCount > 0) {
199
+ const checkTimeout = () => {
200
+ if (timedOut || tasksCount <= 0) {
201
+ return;
202
+ }
203
+ if (Date.now() >= timeoutAt) {
198
204
  timedOut = true;
199
205
  this.addError('ASYNC_TIMEOUT', [tasksCount, validationTimeout]);
200
206
  callback(getValidateError({ details: this.errors }), false);
207
+ return;
201
208
  }
202
- }, validationTimeout);
209
+ setTimeout(checkTimeout, ASYNC_TIMEOUT_POLL_MS);
210
+ };
211
+ setTimeout(checkTimeout, ASYNC_TIMEOUT_POLL_MS);
203
212
  }
204
213
 
205
214
  getPath(returnPathAsString?: boolean) {
@@ -12,6 +12,18 @@ import { normalizeOptions } from './z-schema-options.js';
12
12
  export type SchemaCacheStorage = Record<string, JsonSchemaInternal>;
13
13
  export type ReferenceSchemaCacheStorage = Array<[JsonSchemaInternal, JsonSchemaInternal]>;
14
14
 
15
+ // Normalize a URI into a cache key, rejecting keys that could pollute Object.prototype.
16
+ function getSafeRemotePath(uri: string): string | undefined {
17
+ const remotePath = getRemotePath(uri);
18
+ if (!remotePath) {
19
+ return undefined;
20
+ }
21
+ if (remotePath === '__proto__' || remotePath === 'constructor' || remotePath === 'prototype') {
22
+ return undefined;
23
+ }
24
+ return remotePath;
25
+ }
26
+
15
27
  const getEffectiveId = (schema: JsonSchemaInternal): string | undefined => {
16
28
  let id = getId(schema);
17
29
  if ((!id || !isAbsoluteUri(id)) && typeof schema.id === 'string' && isAbsoluteUri(schema.id)) {
@@ -51,34 +63,34 @@ export function prepareRemoteSchema(
51
63
  }
52
64
 
53
65
  export class SchemaCache {
54
- static global_cache: SchemaCacheStorage = {};
55
- cache: SchemaCacheStorage = {};
66
+ static global_cache: SchemaCacheStorage = Object.create(null);
67
+ cache: SchemaCacheStorage = Object.create(null);
56
68
 
57
69
  constructor(private validator: ZSchemaBase) {}
58
70
 
59
71
  static cacheSchemaByUri(uri: string, schema: JsonSchemaInternal) {
60
- const remotePath = getRemotePath(uri);
72
+ const remotePath = getSafeRemotePath(uri);
61
73
  if (remotePath) {
62
74
  this.global_cache[remotePath] = schema;
63
75
  }
64
76
  }
65
77
 
66
78
  cacheSchemaByUri(uri: string, schema: JsonSchemaInternal) {
67
- const remotePath = getRemotePath(uri);
79
+ const remotePath = getSafeRemotePath(uri);
68
80
  if (remotePath) {
69
81
  this.cache[remotePath] = schema;
70
82
  }
71
83
  }
72
84
 
73
85
  removeFromCacheByUri(uri: string) {
74
- const remotePath = getRemotePath(uri);
86
+ const remotePath = getSafeRemotePath(uri);
75
87
  if (remotePath) {
76
88
  delete this.cache[remotePath];
77
89
  }
78
90
  }
79
91
 
80
92
  checkCacheForUri(uri: string) {
81
- const remotePath = getRemotePath(uri);
93
+ const remotePath = getSafeRemotePath(uri);
82
94
  return remotePath ? this.cache[remotePath] != null : false;
83
95
  }
84
96
 
@@ -98,6 +110,9 @@ export class SchemaCache {
98
110
  }
99
111
 
100
112
  fromCache(path: string): JsonSchemaInternal | undefined {
113
+ if (path === '__proto__' || path === 'constructor' || path === 'prototype') {
114
+ return undefined;
115
+ }
101
116
  let found = this.cache[path];
102
117
  if (found) {
103
118
  return found;
@@ -130,7 +145,7 @@ export class SchemaCache {
130
145
  }
131
146
  }
132
147
 
133
- const remotePath = getRemotePath(uri);
148
+ const remotePath = getSafeRemotePath(uri);
134
149
  const queryPath = getQueryPath(uri);
135
150
  let result: JsonSchemaInternal | undefined;
136
151
  let resolvedFromAncestor = false;
@@ -9,6 +9,14 @@ import { getSchemaReader } from './z-schema-reader.js';
9
9
 
10
10
  /** Safely assign a property on `obj`, refusing prototype-polluting keys. */
11
11
  function safeSetProperty(obj: Record<string, unknown>, key: string, value: unknown): void {
12
+ const unsafeTargets = [
13
+ Object.prototype as unknown as Record<string, unknown>,
14
+ Function.prototype as unknown as Record<string, unknown>,
15
+ Array.prototype as unknown as Record<string, unknown>,
16
+ ];
17
+ if (unsafeTargets.includes(obj)) {
18
+ return;
19
+ }
12
20
  /** Reject property names that could pollute Object.prototype (CWE-1321). */
13
21
  if (key !== '__proto__' && key !== 'constructor' && key !== 'prototype') {
14
22
  obj[key] = value;
@@ -300,8 +308,18 @@ export class SchemaCompiler {
300
308
  }
301
309
  }
302
310
 
311
+ const canMutateSchemaObject =
312
+ schema !== (Object.prototype as unknown as JsonSchemaInternal) &&
313
+ schema !== (Function.prototype as unknown as JsonSchemaInternal) &&
314
+ schema !== (Array.prototype as unknown as JsonSchemaInternal);
315
+
303
316
  // if we have an id than it should be cached already (if this instance has compiled it)
304
- if (schema.__$compiled && schema.id && this.validator.scache.checkCacheForUri(schema.id) === false) {
317
+ if (
318
+ canMutateSchemaObject &&
319
+ schema.__$compiled &&
320
+ schema.id &&
321
+ this.validator.scache.checkCacheForUri(schema.id) === false
322
+ ) {
305
323
  schema.__$compiled = undefined;
306
324
  }
307
325
 
@@ -311,7 +329,7 @@ export class SchemaCompiler {
311
329
  }
312
330
 
313
331
  // v8 - if $schema is not present, set $schema to default
314
- if (!schema.$schema && this.validator.options.version !== 'none') {
332
+ if (canMutateSchemaObject && !schema.$schema && this.validator.options.version !== 'none') {
315
333
  schema.$schema = this.validator.getDefaultSchemaId();
316
334
  }
317
335
 
@@ -329,7 +347,9 @@ export class SchemaCompiler {
329
347
 
330
348
  // delete all __$missingReferences from previous compilation attempts
331
349
  const isValidExceptReferences = report.isValid();
332
- delete schema.__$missingReferences;
350
+ if (canMutateSchemaObject) {
351
+ delete schema.__$missingReferences;
352
+ }
333
353
 
334
354
  // collect all references that need to be resolved - $ref and $schema
335
355
  const useRefObjectScope =
@@ -393,7 +413,7 @@ export class SchemaCompiler {
393
413
  report.path = report.path.slice(0, -refObj.path.length);
394
414
 
395
415
  // pusblish unresolved references out
396
- if (isValidExceptReferences) {
416
+ if (isValidExceptReferences && canMutateSchemaObject) {
397
417
  schema.__$missingReferences = schema.__$missingReferences || [];
398
418
  schema.__$missingReferences.push(refObj);
399
419
  }
@@ -404,7 +424,7 @@ export class SchemaCompiler {
404
424
  }
405
425
 
406
426
  const isValid = report.isValid();
407
- if (isValid) {
427
+ if (isValid && canMutateSchemaObject) {
408
428
  schema.__$compiled = true;
409
429
  }
410
430
  // else {
@@ -25,6 +25,7 @@ export function compileSchemaRegex(
25
25
  if (needsUnicode) {
26
26
  // Try compiling with 'u' flag only
27
27
  try {
28
+ // lgtm[js/regex-injection] JSON Schema `pattern` is intentionally regex syntax and constrained by MAX_SCHEMA_REGEX_LENGTH.
28
29
  const re = new RegExp(pattern, 'u');
29
30
  return { ok: true, value: re };
30
31
  } catch (e: any) {
@@ -38,6 +39,7 @@ export function compileSchemaRegex(
38
39
  }
39
40
  } else {
40
41
  try {
42
+ // lgtm[js/regex-injection] JSON Schema `pattern` is intentionally regex syntax and constrained by MAX_SCHEMA_REGEX_LENGTH.
41
43
  const re = new RegExp(pattern);
42
44
  return { ok: true, value: re };
43
45
  } catch (e: any) {