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 +91 -28
- package/dist/format-validators.js +34 -3
- package/dist/report.js +11 -3
- package/dist/schema-cache.js +21 -7
- package/dist/schema-compiler.js +21 -5
- package/dist/utils/schema-regex.js +2 -0
- package/dist/validation/string.js +2 -11
- package/package.json +3 -3
- package/src/format-validators.ts +34 -3
- package/src/report.ts +12 -3
- package/src/schema-cache.ts +22 -7
- package/src/schema-compiler.ts +25 -5
- package/src/utils/schema-regex.ts +2 -0
- package/src/validation/string.ts +2 -11
- package/umd/ZSchema.js +91 -28
- package/umd/ZSchema.min.js +1 -1
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
|
-
|
|
470
|
-
if (tasksCount
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
if (tasksCount
|
|
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
|
-
|
|
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 = [];
|
package/dist/schema-cache.js
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
121
|
+
const remotePath = getSafeRemotePath(uri);
|
|
108
122
|
const queryPath = getQueryPath(uri);
|
|
109
123
|
let result;
|
|
110
124
|
let resolvedFromAncestor = false;
|
package/dist/schema-compiler.js
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|