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 +57 -25
- 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 +1 -1
- 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 +57 -25
- 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;
|
|
@@ -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
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
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
|
-
|
|
197
|
-
if (tasksCount
|
|
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
|
-
|
|
209
|
+
setTimeout(checkTimeout, ASYNC_TIMEOUT_POLL_MS);
|
|
210
|
+
};
|
|
211
|
+
setTimeout(checkTimeout, ASYNC_TIMEOUT_POLL_MS);
|
|
203
212
|
}
|
|
204
213
|
|
|
205
214
|
getPath(returnPathAsString?: boolean) {
|
package/src/schema-cache.ts
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
148
|
+
const remotePath = getSafeRemotePath(uri);
|
|
134
149
|
const queryPath = getQueryPath(uri);
|
|
135
150
|
let result: JsonSchemaInternal | undefined;
|
|
136
151
|
let resolvedFromAncestor = false;
|
package/src/schema-compiler.ts
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
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) {
|