z-schema 12.0.1 → 12.0.3

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.
@@ -157,7 +157,9 @@ const durationValidator: FormatValidatorFn = (input: unknown) => {
157
157
  const datePart = parts[0];
158
158
  const timePart = parts.length === 2 ? parts[1] : undefined;
159
159
 
160
- if (!/^(?:\d+Y)?(?:\d+M)?(?:\d+D)?$/.test(datePart)) {
160
+ // RFC 3339 Appendix A ABNF: dur-year = 1*DIGIT "Y" [dur-month], dur-month = 1*DIGIT "M" [dur-day]
161
+ // Y can only be followed by M (not D directly), so P1Y2D is invalid
162
+ if (datePart.length > 0 && !/^(?:\d+Y(?:\d+M(?:\d+D)?)?|\d+M(?:\d+D)?|\d+D)$/.test(datePart)) {
161
163
  return false;
162
164
  }
163
165
 
@@ -168,7 +170,9 @@ const durationValidator: FormatValidatorFn = (input: unknown) => {
168
170
  if (timePart.length === 0) {
169
171
  return false;
170
172
  }
171
- if (!/^(?:\d+H)?(?:\d+M)?(?:\d+S)?$/.test(timePart)) {
173
+ // RFC 3339 Appendix A ABNF: dur-hour = 1*DIGIT "H" [dur-minute], dur-minute = 1*DIGIT "M" [dur-second]
174
+ // H can only be followed by M (not S directly), so PT1H2S is invalid
175
+ if (!/^(?:\d+H(?:\d+M(?:\d+S)?)?|\d+M(?:\d+S)?|\d+S)$/.test(timePart)) {
172
176
  return false;
173
177
  }
174
178
  hasTimeComponent = /\d+[HMS]/.test(timePart);
@@ -189,11 +193,23 @@ const uuidValidator: FormatValidatorFn = (input: unknown) => {
189
193
 
190
194
  const strictUriValidator: FormatValidatorFn = (uri: unknown) => typeof uri !== 'string' || isURLModule.default(uri);
191
195
 
196
+ const hasValidPercentEncoding = (str: string): boolean => {
197
+ for (let i = 0; i < str.length; i++) {
198
+ if (str[i] === '%') {
199
+ if (i + 2 >= str.length || !/[0-9a-fA-F]/.test(str[i + 1]) || !/[0-9a-fA-F]/.test(str[i + 2])) {
200
+ return false;
201
+ }
202
+ }
203
+ }
204
+ return true;
205
+ };
206
+
192
207
  const uriValidator: FormatValidatorFn = function (uri: unknown) {
193
208
  if (typeof uri !== 'string') return true;
194
209
  // eslint-disable-next-line no-control-regex
195
210
  if (/[^\x00-\x7F]/.test(uri)) return false;
196
- const match = uri.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^/]*)/);
211
+ if (!hasValidPercentEncoding(uri)) return false;
212
+ const match = uri.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^/?#]*)/);
197
213
  if (match) {
198
214
  const authority = match[2];
199
215
  const atIndex = authority.indexOf('@');
@@ -203,6 +219,21 @@ const uriValidator: FormatValidatorFn = function (uri: unknown) {
203
219
  return false;
204
220
  }
205
221
  }
222
+ // Validate port: must be numeric
223
+ let hostPort = atIndex >= 0 ? authority.substring(atIndex + 1) : authority;
224
+ if (hostPort.startsWith('[')) {
225
+ const bracketEnd = hostPort.indexOf(']');
226
+ if (bracketEnd >= 0) {
227
+ hostPort = hostPort.substring(bracketEnd + 1);
228
+ }
229
+ }
230
+ const colonIndex = hostPort.lastIndexOf(':');
231
+ if (colonIndex >= 0) {
232
+ const port = hostPort.substring(colonIndex + 1);
233
+ if (port.length > 0 && !/^\d+$/.test(port)) {
234
+ return false;
235
+ }
236
+ }
206
237
  }
207
238
  return /^[a-zA-Z][a-zA-Z0-9+.-]*:[^"\\<>^{}^`| ]*$/.test(uri);
208
239
  };
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) {
@@ -4,7 +4,6 @@ import type { ZSchemaBase } from '../z-schema-base.js';
4
4
 
5
5
  import { getFormatValidators } from '../format-validators.js';
6
6
  import { decodeBase64, isValidBase64 } from '../utils/base64.js';
7
- import { MAX_ASYNC_TIMEOUT } from '../utils/constants.js';
8
7
  import { compileSchemaRegex } from '../utils/schema-regex.js';
9
8
  import { unicodeLength } from '../utils/unicode.js';
10
9
  import { whatIs } from '../utils/what-is.js';
@@ -108,21 +107,13 @@ export function formatValidator(this: ZSchemaBase, report: Report, schema: JsonS
108
107
  const result = formatValidatorFn.call(this, json);
109
108
  if (result instanceof Promise) {
110
109
  // Promise-based async
111
- const timeoutMs = Math.min(Math.max(this.options.asyncTimeout || 2000, 0), MAX_ASYNC_TIMEOUT);
112
110
  const promiseResult = result;
113
111
  report.addAsyncTaskWithPath(
114
112
  async (callback) => {
115
113
  try {
116
- const timeoutPromise = new Promise<never>((_, reject) => {
117
- setTimeout(() => reject(new Error('Async timeout')), timeoutMs);
118
- });
119
- const resolved = await Promise.race([promiseResult, timeoutPromise]);
114
+ const resolved = await promiseResult;
120
115
  callback(resolved);
121
- } catch (error) {
122
- if ((error as Error).message === 'Async timeout') {
123
- // Don't call callback, let global timeout handle it
124
- return;
125
- }
116
+ } catch (_error) {
126
117
  callback(false);
127
118
  }
128
119
  },
package/umd/ZSchema.js CHANGED
@@ -366,6 +366,7 @@
366
366
  const jsonSymbol = Symbol.for('z-schema/json');
367
367
  const schemaSymbol = Symbol.for('z-schema/schema');
368
368
 
369
+ const ASYNC_TIMEOUT_POLL_MS = 10;
369
370
  class Report {
370
371
  asyncTasks = [];
371
372
  commonErrorMessage;
@@ -440,6 +441,7 @@
440
441
  }
441
442
  processAsyncTasks(timeout, callback) {
442
443
  const validationTimeout = Math.min(Math.max(timeout || 2000, 0), MAX_ASYNC_TIMEOUT);
444
+ const timeoutAt = Date.now() + validationTimeout;
443
445
  let tasksCount = this.asyncTasks.length;
444
446
  let timedOut = false;
445
447
  const finish = () => {
@@ -468,13 +470,19 @@
468
470
  const respondCallback = respond(processFn);
469
471
  fn(...fnArgs, respondCallback);
470
472
  }
471
- setTimeout(() => {
472
- if (tasksCount > 0) {
473
+ const checkTimeout = () => {
474
+ if (timedOut || tasksCount <= 0) {
475
+ return;
476
+ }
477
+ if (Date.now() >= timeoutAt) {
473
478
  timedOut = true;
474
479
  this.addError('ASYNC_TIMEOUT', [tasksCount, validationTimeout]);
475
480
  callback(getValidateError({ details: this.errors }), false);
481
+ return;
476
482
  }
477
- }, validationTimeout);
483
+ setTimeout(checkTimeout, ASYNC_TIMEOUT_POLL_MS);
484
+ };
485
+ setTimeout(checkTimeout, ASYNC_TIMEOUT_POLL_MS);
478
486
  }
479
487
  getPath(returnPathAsString) {
480
488
  let path = [];
@@ -722,6 +730,17 @@
722
730
  return normalized;
723
731
  };
724
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
+ }
725
744
  const getEffectiveId = (schema) => {
726
745
  let id = getId(schema);
727
746
  if ((!id || !isAbsoluteUri(id)) && typeof schema.id === 'string' && isAbsoluteUri(schema.id)) {
@@ -752,31 +771,31 @@
752
771
  }
753
772
  class SchemaCache {
754
773
  validator;
755
- static global_cache = {};
756
- cache = {};
774
+ static global_cache = Object.create(null);
775
+ cache = Object.create(null);
757
776
  constructor(validator) {
758
777
  this.validator = validator;
759
778
  }
760
779
  static cacheSchemaByUri(uri, schema) {
761
- const remotePath = getRemotePath(uri);
780
+ const remotePath = getSafeRemotePath(uri);
762
781
  if (remotePath) {
763
782
  this.global_cache[remotePath] = schema;
764
783
  }
765
784
  }
766
785
  cacheSchemaByUri(uri, schema) {
767
- const remotePath = getRemotePath(uri);
786
+ const remotePath = getSafeRemotePath(uri);
768
787
  if (remotePath) {
769
788
  this.cache[remotePath] = schema;
770
789
  }
771
790
  }
772
791
  removeFromCacheByUri(uri) {
773
- const remotePath = getRemotePath(uri);
792
+ const remotePath = getSafeRemotePath(uri);
774
793
  if (remotePath) {
775
794
  delete this.cache[remotePath];
776
795
  }
777
796
  }
778
797
  checkCacheForUri(uri) {
779
- const remotePath = getRemotePath(uri);
798
+ const remotePath = getSafeRemotePath(uri);
780
799
  return remotePath ? this.cache[remotePath] != null : false;
781
800
  }
782
801
  getSchema(report, refOrSchema) {
@@ -791,6 +810,9 @@
791
810
  return deepClone(refOrSchema, this.validator.options.maxRecursionDepth);
792
811
  }
793
812
  fromCache(path) {
813
+ if (path === '__proto__' || path === 'constructor' || path === 'prototype') {
814
+ return undefined;
815
+ }
794
816
  let found = this.cache[path];
795
817
  if (found) {
796
818
  return found;
@@ -822,7 +844,7 @@
822
844
  }
823
845
  }
824
846
  }
825
- const remotePath = getRemotePath(uri);
847
+ const remotePath = getSafeRemotePath(uri);
826
848
  const queryPath = getQueryPath(uri);
827
849
  let result;
828
850
  let resolvedFromAncestor = false;
@@ -4277,7 +4299,9 @@
4277
4299
  }
4278
4300
  const datePart = parts[0];
4279
4301
  const timePart = parts.length === 2 ? parts[1] : undefined;
4280
- if (!/^(?:\d+Y)?(?:\d+M)?(?:\d+D)?$/.test(datePart)) {
4302
+ // RFC 3339 Appendix A ABNF: dur-year = 1*DIGIT "Y" [dur-month], dur-month = 1*DIGIT "M" [dur-day]
4303
+ // Y can only be followed by M (not D directly), so P1Y2D is invalid
4304
+ if (datePart.length > 0 && !/^(?:\d+Y(?:\d+M(?:\d+D)?)?|\d+M(?:\d+D)?|\d+D)$/.test(datePart)) {
4281
4305
  return false;
4282
4306
  }
4283
4307
  const hasDateComponent = /\d+[YMD]/.test(datePart);
@@ -4286,7 +4310,9 @@
4286
4310
  if (timePart.length === 0) {
4287
4311
  return false;
4288
4312
  }
4289
- if (!/^(?:\d+H)?(?:\d+M)?(?:\d+S)?$/.test(timePart)) {
4313
+ // RFC 3339 Appendix A ABNF: dur-hour = 1*DIGIT "H" [dur-minute], dur-minute = 1*DIGIT "M" [dur-second]
4314
+ // H can only be followed by M (not S directly), so PT1H2S is invalid
4315
+ if (!/^(?:\d+H(?:\d+M(?:\d+S)?)?|\d+M(?:\d+S)?|\d+S)$/.test(timePart)) {
4290
4316
  return false;
4291
4317
  }
4292
4318
  hasTimeComponent = /\d+[HMS]/.test(timePart);
@@ -4303,13 +4329,25 @@
4303
4329
  return /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(input);
4304
4330
  };
4305
4331
  const strictUriValidator = (uri) => typeof uri !== 'string' || isURLModule.default(uri);
4332
+ const hasValidPercentEncoding = (str) => {
4333
+ for (let i = 0; i < str.length; i++) {
4334
+ if (str[i] === '%') {
4335
+ if (i + 2 >= str.length || !/[0-9a-fA-F]/.test(str[i + 1]) || !/[0-9a-fA-F]/.test(str[i + 2])) {
4336
+ return false;
4337
+ }
4338
+ }
4339
+ }
4340
+ return true;
4341
+ };
4306
4342
  const uriValidator = function (uri) {
4307
4343
  if (typeof uri !== 'string')
4308
4344
  return true;
4309
4345
  // eslint-disable-next-line no-control-regex
4310
4346
  if (/[^\x00-\x7F]/.test(uri))
4311
4347
  return false;
4312
- const match = uri.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^/]*)/);
4348
+ if (!hasValidPercentEncoding(uri))
4349
+ return false;
4350
+ const match = uri.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^/?#]*)/);
4313
4351
  if (match) {
4314
4352
  const authority = match[2];
4315
4353
  const atIndex = authority.indexOf('@');
@@ -4319,6 +4357,21 @@
4319
4357
  return false;
4320
4358
  }
4321
4359
  }
4360
+ // Validate port: must be numeric
4361
+ let hostPort = atIndex >= 0 ? authority.substring(atIndex + 1) : authority;
4362
+ if (hostPort.startsWith('[')) {
4363
+ const bracketEnd = hostPort.indexOf(']');
4364
+ if (bracketEnd >= 0) {
4365
+ hostPort = hostPort.substring(bracketEnd + 1);
4366
+ }
4367
+ }
4368
+ const colonIndex = hostPort.lastIndexOf(':');
4369
+ if (colonIndex >= 0) {
4370
+ const port = hostPort.substring(colonIndex + 1);
4371
+ if (port.length > 0 && !/^\d+$/.test(port)) {
4372
+ return false;
4373
+ }
4374
+ }
4322
4375
  }
4323
4376
  return /^[a-zA-Z][a-zA-Z0-9+.-]*:[^"\\<>^{}^`| ]*$/.test(uri);
4324
4377
  };
@@ -4525,6 +4578,7 @@
4525
4578
  if (needsUnicode) {
4526
4579
  // Try compiling with 'u' flag only
4527
4580
  try {
4581
+ // lgtm[js/regex-injection] JSON Schema `pattern` is intentionally regex syntax and constrained by MAX_SCHEMA_REGEX_LENGTH.
4528
4582
  const re = new RegExp(pattern, 'u');
4529
4583
  return { ok: true, value: re };
4530
4584
  }
@@ -4540,6 +4594,7 @@
4540
4594
  }
4541
4595
  else {
4542
4596
  try {
4597
+ // lgtm[js/regex-injection] JSON Schema `pattern` is intentionally regex syntax and constrained by MAX_SCHEMA_REGEX_LENGTH.
4543
4598
  const re = new RegExp(pattern);
4544
4599
  return { ok: true, value: re };
4545
4600
  }
@@ -5522,21 +5577,13 @@
5522
5577
  const result = formatValidatorFn.call(this, json);
5523
5578
  if (result instanceof Promise) {
5524
5579
  // Promise-based async
5525
- const timeoutMs = Math.min(Math.max(this.options.asyncTimeout || 2000, 0), MAX_ASYNC_TIMEOUT);
5526
5580
  const promiseResult = result;
5527
5581
  report.addAsyncTaskWithPath(async (callback) => {
5528
5582
  try {
5529
- const timeoutPromise = new Promise((_, reject) => {
5530
- setTimeout(() => reject(new Error('Async timeout')), timeoutMs);
5531
- });
5532
- const resolved = await Promise.race([promiseResult, timeoutPromise]);
5583
+ const resolved = await promiseResult;
5533
5584
  callback(resolved);
5534
5585
  }
5535
- catch (error) {
5536
- if (error.message === 'Async timeout') {
5537
- // Don't call callback, let global timeout handle it
5538
- return;
5539
- }
5586
+ catch (_error) {
5540
5587
  callback(false);
5541
5588
  }
5542
5589
  }, [], function (resolvedResult) {
@@ -6386,6 +6433,14 @@
6386
6433
 
6387
6434
  /** Safely assign a property on `obj`, refusing prototype-polluting keys. */
6388
6435
  function safeSetProperty(obj, key, value) {
6436
+ const unsafeTargets = [
6437
+ Object.prototype,
6438
+ Function.prototype,
6439
+ Array.prototype,
6440
+ ];
6441
+ if (unsafeTargets.includes(obj)) {
6442
+ return;
6443
+ }
6389
6444
  /** Reject property names that could pollute Object.prototype (CWE-1321). */
6390
6445
  if (key !== '__proto__' && key !== 'constructor' && key !== 'prototype') {
6391
6446
  obj[key] = value;
@@ -6628,8 +6683,14 @@
6628
6683
  this.collectAndCacheIds(schema);
6629
6684
  }
6630
6685
  }
6686
+ const canMutateSchemaObject = schema !== Object.prototype &&
6687
+ schema !== Function.prototype &&
6688
+ schema !== Array.prototype;
6631
6689
  // if we have an id than it should be cached already (if this instance has compiled it)
6632
- if (schema.__$compiled && schema.id && this.validator.scache.checkCacheForUri(schema.id) === false) {
6690
+ if (canMutateSchemaObject &&
6691
+ schema.__$compiled &&
6692
+ schema.id &&
6693
+ this.validator.scache.checkCacheForUri(schema.id) === false) {
6633
6694
  schema.__$compiled = undefined;
6634
6695
  }
6635
6696
  // do not re-compile schemas
@@ -6637,7 +6698,7 @@
6637
6698
  return true;
6638
6699
  }
6639
6700
  // v8 - if $schema is not present, set $schema to default
6640
- if (!schema.$schema && this.validator.options.version !== 'none') {
6701
+ if (canMutateSchemaObject && !schema.$schema && this.validator.options.version !== 'none') {
6641
6702
  schema.$schema = this.validator.getDefaultSchemaId();
6642
6703
  }
6643
6704
  if (schema.id && typeof schema.id === 'string' && !options?.noCache) {
@@ -6652,7 +6713,9 @@
6652
6713
  }
6653
6714
  // delete all __$missingReferences from previous compilation attempts
6654
6715
  const isValidExceptReferences = report.isValid();
6655
- delete schema.__$missingReferences;
6716
+ if (canMutateSchemaObject) {
6717
+ delete schema.__$missingReferences;
6718
+ }
6656
6719
  // collect all references that need to be resolved - $ref and $schema
6657
6720
  const useRefObjectScope = this.validator.options.version === 'draft2019-09' || this.validator.options.version === 'draft2020-12';
6658
6721
  const refs = collectReferences(schema, undefined, undefined, undefined, { useRefObjectScope }, this.validator.options.maxRecursionDepth);
@@ -6700,7 +6763,7 @@
6700
6763
  report.addError('UNRESOLVABLE_REFERENCE', [refObj.ref]);
6701
6764
  report.path = report.path.slice(0, -refObj.path.length);
6702
6765
  // pusblish unresolved references out
6703
- if (isValidExceptReferences) {
6766
+ if (isValidExceptReferences && canMutateSchemaObject) {
6704
6767
  schema.__$missingReferences = schema.__$missingReferences || [];
6705
6768
  schema.__$missingReferences.push(refObj);
6706
6769
  }
@@ -6710,7 +6773,7 @@
6710
6773
  safeSetProperty(refObj.obj, `__${refObj.key}Resolved`, response);
6711
6774
  }
6712
6775
  const isValid = report.isValid();
6713
- if (isValid) {
6776
+ if (isValid && canMutateSchemaObject) {
6714
6777
  schema.__$compiled = true;
6715
6778
  }
6716
6779
  // else {