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.
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;
@@ -4275,7 +4297,9 @@ const durationValidator = (input) => {
4275
4297
  }
4276
4298
  const datePart = parts[0];
4277
4299
  const timePart = parts.length === 2 ? parts[1] : undefined;
4278
- if (!/^(?:\d+Y)?(?:\d+M)?(?:\d+D)?$/.test(datePart)) {
4300
+ // RFC 3339 Appendix A ABNF: dur-year = 1*DIGIT "Y" [dur-month], dur-month = 1*DIGIT "M" [dur-day]
4301
+ // Y can only be followed by M (not D directly), so P1Y2D is invalid
4302
+ if (datePart.length > 0 && !/^(?:\d+Y(?:\d+M(?:\d+D)?)?|\d+M(?:\d+D)?|\d+D)$/.test(datePart)) {
4279
4303
  return false;
4280
4304
  }
4281
4305
  const hasDateComponent = /\d+[YMD]/.test(datePart);
@@ -4284,7 +4308,9 @@ const durationValidator = (input) => {
4284
4308
  if (timePart.length === 0) {
4285
4309
  return false;
4286
4310
  }
4287
- if (!/^(?:\d+H)?(?:\d+M)?(?:\d+S)?$/.test(timePart)) {
4311
+ // RFC 3339 Appendix A ABNF: dur-hour = 1*DIGIT "H" [dur-minute], dur-minute = 1*DIGIT "M" [dur-second]
4312
+ // H can only be followed by M (not S directly), so PT1H2S is invalid
4313
+ if (!/^(?:\d+H(?:\d+M(?:\d+S)?)?|\d+M(?:\d+S)?|\d+S)$/.test(timePart)) {
4288
4314
  return false;
4289
4315
  }
4290
4316
  hasTimeComponent = /\d+[HMS]/.test(timePart);
@@ -4301,13 +4327,25 @@ const uuidValidator = (input) => {
4301
4327
  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);
4302
4328
  };
4303
4329
  const strictUriValidator = (uri) => typeof uri !== 'string' || isURLModule.default(uri);
4330
+ const hasValidPercentEncoding = (str) => {
4331
+ for (let i = 0; i < str.length; i++) {
4332
+ if (str[i] === '%') {
4333
+ if (i + 2 >= str.length || !/[0-9a-fA-F]/.test(str[i + 1]) || !/[0-9a-fA-F]/.test(str[i + 2])) {
4334
+ return false;
4335
+ }
4336
+ }
4337
+ }
4338
+ return true;
4339
+ };
4304
4340
  const uriValidator = function (uri) {
4305
4341
  if (typeof uri !== 'string')
4306
4342
  return true;
4307
4343
  // eslint-disable-next-line no-control-regex
4308
4344
  if (/[^\x00-\x7F]/.test(uri))
4309
4345
  return false;
4310
- const match = uri.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^/]*)/);
4346
+ if (!hasValidPercentEncoding(uri))
4347
+ return false;
4348
+ const match = uri.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^/?#]*)/);
4311
4349
  if (match) {
4312
4350
  const authority = match[2];
4313
4351
  const atIndex = authority.indexOf('@');
@@ -4317,6 +4355,21 @@ const uriValidator = function (uri) {
4317
4355
  return false;
4318
4356
  }
4319
4357
  }
4358
+ // Validate port: must be numeric
4359
+ let hostPort = atIndex >= 0 ? authority.substring(atIndex + 1) : authority;
4360
+ if (hostPort.startsWith('[')) {
4361
+ const bracketEnd = hostPort.indexOf(']');
4362
+ if (bracketEnd >= 0) {
4363
+ hostPort = hostPort.substring(bracketEnd + 1);
4364
+ }
4365
+ }
4366
+ const colonIndex = hostPort.lastIndexOf(':');
4367
+ if (colonIndex >= 0) {
4368
+ const port = hostPort.substring(colonIndex + 1);
4369
+ if (port.length > 0 && !/^\d+$/.test(port)) {
4370
+ return false;
4371
+ }
4372
+ }
4320
4373
  }
4321
4374
  return /^[a-zA-Z][a-zA-Z0-9+.-]*:[^"\\<>^{}^`| ]*$/.test(uri);
4322
4375
  };
@@ -4523,6 +4576,7 @@ function compileSchemaRegex(pattern) {
4523
4576
  if (needsUnicode) {
4524
4577
  // Try compiling with 'u' flag only
4525
4578
  try {
4579
+ // lgtm[js/regex-injection] JSON Schema `pattern` is intentionally regex syntax and constrained by MAX_SCHEMA_REGEX_LENGTH.
4526
4580
  const re = new RegExp(pattern, 'u');
4527
4581
  return { ok: true, value: re };
4528
4582
  }
@@ -4538,6 +4592,7 @@ function compileSchemaRegex(pattern) {
4538
4592
  }
4539
4593
  else {
4540
4594
  try {
4595
+ // lgtm[js/regex-injection] JSON Schema `pattern` is intentionally regex syntax and constrained by MAX_SCHEMA_REGEX_LENGTH.
4541
4596
  const re = new RegExp(pattern);
4542
4597
  return { ok: true, value: re };
4543
4598
  }
@@ -5520,21 +5575,13 @@ function formatValidator(report, schema, json) {
5520
5575
  const result = formatValidatorFn.call(this, json);
5521
5576
  if (result instanceof Promise) {
5522
5577
  // Promise-based async
5523
- const timeoutMs = Math.min(Math.max(this.options.asyncTimeout || 2000, 0), MAX_ASYNC_TIMEOUT);
5524
5578
  const promiseResult = result;
5525
5579
  report.addAsyncTaskWithPath(async (callback) => {
5526
5580
  try {
5527
- const timeoutPromise = new Promise((_, reject) => {
5528
- setTimeout(() => reject(new Error('Async timeout')), timeoutMs);
5529
- });
5530
- const resolved = await Promise.race([promiseResult, timeoutPromise]);
5581
+ const resolved = await promiseResult;
5531
5582
  callback(resolved);
5532
5583
  }
5533
- catch (error) {
5534
- if (error.message === 'Async timeout') {
5535
- // Don't call callback, let global timeout handle it
5536
- return;
5537
- }
5584
+ catch (_error) {
5538
5585
  callback(false);
5539
5586
  }
5540
5587
  }, [], function (resolvedResult) {
@@ -6384,6 +6431,14 @@ function setSchemaReader(schemaReader) {
6384
6431
 
6385
6432
  /** Safely assign a property on `obj`, refusing prototype-polluting keys. */
6386
6433
  function safeSetProperty(obj, key, value) {
6434
+ const unsafeTargets = [
6435
+ Object.prototype,
6436
+ Function.prototype,
6437
+ Array.prototype,
6438
+ ];
6439
+ if (unsafeTargets.includes(obj)) {
6440
+ return;
6441
+ }
6387
6442
  /** Reject property names that could pollute Object.prototype (CWE-1321). */
6388
6443
  if (key !== '__proto__' && key !== 'constructor' && key !== 'prototype') {
6389
6444
  obj[key] = value;
@@ -6626,8 +6681,14 @@ class SchemaCompiler {
6626
6681
  this.collectAndCacheIds(schema);
6627
6682
  }
6628
6683
  }
6684
+ const canMutateSchemaObject = schema !== Object.prototype &&
6685
+ schema !== Function.prototype &&
6686
+ schema !== Array.prototype;
6629
6687
  // 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) {
6688
+ if (canMutateSchemaObject &&
6689
+ schema.__$compiled &&
6690
+ schema.id &&
6691
+ this.validator.scache.checkCacheForUri(schema.id) === false) {
6631
6692
  schema.__$compiled = undefined;
6632
6693
  }
6633
6694
  // do not re-compile schemas
@@ -6635,7 +6696,7 @@ class SchemaCompiler {
6635
6696
  return true;
6636
6697
  }
6637
6698
  // v8 - if $schema is not present, set $schema to default
6638
- if (!schema.$schema && this.validator.options.version !== 'none') {
6699
+ if (canMutateSchemaObject && !schema.$schema && this.validator.options.version !== 'none') {
6639
6700
  schema.$schema = this.validator.getDefaultSchemaId();
6640
6701
  }
6641
6702
  if (schema.id && typeof schema.id === 'string' && !options?.noCache) {
@@ -6650,7 +6711,9 @@ class SchemaCompiler {
6650
6711
  }
6651
6712
  // delete all __$missingReferences from previous compilation attempts
6652
6713
  const isValidExceptReferences = report.isValid();
6653
- delete schema.__$missingReferences;
6714
+ if (canMutateSchemaObject) {
6715
+ delete schema.__$missingReferences;
6716
+ }
6654
6717
  // collect all references that need to be resolved - $ref and $schema
6655
6718
  const useRefObjectScope = this.validator.options.version === 'draft2019-09' || this.validator.options.version === 'draft2020-12';
6656
6719
  const refs = collectReferences(schema, undefined, undefined, undefined, { useRefObjectScope }, this.validator.options.maxRecursionDepth);
@@ -6698,7 +6761,7 @@ class SchemaCompiler {
6698
6761
  report.addError('UNRESOLVABLE_REFERENCE', [refObj.ref]);
6699
6762
  report.path = report.path.slice(0, -refObj.path.length);
6700
6763
  // pusblish unresolved references out
6701
- if (isValidExceptReferences) {
6764
+ if (isValidExceptReferences && canMutateSchemaObject) {
6702
6765
  schema.__$missingReferences = schema.__$missingReferences || [];
6703
6766
  schema.__$missingReferences.push(refObj);
6704
6767
  }
@@ -6708,7 +6771,7 @@ class SchemaCompiler {
6708
6771
  safeSetProperty(refObj.obj, `__${refObj.key}Resolved`, response);
6709
6772
  }
6710
6773
  const isValid = report.isValid();
6711
- if (isValid) {
6774
+ if (isValid && canMutateSchemaObject) {
6712
6775
  schema.__$compiled = true;
6713
6776
  }
6714
6777
  // else {
@@ -132,7 +132,9 @@ const durationValidator = (input) => {
132
132
  }
133
133
  const datePart = parts[0];
134
134
  const timePart = parts.length === 2 ? parts[1] : undefined;
135
- if (!/^(?:\d+Y)?(?:\d+M)?(?:\d+D)?$/.test(datePart)) {
135
+ // RFC 3339 Appendix A ABNF: dur-year = 1*DIGIT "Y" [dur-month], dur-month = 1*DIGIT "M" [dur-day]
136
+ // Y can only be followed by M (not D directly), so P1Y2D is invalid
137
+ if (datePart.length > 0 && !/^(?:\d+Y(?:\d+M(?:\d+D)?)?|\d+M(?:\d+D)?|\d+D)$/.test(datePart)) {
136
138
  return false;
137
139
  }
138
140
  const hasDateComponent = /\d+[YMD]/.test(datePart);
@@ -141,7 +143,9 @@ const durationValidator = (input) => {
141
143
  if (timePart.length === 0) {
142
144
  return false;
143
145
  }
144
- if (!/^(?:\d+H)?(?:\d+M)?(?:\d+S)?$/.test(timePart)) {
146
+ // RFC 3339 Appendix A ABNF: dur-hour = 1*DIGIT "H" [dur-minute], dur-minute = 1*DIGIT "M" [dur-second]
147
+ // H can only be followed by M (not S directly), so PT1H2S is invalid
148
+ if (!/^(?:\d+H(?:\d+M(?:\d+S)?)?|\d+M(?:\d+S)?|\d+S)$/.test(timePart)) {
145
149
  return false;
146
150
  }
147
151
  hasTimeComponent = /\d+[HMS]/.test(timePart);
@@ -158,13 +162,25 @@ const uuidValidator = (input) => {
158
162
  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);
159
163
  };
160
164
  const strictUriValidator = (uri) => typeof uri !== 'string' || isURLModule.default(uri);
165
+ const hasValidPercentEncoding = (str) => {
166
+ for (let i = 0; i < str.length; i++) {
167
+ if (str[i] === '%') {
168
+ if (i + 2 >= str.length || !/[0-9a-fA-F]/.test(str[i + 1]) || !/[0-9a-fA-F]/.test(str[i + 2])) {
169
+ return false;
170
+ }
171
+ }
172
+ }
173
+ return true;
174
+ };
161
175
  const uriValidator = function (uri) {
162
176
  if (typeof uri !== 'string')
163
177
  return true;
164
178
  // eslint-disable-next-line no-control-regex
165
179
  if (/[^\x00-\x7F]/.test(uri))
166
180
  return false;
167
- const match = uri.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^/]*)/);
181
+ if (!hasValidPercentEncoding(uri))
182
+ return false;
183
+ const match = uri.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^/?#]*)/);
168
184
  if (match) {
169
185
  const authority = match[2];
170
186
  const atIndex = authority.indexOf('@');
@@ -174,6 +190,21 @@ const uriValidator = function (uri) {
174
190
  return false;
175
191
  }
176
192
  }
193
+ // Validate port: must be numeric
194
+ let hostPort = atIndex >= 0 ? authority.substring(atIndex + 1) : authority;
195
+ if (hostPort.startsWith('[')) {
196
+ const bracketEnd = hostPort.indexOf(']');
197
+ if (bracketEnd >= 0) {
198
+ hostPort = hostPort.substring(bracketEnd + 1);
199
+ }
200
+ }
201
+ const colonIndex = hostPort.lastIndexOf(':');
202
+ if (colonIndex >= 0) {
203
+ const port = hostPort.substring(colonIndex + 1);
204
+ if (port.length > 0 && !/^\d+$/.test(port)) {
205
+ return false;
206
+ }
207
+ }
177
208
  }
178
209
  return /^[a-zA-Z][a-zA-Z0-9+.-]*:[^"\\<>^{}^`| ]*$/.test(uri);
179
210
  };
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.3",
4
4
  "engines": {
5
5
  "node": ">=22.0.0"
6
6
  },
@@ -97,7 +97,7 @@
97
97
  "@rollup/plugin-commonjs": "^29.0.0",
98
98
  "@rollup/plugin-json": "^6.1.0",
99
99
  "@rollup/plugin-node-resolve": "^16.0.3",
100
- "@rollup/plugin-terser": "^0.4.4",
100
+ "@rollup/plugin-terser": "^1.0.0",
101
101
  "@rollup/plugin-typescript": "^12.3.0",
102
102
  "@types/lodash.isequal": "^4.5.8",
103
103
  "@types/node": "^25.2.0",
@@ -115,7 +115,7 @@
115
115
  "lodash.isequal": "^4.5.0",
116
116
  "prettier": "^3.8.1",
117
117
  "rimraf": "^6.1.2",
118
- "rollup": "^4.57.1",
118
+ "rollup": "^4.59.0",
119
119
  "rollup-plugin-dts": "^6.3.0",
120
120
  "tslib": "^2.8.1",
121
121
  "typescript": "^5.9.3",