z-schema 12.0.0 → 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/README.md +12 -5
- package/bin/z-schema +28 -1
- package/cjs/index.d.ts +8 -0
- package/cjs/index.js +330 -212
- package/dist/format-validators.js +38 -2
- package/dist/json-schema.js +1 -1
- package/dist/report.js +13 -4
- package/dist/schema-cache.js +21 -7
- package/dist/schema-compiler.js +33 -9
- package/dist/types/utils/constants.d.ts +19 -0
- package/dist/types/z-schema-base.d.ts +10 -1
- package/dist/types/z-schema-options.d.ts +2 -1
- package/dist/utils/clone.js +1 -1
- package/dist/utils/constants.js +19 -0
- package/dist/utils/json.js +1 -1
- package/dist/utils/schema-regex.js +12 -0
- package/dist/validation/array.js +1 -2
- package/dist/validation/combinators.js +6 -7
- package/dist/validation/object.js +3 -4
- package/dist/validation/string.js +2 -10
- package/dist/z-schema-base.js +10 -0
- package/dist/z-schema-options.js +6 -1
- package/package.json +3 -1
- package/src/format-validators.ts +31 -2
- package/src/json-schema.ts +1 -1
- package/src/report.ts +14 -4
- package/src/schema-cache.ts +22 -7
- package/src/schema-compiler.ts +38 -9
- package/src/utils/clone.ts +1 -1
- package/src/utils/constants.ts +21 -0
- package/src/utils/json.ts +1 -1
- package/src/utils/schema-regex.ts +14 -0
- package/src/validation/array.ts +1 -2
- package/src/validation/combinators.ts +6 -7
- package/src/validation/object.ts +3 -4
- package/src/validation/string.ts +2 -10
- package/src/z-schema-base.ts +11 -0
- package/src/z-schema-options.ts +7 -1
- package/umd/ZSchema.js +330 -212
- package/umd/ZSchema.min.js +1 -1
package/umd/ZSchema.js
CHANGED
|
@@ -17,6 +17,26 @@
|
|
|
17
17
|
};
|
|
18
18
|
const isAbsoluteUri = (uri) => /^[a-zA-Z][a-zA-Z\d+.-]*:/.test(uri);
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Default maximum recursion depth for deeply nested schema/data traversal.
|
|
22
|
+
* Used as the default for {@link ZSchemaOptions.maxRecursionDepth} and
|
|
23
|
+
* internal helpers like `deepClone` and `collectIds`.
|
|
24
|
+
*/
|
|
25
|
+
const DEFAULT_MAX_RECURSION_DEPTH = 100;
|
|
26
|
+
/**
|
|
27
|
+
* Maximum allowed value for {@link ZSchemaOptions.asyncTimeout} in milliseconds.
|
|
28
|
+
* Values exceeding this limit are clamped during option normalization to
|
|
29
|
+
* prevent resource exhaustion (CWE-400).
|
|
30
|
+
*/
|
|
31
|
+
const MAX_ASYNC_TIMEOUT = 60_000;
|
|
32
|
+
/**
|
|
33
|
+
* Maximum allowed length for a JSON Schema `pattern` regular expression string.
|
|
34
|
+
* Patterns exceeding this limit are rejected by {@link compileSchemaRegex} to
|
|
35
|
+
* mitigate Regular Expression Denial-of-Service (CWE-1333) and regex injection
|
|
36
|
+
* (CWE-95).
|
|
37
|
+
*/
|
|
38
|
+
const MAX_SCHEMA_REGEX_LENGTH = 10_000;
|
|
39
|
+
|
|
20
40
|
const whatIs = (what) => {
|
|
21
41
|
if (typeof what === 'object') {
|
|
22
42
|
if (what === null) {
|
|
@@ -50,177 +70,6 @@
|
|
|
50
70
|
return typeof value === 'number' && Number.isFinite(value) && value % 1 === 0;
|
|
51
71
|
}
|
|
52
72
|
|
|
53
|
-
const CURRENT_DEFAULT_SCHEMA_VERSION = 'draft2020-12';
|
|
54
|
-
const VERSION_SCHEMA_URL_MAPPING = {
|
|
55
|
-
'draft-04': 'http://json-schema.org/draft-04/schema#',
|
|
56
|
-
'draft-06': 'http://json-schema.org/draft-06/schema#',
|
|
57
|
-
'draft-07': 'http://json-schema.org/draft-07/schema#',
|
|
58
|
-
'draft2019-09': 'https://json-schema.org/draft/2019-09/schema',
|
|
59
|
-
'draft2020-12': 'https://json-schema.org/draft/2020-12/schema',
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
function copyProp(from, to, key, fn) {
|
|
63
|
-
if (Object.hasOwn(from, key)) {
|
|
64
|
-
Object.defineProperty(to, key, {
|
|
65
|
-
value: fn ? fn(from[key]) : from[key],
|
|
66
|
-
enumerable: true,
|
|
67
|
-
writable: true,
|
|
68
|
-
configurable: true,
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const shallowClone = (src) => {
|
|
74
|
-
if (src == null || typeof src !== 'object') {
|
|
75
|
-
return src;
|
|
76
|
-
}
|
|
77
|
-
let res;
|
|
78
|
-
if (Array.isArray(src)) {
|
|
79
|
-
res = [];
|
|
80
|
-
for (let i = 0; i < src.length; i++) {
|
|
81
|
-
res[i] = src[i];
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
else {
|
|
85
|
-
res = {};
|
|
86
|
-
const keys = Object.keys(src).sort();
|
|
87
|
-
for (const key of keys) {
|
|
88
|
-
copyProp(src, res, key);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return res;
|
|
92
|
-
};
|
|
93
|
-
const deepClone = (src, maxDepth = DEFAULT_MAX_RECURSION_DEPTH) => {
|
|
94
|
-
let vidx = 0;
|
|
95
|
-
const visited = new Map();
|
|
96
|
-
const cloned = [];
|
|
97
|
-
const cloneDeepInner = (src, _depth) => {
|
|
98
|
-
if (typeof src !== 'object' || src === null) {
|
|
99
|
-
return src;
|
|
100
|
-
}
|
|
101
|
-
if (_depth >= maxDepth) {
|
|
102
|
-
throw new Error(`Maximum recursion depth (${maxDepth}) exceeded in deepClone. ` +
|
|
103
|
-
'If your schema or data is deeply nested and valid, increase the maxRecursionDepth option.');
|
|
104
|
-
}
|
|
105
|
-
let res;
|
|
106
|
-
const cidx = visited.get(src);
|
|
107
|
-
if (cidx !== undefined) {
|
|
108
|
-
return cloned[cidx];
|
|
109
|
-
}
|
|
110
|
-
visited.set(src, vidx++);
|
|
111
|
-
if (Array.isArray(src)) {
|
|
112
|
-
res = [];
|
|
113
|
-
cloned.push(res);
|
|
114
|
-
for (let i = 0; i < src.length; i++) {
|
|
115
|
-
res[i] = cloneDeepInner(src[i], _depth + 1);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
else {
|
|
119
|
-
res = {};
|
|
120
|
-
cloned.push(res);
|
|
121
|
-
const keys = Object.keys(src).sort();
|
|
122
|
-
for (const key of keys) {
|
|
123
|
-
copyProp(src, res, key, (v) => cloneDeepInner(v, _depth + 1));
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
return res;
|
|
127
|
-
};
|
|
128
|
-
return cloneDeepInner(src, 0);
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
const DEFAULT_MAX_RECURSION_DEPTH = 100;
|
|
132
|
-
const defaultOptions = {
|
|
133
|
-
// default version to validate against
|
|
134
|
-
version: CURRENT_DEFAULT_SCHEMA_VERSION,
|
|
135
|
-
// default timeout for all async tasks
|
|
136
|
-
asyncTimeout: 2000,
|
|
137
|
-
// force additionalProperties and additionalItems to be defined on "object" and "array" types
|
|
138
|
-
forceAdditional: false,
|
|
139
|
-
// assume additionalProperties and additionalItems are defined as "false" where appropriate
|
|
140
|
-
assumeAdditional: false,
|
|
141
|
-
// do case insensitive comparison for enums
|
|
142
|
-
enumCaseInsensitiveComparison: false,
|
|
143
|
-
// force items to be defined on "array" types
|
|
144
|
-
forceItems: false,
|
|
145
|
-
// force minItems to be defined on "array" types
|
|
146
|
-
forceMinItems: false,
|
|
147
|
-
// force maxItems to be defined on "array" types
|
|
148
|
-
forceMaxItems: false,
|
|
149
|
-
// force minLength to be defined on "string" types
|
|
150
|
-
forceMinLength: false,
|
|
151
|
-
// force maxLength to be defined on "string" types
|
|
152
|
-
forceMaxLength: false,
|
|
153
|
-
// force properties or patternProperties to be defined on "object" types
|
|
154
|
-
forceProperties: false,
|
|
155
|
-
// ignore references that cannot be resolved (remote schemas)
|
|
156
|
-
ignoreUnresolvableReferences: false,
|
|
157
|
-
// disallow usage of keywords that this validator can't handle
|
|
158
|
-
noExtraKeywords: false,
|
|
159
|
-
// disallow usage of schema's without "type" defined
|
|
160
|
-
noTypeless: false,
|
|
161
|
-
// disallow zero length strings in validated objects
|
|
162
|
-
noEmptyStrings: false,
|
|
163
|
-
// disallow zero length arrays in validated objects
|
|
164
|
-
noEmptyArrays: false,
|
|
165
|
-
// forces "uri" format to be in fully rfc3986 compliant
|
|
166
|
-
strictUris: false,
|
|
167
|
-
// turn on some of the above
|
|
168
|
-
strictMode: false,
|
|
169
|
-
// report error paths as an array of path segments to get to the offending node
|
|
170
|
-
reportPathAsArray: false,
|
|
171
|
-
// stop validation as soon as an error is found
|
|
172
|
-
breakOnFirstError: false,
|
|
173
|
-
// check if schema follows best practices and common sense
|
|
174
|
-
pedanticCheck: false,
|
|
175
|
-
// ignore unknown formats (do not report them as an error)
|
|
176
|
-
ignoreUnknownFormats: false,
|
|
177
|
-
// control format assertion behavior:
|
|
178
|
-
// true - respect vocabulary: for draft 2019-09/2020-12, check meta-schema $vocabulary
|
|
179
|
-
// to determine if format is assertion or annotation-only
|
|
180
|
-
// false - format is always annotation-only (never asserts)
|
|
181
|
-
// null - legacy behavior: always assert format validation
|
|
182
|
-
formatAssertions: null,
|
|
183
|
-
// function to be called on every schema
|
|
184
|
-
customValidator: null,
|
|
185
|
-
// maximum recursion depth for deeply nested schema/data traversal (prevents stack overflow)
|
|
186
|
-
maxRecursionDepth: DEFAULT_MAX_RECURSION_DEPTH,
|
|
187
|
-
};
|
|
188
|
-
const normalizeOptions = (options) => {
|
|
189
|
-
let normalized;
|
|
190
|
-
// options
|
|
191
|
-
if (typeof options === 'object') {
|
|
192
|
-
let keys = Object.keys(options);
|
|
193
|
-
// check that the options are correctly configured
|
|
194
|
-
for (const key of keys) {
|
|
195
|
-
if (defaultOptions[key] === undefined) {
|
|
196
|
-
throw new Error('Unexpected option passed to constructor: ' + key);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
// copy the default options into passed options
|
|
200
|
-
keys = Object.keys(defaultOptions);
|
|
201
|
-
for (const key of keys) {
|
|
202
|
-
if (options[key] === undefined) {
|
|
203
|
-
options[key] = shallowClone(defaultOptions[key]);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
normalized = options;
|
|
207
|
-
}
|
|
208
|
-
else {
|
|
209
|
-
normalized = shallowClone(defaultOptions);
|
|
210
|
-
}
|
|
211
|
-
if (normalized.strictMode === true) {
|
|
212
|
-
normalized.forceAdditional = true;
|
|
213
|
-
normalized.forceItems = true;
|
|
214
|
-
normalized.forceMaxLength = true;
|
|
215
|
-
normalized.forceProperties = true;
|
|
216
|
-
normalized.noExtraKeywords = true;
|
|
217
|
-
normalized.noTypeless = true;
|
|
218
|
-
normalized.noEmptyStrings = true;
|
|
219
|
-
normalized.noEmptyArrays = true;
|
|
220
|
-
}
|
|
221
|
-
return normalized;
|
|
222
|
-
};
|
|
223
|
-
|
|
224
73
|
/**
|
|
225
74
|
* Keywords whose values are not JSON Schema sub-schemas and must not be
|
|
226
75
|
* traversed during schema walking (id collection, reference collection, etc.).
|
|
@@ -376,6 +225,75 @@
|
|
|
376
225
|
return new ValidateError(message || '', details);
|
|
377
226
|
}
|
|
378
227
|
|
|
228
|
+
function copyProp(from, to, key, fn) {
|
|
229
|
+
if (Object.hasOwn(from, key)) {
|
|
230
|
+
Object.defineProperty(to, key, {
|
|
231
|
+
value: fn ? fn(from[key]) : from[key],
|
|
232
|
+
enumerable: true,
|
|
233
|
+
writable: true,
|
|
234
|
+
configurable: true,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const shallowClone = (src) => {
|
|
240
|
+
if (src == null || typeof src !== 'object') {
|
|
241
|
+
return src;
|
|
242
|
+
}
|
|
243
|
+
let res;
|
|
244
|
+
if (Array.isArray(src)) {
|
|
245
|
+
res = [];
|
|
246
|
+
for (let i = 0; i < src.length; i++) {
|
|
247
|
+
res[i] = src[i];
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
res = {};
|
|
252
|
+
const keys = Object.keys(src).sort();
|
|
253
|
+
for (const key of keys) {
|
|
254
|
+
copyProp(src, res, key);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return res;
|
|
258
|
+
};
|
|
259
|
+
const deepClone = (src, maxDepth = DEFAULT_MAX_RECURSION_DEPTH) => {
|
|
260
|
+
let vidx = 0;
|
|
261
|
+
const visited = new Map();
|
|
262
|
+
const cloned = [];
|
|
263
|
+
const cloneDeepInner = (src, _depth) => {
|
|
264
|
+
if (typeof src !== 'object' || src === null) {
|
|
265
|
+
return src;
|
|
266
|
+
}
|
|
267
|
+
if (_depth >= maxDepth) {
|
|
268
|
+
throw new Error(`Maximum recursion depth (${maxDepth}) exceeded in deepClone. ` +
|
|
269
|
+
'If your schema or data is deeply nested and valid, increase the maxRecursionDepth option.');
|
|
270
|
+
}
|
|
271
|
+
let res;
|
|
272
|
+
const cidx = visited.get(src);
|
|
273
|
+
if (cidx !== undefined) {
|
|
274
|
+
return cloned[cidx];
|
|
275
|
+
}
|
|
276
|
+
visited.set(src, vidx++);
|
|
277
|
+
if (Array.isArray(src)) {
|
|
278
|
+
res = [];
|
|
279
|
+
cloned.push(res);
|
|
280
|
+
for (let i = 0; i < src.length; i++) {
|
|
281
|
+
res[i] = cloneDeepInner(src[i], _depth + 1);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
res = {};
|
|
286
|
+
cloned.push(res);
|
|
287
|
+
const keys = Object.keys(src).sort();
|
|
288
|
+
for (const key of keys) {
|
|
289
|
+
copyProp(src, res, key, (v) => cloneDeepInner(v, _depth + 1));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return res;
|
|
293
|
+
};
|
|
294
|
+
return cloneDeepInner(src, 0);
|
|
295
|
+
};
|
|
296
|
+
|
|
379
297
|
const areEqual = (json1, json2, options, _depth = 0) => {
|
|
380
298
|
const caseInsensitiveComparison = options?.caseInsensitiveComparison || false;
|
|
381
299
|
const maxDepth = options?.maxDepth ?? DEFAULT_MAX_RECURSION_DEPTH;
|
|
@@ -448,6 +366,7 @@
|
|
|
448
366
|
const jsonSymbol = Symbol.for('z-schema/json');
|
|
449
367
|
const schemaSymbol = Symbol.for('z-schema/schema');
|
|
450
368
|
|
|
369
|
+
const ASYNC_TIMEOUT_POLL_MS = 10;
|
|
451
370
|
class Report {
|
|
452
371
|
asyncTasks = [];
|
|
453
372
|
commonErrorMessage;
|
|
@@ -521,7 +440,8 @@
|
|
|
521
440
|
return this.parentReport.getAncestor(id);
|
|
522
441
|
}
|
|
523
442
|
processAsyncTasks(timeout, callback) {
|
|
524
|
-
const validationTimeout = timeout || 2000;
|
|
443
|
+
const validationTimeout = Math.min(Math.max(timeout || 2000, 0), MAX_ASYNC_TIMEOUT);
|
|
444
|
+
const timeoutAt = Date.now() + validationTimeout;
|
|
525
445
|
let tasksCount = this.asyncTasks.length;
|
|
526
446
|
let timedOut = false;
|
|
527
447
|
const finish = () => {
|
|
@@ -550,13 +470,19 @@
|
|
|
550
470
|
const respondCallback = respond(processFn);
|
|
551
471
|
fn(...fnArgs, respondCallback);
|
|
552
472
|
}
|
|
553
|
-
|
|
554
|
-
if (tasksCount
|
|
473
|
+
const checkTimeout = () => {
|
|
474
|
+
if (timedOut || tasksCount <= 0) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (Date.now() >= timeoutAt) {
|
|
555
478
|
timedOut = true;
|
|
556
479
|
this.addError('ASYNC_TIMEOUT', [tasksCount, validationTimeout]);
|
|
557
480
|
callback(getValidateError({ details: this.errors }), false);
|
|
481
|
+
return;
|
|
558
482
|
}
|
|
559
|
-
|
|
483
|
+
setTimeout(checkTimeout, ASYNC_TIMEOUT_POLL_MS);
|
|
484
|
+
};
|
|
485
|
+
setTimeout(checkTimeout, ASYNC_TIMEOUT_POLL_MS);
|
|
560
486
|
}
|
|
561
487
|
getPath(returnPathAsString) {
|
|
562
488
|
let path = [];
|
|
@@ -699,6 +625,122 @@
|
|
|
699
625
|
}
|
|
700
626
|
}
|
|
701
627
|
|
|
628
|
+
const CURRENT_DEFAULT_SCHEMA_VERSION = 'draft2020-12';
|
|
629
|
+
const VERSION_SCHEMA_URL_MAPPING = {
|
|
630
|
+
'draft-04': 'http://json-schema.org/draft-04/schema#',
|
|
631
|
+
'draft-06': 'http://json-schema.org/draft-06/schema#',
|
|
632
|
+
'draft-07': 'http://json-schema.org/draft-07/schema#',
|
|
633
|
+
'draft2019-09': 'https://json-schema.org/draft/2019-09/schema',
|
|
634
|
+
'draft2020-12': 'https://json-schema.org/draft/2020-12/schema',
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
const defaultOptions = {
|
|
638
|
+
// default version to validate against
|
|
639
|
+
version: CURRENT_DEFAULT_SCHEMA_VERSION,
|
|
640
|
+
// default timeout for all async tasks
|
|
641
|
+
asyncTimeout: 2000,
|
|
642
|
+
// force additionalProperties and additionalItems to be defined on "object" and "array" types
|
|
643
|
+
forceAdditional: false,
|
|
644
|
+
// assume additionalProperties and additionalItems are defined as "false" where appropriate
|
|
645
|
+
assumeAdditional: false,
|
|
646
|
+
// do case insensitive comparison for enums
|
|
647
|
+
enumCaseInsensitiveComparison: false,
|
|
648
|
+
// force items to be defined on "array" types
|
|
649
|
+
forceItems: false,
|
|
650
|
+
// force minItems to be defined on "array" types
|
|
651
|
+
forceMinItems: false,
|
|
652
|
+
// force maxItems to be defined on "array" types
|
|
653
|
+
forceMaxItems: false,
|
|
654
|
+
// force minLength to be defined on "string" types
|
|
655
|
+
forceMinLength: false,
|
|
656
|
+
// force maxLength to be defined on "string" types
|
|
657
|
+
forceMaxLength: false,
|
|
658
|
+
// force properties or patternProperties to be defined on "object" types
|
|
659
|
+
forceProperties: false,
|
|
660
|
+
// ignore references that cannot be resolved (remote schemas)
|
|
661
|
+
ignoreUnresolvableReferences: false,
|
|
662
|
+
// disallow usage of keywords that this validator can't handle
|
|
663
|
+
noExtraKeywords: false,
|
|
664
|
+
// disallow usage of schema's without "type" defined
|
|
665
|
+
noTypeless: false,
|
|
666
|
+
// disallow zero length strings in validated objects
|
|
667
|
+
noEmptyStrings: false,
|
|
668
|
+
// disallow zero length arrays in validated objects
|
|
669
|
+
noEmptyArrays: false,
|
|
670
|
+
// forces "uri" format to be in fully rfc3986 compliant
|
|
671
|
+
strictUris: false,
|
|
672
|
+
// turn on some of the above
|
|
673
|
+
strictMode: false,
|
|
674
|
+
// report error paths as an array of path segments to get to the offending node
|
|
675
|
+
reportPathAsArray: false,
|
|
676
|
+
// stop validation as soon as an error is found
|
|
677
|
+
breakOnFirstError: false,
|
|
678
|
+
// check if schema follows best practices and common sense
|
|
679
|
+
pedanticCheck: false,
|
|
680
|
+
// ignore unknown formats (do not report them as an error)
|
|
681
|
+
ignoreUnknownFormats: false,
|
|
682
|
+
// control format assertion behavior:
|
|
683
|
+
// true - respect vocabulary: for draft 2019-09/2020-12, check meta-schema $vocabulary
|
|
684
|
+
// to determine if format is assertion or annotation-only
|
|
685
|
+
// false - format is always annotation-only (never asserts)
|
|
686
|
+
// null - legacy behavior: always assert format validation
|
|
687
|
+
formatAssertions: null,
|
|
688
|
+
// function to be called on every schema
|
|
689
|
+
customValidator: null,
|
|
690
|
+
// maximum recursion depth for deeply nested schema/data traversal (prevents stack overflow)
|
|
691
|
+
maxRecursionDepth: DEFAULT_MAX_RECURSION_DEPTH,
|
|
692
|
+
};
|
|
693
|
+
const normalizeOptions = (options) => {
|
|
694
|
+
let normalized;
|
|
695
|
+
// options
|
|
696
|
+
if (typeof options === 'object') {
|
|
697
|
+
let keys = Object.keys(options);
|
|
698
|
+
// check that the options are correctly configured
|
|
699
|
+
for (const key of keys) {
|
|
700
|
+
if (defaultOptions[key] === undefined) {
|
|
701
|
+
throw new Error('Unexpected option passed to constructor: ' + key);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
// copy the default options into passed options
|
|
705
|
+
keys = Object.keys(defaultOptions);
|
|
706
|
+
for (const key of keys) {
|
|
707
|
+
if (options[key] === undefined) {
|
|
708
|
+
options[key] = shallowClone(defaultOptions[key]);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
normalized = options;
|
|
712
|
+
}
|
|
713
|
+
else {
|
|
714
|
+
normalized = shallowClone(defaultOptions);
|
|
715
|
+
}
|
|
716
|
+
// Clamp asyncTimeout to prevent resource exhaustion (CWE-400)
|
|
717
|
+
if (normalized.asyncTimeout != null) {
|
|
718
|
+
normalized.asyncTimeout = Math.min(Math.max(normalized.asyncTimeout, 0), MAX_ASYNC_TIMEOUT);
|
|
719
|
+
}
|
|
720
|
+
if (normalized.strictMode === true) {
|
|
721
|
+
normalized.forceAdditional = true;
|
|
722
|
+
normalized.forceItems = true;
|
|
723
|
+
normalized.forceMaxLength = true;
|
|
724
|
+
normalized.forceProperties = true;
|
|
725
|
+
normalized.noExtraKeywords = true;
|
|
726
|
+
normalized.noTypeless = true;
|
|
727
|
+
normalized.noEmptyStrings = true;
|
|
728
|
+
normalized.noEmptyArrays = true;
|
|
729
|
+
}
|
|
730
|
+
return normalized;
|
|
731
|
+
};
|
|
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
|
+
}
|
|
702
744
|
const getEffectiveId = (schema) => {
|
|
703
745
|
let id = getId(schema);
|
|
704
746
|
if ((!id || !isAbsoluteUri(id)) && typeof schema.id === 'string' && isAbsoluteUri(schema.id)) {
|
|
@@ -729,31 +771,31 @@
|
|
|
729
771
|
}
|
|
730
772
|
class SchemaCache {
|
|
731
773
|
validator;
|
|
732
|
-
static global_cache =
|
|
733
|
-
cache =
|
|
774
|
+
static global_cache = Object.create(null);
|
|
775
|
+
cache = Object.create(null);
|
|
734
776
|
constructor(validator) {
|
|
735
777
|
this.validator = validator;
|
|
736
778
|
}
|
|
737
779
|
static cacheSchemaByUri(uri, schema) {
|
|
738
|
-
const remotePath =
|
|
780
|
+
const remotePath = getSafeRemotePath(uri);
|
|
739
781
|
if (remotePath) {
|
|
740
782
|
this.global_cache[remotePath] = schema;
|
|
741
783
|
}
|
|
742
784
|
}
|
|
743
785
|
cacheSchemaByUri(uri, schema) {
|
|
744
|
-
const remotePath =
|
|
786
|
+
const remotePath = getSafeRemotePath(uri);
|
|
745
787
|
if (remotePath) {
|
|
746
788
|
this.cache[remotePath] = schema;
|
|
747
789
|
}
|
|
748
790
|
}
|
|
749
791
|
removeFromCacheByUri(uri) {
|
|
750
|
-
const remotePath =
|
|
792
|
+
const remotePath = getSafeRemotePath(uri);
|
|
751
793
|
if (remotePath) {
|
|
752
794
|
delete this.cache[remotePath];
|
|
753
795
|
}
|
|
754
796
|
}
|
|
755
797
|
checkCacheForUri(uri) {
|
|
756
|
-
const remotePath =
|
|
798
|
+
const remotePath = getSafeRemotePath(uri);
|
|
757
799
|
return remotePath ? this.cache[remotePath] != null : false;
|
|
758
800
|
}
|
|
759
801
|
getSchema(report, refOrSchema) {
|
|
@@ -768,6 +810,9 @@
|
|
|
768
810
|
return deepClone(refOrSchema, this.validator.options.maxRecursionDepth);
|
|
769
811
|
}
|
|
770
812
|
fromCache(path) {
|
|
813
|
+
if (path === '__proto__' || path === 'constructor' || path === 'prototype') {
|
|
814
|
+
return undefined;
|
|
815
|
+
}
|
|
771
816
|
let found = this.cache[path];
|
|
772
817
|
if (found) {
|
|
773
818
|
return found;
|
|
@@ -799,7 +844,7 @@
|
|
|
799
844
|
}
|
|
800
845
|
}
|
|
801
846
|
}
|
|
802
|
-
const remotePath =
|
|
847
|
+
const remotePath = getSafeRemotePath(uri);
|
|
803
848
|
const queryPath = getQueryPath(uri);
|
|
804
849
|
let result;
|
|
805
850
|
let resolvedFromAncestor = false;
|
|
@@ -4333,19 +4378,55 @@
|
|
|
4333
4378
|
}
|
|
4334
4379
|
return !inExpression;
|
|
4335
4380
|
};
|
|
4381
|
+
const hasValidTildeEscapes = (segment) => {
|
|
4382
|
+
for (let i = 0; i < segment.length; i++) {
|
|
4383
|
+
if (segment[i] === '~') {
|
|
4384
|
+
const next = segment[i + 1];
|
|
4385
|
+
if (next !== '0' && next !== '1') {
|
|
4386
|
+
return false;
|
|
4387
|
+
}
|
|
4388
|
+
i++; // skip the escape character
|
|
4389
|
+
}
|
|
4390
|
+
}
|
|
4391
|
+
return true;
|
|
4392
|
+
};
|
|
4336
4393
|
const jsonPointerValidator = (pointer) => {
|
|
4337
4394
|
if (typeof pointer !== 'string')
|
|
4338
4395
|
return true;
|
|
4339
4396
|
// JSON Pointer: empty, or a sequence of '/'-prefixed reference tokens.
|
|
4340
4397
|
// In each token, '~' must be escaped as '~0' or '~1'.
|
|
4341
|
-
|
|
4398
|
+
if (pointer === '')
|
|
4399
|
+
return true;
|
|
4400
|
+
if (!/^(?:\/[^/]*)+$/.test(pointer))
|
|
4401
|
+
return false;
|
|
4402
|
+
const tokens = pointer.split('/').slice(1); // first element is empty before leading '/'
|
|
4403
|
+
for (const token of tokens) {
|
|
4404
|
+
if (!hasValidTildeEscapes(token))
|
|
4405
|
+
return false;
|
|
4406
|
+
}
|
|
4407
|
+
return true;
|
|
4342
4408
|
};
|
|
4343
4409
|
const relativeJsonPointerValidator = (pointer) => {
|
|
4344
4410
|
if (typeof pointer !== 'string')
|
|
4345
4411
|
return true;
|
|
4346
4412
|
// Relative JSON Pointer: non-negative integer prefix (no leading zeros unless zero),
|
|
4347
4413
|
// followed by either '#', a JSON Pointer, or nothing.
|
|
4348
|
-
|
|
4414
|
+
const match = pointer.match(/^(0|[1-9]\d*)(.*)$/);
|
|
4415
|
+
if (!match)
|
|
4416
|
+
return false;
|
|
4417
|
+
const suffix = match[2];
|
|
4418
|
+
if (suffix === '' || suffix === '#')
|
|
4419
|
+
return true;
|
|
4420
|
+
if (!suffix.startsWith('/'))
|
|
4421
|
+
return false;
|
|
4422
|
+
if (!/^(?:\/[^/]*)+$/.test(suffix))
|
|
4423
|
+
return false;
|
|
4424
|
+
const tokens = suffix.split('/').slice(1);
|
|
4425
|
+
for (const token of tokens) {
|
|
4426
|
+
if (!hasValidTildeEscapes(token))
|
|
4427
|
+
return false;
|
|
4428
|
+
}
|
|
4429
|
+
return true;
|
|
4349
4430
|
};
|
|
4350
4431
|
const timeValidator = (time) => {
|
|
4351
4432
|
if (typeof time !== 'string')
|
|
@@ -4449,6 +4530,15 @@
|
|
|
4449
4530
|
// Shared regex compilation helper for JSON Schema patterns
|
|
4450
4531
|
// Returns { ok: true, value: RegExp } or { ok: false, error: { pattern, message } }
|
|
4451
4532
|
function compileSchemaRegex(pattern) {
|
|
4533
|
+
if (pattern.length > MAX_SCHEMA_REGEX_LENGTH) {
|
|
4534
|
+
return {
|
|
4535
|
+
ok: false,
|
|
4536
|
+
error: {
|
|
4537
|
+
pattern,
|
|
4538
|
+
message: `Pattern length ${pattern.length} exceeds maximum allowed length of ${MAX_SCHEMA_REGEX_LENGTH}`,
|
|
4539
|
+
},
|
|
4540
|
+
};
|
|
4541
|
+
}
|
|
4452
4542
|
const unicodePropertyEscape = /\\[pP]{/;
|
|
4453
4543
|
const nonBmpCharacter = /[\u{10000}-\u{10FFFF}]/u;
|
|
4454
4544
|
const surrogatePairEscape = /\\uD[89AB][0-9A-Fa-f]{2}\\uD[CDEF][0-9A-Fa-f]{2}/;
|
|
@@ -4457,6 +4547,7 @@
|
|
|
4457
4547
|
if (needsUnicode) {
|
|
4458
4548
|
// Try compiling with 'u' flag only
|
|
4459
4549
|
try {
|
|
4550
|
+
// lgtm[js/regex-injection] JSON Schema `pattern` is intentionally regex syntax and constrained by MAX_SCHEMA_REGEX_LENGTH.
|
|
4460
4551
|
const re = new RegExp(pattern, 'u');
|
|
4461
4552
|
return { ok: true, value: re };
|
|
4462
4553
|
}
|
|
@@ -4472,6 +4563,7 @@
|
|
|
4472
4563
|
}
|
|
4473
4564
|
else {
|
|
4474
4565
|
try {
|
|
4566
|
+
// lgtm[js/regex-injection] JSON Schema `pattern` is intentionally regex syntax and constrained by MAX_SCHEMA_REGEX_LENGTH.
|
|
4475
4567
|
const re = new RegExp(pattern);
|
|
4476
4568
|
return { ok: true, value: re };
|
|
4477
4569
|
}
|
|
@@ -4791,7 +4883,7 @@
|
|
|
4791
4883
|
for (let idx = 0; idx < json.length; idx++) {
|
|
4792
4884
|
const subReport = new Report_(report);
|
|
4793
4885
|
subReports.push(subReport);
|
|
4794
|
-
|
|
4886
|
+
this._jsonValidate(subReport, containsSchema, json[idx]);
|
|
4795
4887
|
cacheValidationResult(report, containsSchema, json[idx], subReport.errors.length === 0);
|
|
4796
4888
|
}
|
|
4797
4889
|
const addContainsErrorIfNeeded = () => {
|
|
@@ -4824,7 +4916,7 @@
|
|
|
4824
4916
|
function allOfValidator(report, schema, json) {
|
|
4825
4917
|
// http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.5.3.2
|
|
4826
4918
|
for (let i = 0; i < schema.allOf.length; i++) {
|
|
4827
|
-
const validateResult =
|
|
4919
|
+
const validateResult = this._jsonValidate(report, schema.allOf[i], json);
|
|
4828
4920
|
if (this.options.breakOnFirstError && validateResult === false) {
|
|
4829
4921
|
break;
|
|
4830
4922
|
}
|
|
@@ -4839,7 +4931,7 @@
|
|
|
4839
4931
|
for (let i = 0; i < schema.anyOf.length; i++) {
|
|
4840
4932
|
const subReport = new Report(report);
|
|
4841
4933
|
subReports.push(subReport);
|
|
4842
|
-
|
|
4934
|
+
this._jsonValidate(subReport, schema.anyOf[i], json);
|
|
4843
4935
|
cacheValidationResult(report, schema.anyOf[i], json, subReport.errors.length === 0);
|
|
4844
4936
|
}
|
|
4845
4937
|
// Aggregate async tasks and decide when ready
|
|
@@ -4865,7 +4957,7 @@
|
|
|
4865
4957
|
for (let i = 0; i < schema.oneOf.length; i++) {
|
|
4866
4958
|
const subReport = new Report(report);
|
|
4867
4959
|
subReports.push(subReport);
|
|
4868
|
-
|
|
4960
|
+
this._jsonValidate(subReport, schema.oneOf[i], json);
|
|
4869
4961
|
cacheValidationResult(report, schema.oneOf[i], json, subReport.errors.length === 0);
|
|
4870
4962
|
}
|
|
4871
4963
|
// Aggregate async tasks and decide when ready
|
|
@@ -4890,7 +4982,7 @@
|
|
|
4890
4982
|
function notValidator(report, schema, json) {
|
|
4891
4983
|
// http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.5.6.2
|
|
4892
4984
|
const subReport = new Report(report);
|
|
4893
|
-
if (
|
|
4985
|
+
if (this._jsonValidate(subReport, schema.not, json) === true) {
|
|
4894
4986
|
report.addError('NOT_PASSED', undefined, undefined, schema, 'not');
|
|
4895
4987
|
}
|
|
4896
4988
|
}
|
|
@@ -4908,13 +5000,13 @@
|
|
|
4908
5000
|
return;
|
|
4909
5001
|
}
|
|
4910
5002
|
const conditionReport = new Report(report);
|
|
4911
|
-
|
|
5003
|
+
this._jsonValidate(conditionReport, conditionSchema, json);
|
|
4912
5004
|
cacheValidationResult(report, conditionSchema, json, conditionReport.errors.length === 0);
|
|
4913
5005
|
const branchSchema = conditionReport.errors.length === 0 ? thenSchema : elseSchema;
|
|
4914
5006
|
if (branchSchema === undefined) {
|
|
4915
5007
|
return;
|
|
4916
5008
|
}
|
|
4917
|
-
|
|
5009
|
+
this._jsonValidate(report, branchSchema, json);
|
|
4918
5010
|
}
|
|
4919
5011
|
function thenValidator() {
|
|
4920
5012
|
// handled by if
|
|
@@ -5170,7 +5262,7 @@
|
|
|
5170
5262
|
}
|
|
5171
5263
|
else {
|
|
5172
5264
|
// if dependency is a schema, validate against this schema
|
|
5173
|
-
|
|
5265
|
+
this._jsonValidate(report, dependencyDefinition, json);
|
|
5174
5266
|
}
|
|
5175
5267
|
}
|
|
5176
5268
|
}
|
|
@@ -5189,7 +5281,7 @@
|
|
|
5189
5281
|
for (const dependencyName of keys) {
|
|
5190
5282
|
if (Object.hasOwn(json, dependencyName)) {
|
|
5191
5283
|
const dependencySchema = schema.dependentSchemas[dependencyName];
|
|
5192
|
-
|
|
5284
|
+
this._jsonValidate(report, dependencySchema, json);
|
|
5193
5285
|
}
|
|
5194
5286
|
}
|
|
5195
5287
|
}
|
|
@@ -5242,7 +5334,7 @@
|
|
|
5242
5334
|
for (const key of keys) {
|
|
5243
5335
|
const subReport = new Report_(report);
|
|
5244
5336
|
subReports.push(subReport);
|
|
5245
|
-
|
|
5337
|
+
this._jsonValidate(subReport, propertyNamesSchema, key);
|
|
5246
5338
|
}
|
|
5247
5339
|
const addPropertyNameErrors = () => {
|
|
5248
5340
|
for (let idx = 0; idx < keys.length; idx++) {
|
|
@@ -5454,21 +5546,13 @@
|
|
|
5454
5546
|
const result = formatValidatorFn.call(this, json);
|
|
5455
5547
|
if (result instanceof Promise) {
|
|
5456
5548
|
// Promise-based async
|
|
5457
|
-
const timeoutMs = this.options.asyncTimeout || 2000;
|
|
5458
5549
|
const promiseResult = result;
|
|
5459
5550
|
report.addAsyncTaskWithPath(async (callback) => {
|
|
5460
5551
|
try {
|
|
5461
|
-
const
|
|
5462
|
-
setTimeout(() => reject(new Error('Async timeout')), timeoutMs);
|
|
5463
|
-
});
|
|
5464
|
-
const resolved = await Promise.race([promiseResult, timeoutPromise]);
|
|
5552
|
+
const resolved = await promiseResult;
|
|
5465
5553
|
callback(resolved);
|
|
5466
5554
|
}
|
|
5467
|
-
catch (
|
|
5468
|
-
if (error.message === 'Async timeout') {
|
|
5469
|
-
// Don't call callback, let global timeout handle it
|
|
5470
|
-
return;
|
|
5471
|
-
}
|
|
5555
|
+
catch (_error) {
|
|
5472
5556
|
callback(false);
|
|
5473
5557
|
}
|
|
5474
5558
|
}, [], function (resolvedResult) {
|
|
@@ -6316,6 +6400,21 @@
|
|
|
6316
6400
|
_schemaReader = schemaReader;
|
|
6317
6401
|
}
|
|
6318
6402
|
|
|
6403
|
+
/** Safely assign a property on `obj`, refusing prototype-polluting keys. */
|
|
6404
|
+
function safeSetProperty(obj, key, value) {
|
|
6405
|
+
const unsafeTargets = [
|
|
6406
|
+
Object.prototype,
|
|
6407
|
+
Function.prototype,
|
|
6408
|
+
Array.prototype,
|
|
6409
|
+
];
|
|
6410
|
+
if (unsafeTargets.includes(obj)) {
|
|
6411
|
+
return;
|
|
6412
|
+
}
|
|
6413
|
+
/** Reject property names that could pollute Object.prototype (CWE-1321). */
|
|
6414
|
+
if (key !== '__proto__' && key !== 'constructor' && key !== 'prototype') {
|
|
6415
|
+
obj[key] = value;
|
|
6416
|
+
}
|
|
6417
|
+
}
|
|
6319
6418
|
const collectIds = (obj, maxDepth = DEFAULT_MAX_RECURSION_DEPTH) => {
|
|
6320
6419
|
const ids = [];
|
|
6321
6420
|
function walk(node, scope, _depth = 0) {
|
|
@@ -6481,7 +6580,8 @@
|
|
|
6481
6580
|
}
|
|
6482
6581
|
let baseDir = baseNoFrag;
|
|
6483
6582
|
if (!baseDir.endsWith('/')) {
|
|
6484
|
-
|
|
6583
|
+
const lastSlash = baseDir.lastIndexOf('/');
|
|
6584
|
+
baseDir = lastSlash === -1 ? '' : baseDir.slice(0, lastSlash + 1);
|
|
6485
6585
|
}
|
|
6486
6586
|
return baseDir + ref;
|
|
6487
6587
|
};
|
|
@@ -6552,8 +6652,14 @@
|
|
|
6552
6652
|
this.collectAndCacheIds(schema);
|
|
6553
6653
|
}
|
|
6554
6654
|
}
|
|
6655
|
+
const canMutateSchemaObject = schema !== Object.prototype &&
|
|
6656
|
+
schema !== Function.prototype &&
|
|
6657
|
+
schema !== Array.prototype;
|
|
6555
6658
|
// if we have an id than it should be cached already (if this instance has compiled it)
|
|
6556
|
-
if (
|
|
6659
|
+
if (canMutateSchemaObject &&
|
|
6660
|
+
schema.__$compiled &&
|
|
6661
|
+
schema.id &&
|
|
6662
|
+
this.validator.scache.checkCacheForUri(schema.id) === false) {
|
|
6557
6663
|
schema.__$compiled = undefined;
|
|
6558
6664
|
}
|
|
6559
6665
|
// do not re-compile schemas
|
|
@@ -6561,7 +6667,7 @@
|
|
|
6561
6667
|
return true;
|
|
6562
6668
|
}
|
|
6563
6669
|
// v8 - if $schema is not present, set $schema to default
|
|
6564
|
-
if (!schema.$schema && this.validator.options.version !== 'none') {
|
|
6670
|
+
if (canMutateSchemaObject && !schema.$schema && this.validator.options.version !== 'none') {
|
|
6565
6671
|
schema.$schema = this.validator.getDefaultSchemaId();
|
|
6566
6672
|
}
|
|
6567
6673
|
if (schema.id && typeof schema.id === 'string' && !options?.noCache) {
|
|
@@ -6576,7 +6682,9 @@
|
|
|
6576
6682
|
}
|
|
6577
6683
|
// delete all __$missingReferences from previous compilation attempts
|
|
6578
6684
|
const isValidExceptReferences = report.isValid();
|
|
6579
|
-
|
|
6685
|
+
if (canMutateSchemaObject) {
|
|
6686
|
+
delete schema.__$missingReferences;
|
|
6687
|
+
}
|
|
6580
6688
|
// collect all references that need to be resolved - $ref and $schema
|
|
6581
6689
|
const useRefObjectScope = this.validator.options.version === 'draft2019-09' || this.validator.options.version === 'draft2020-12';
|
|
6582
6690
|
const refs = collectReferences(schema, undefined, undefined, undefined, { useRefObjectScope }, this.validator.options.maxRecursionDepth);
|
|
@@ -6624,17 +6732,17 @@
|
|
|
6624
6732
|
report.addError('UNRESOLVABLE_REFERENCE', [refObj.ref]);
|
|
6625
6733
|
report.path = report.path.slice(0, -refObj.path.length);
|
|
6626
6734
|
// pusblish unresolved references out
|
|
6627
|
-
if (isValidExceptReferences) {
|
|
6735
|
+
if (isValidExceptReferences && canMutateSchemaObject) {
|
|
6628
6736
|
schema.__$missingReferences = schema.__$missingReferences || [];
|
|
6629
6737
|
schema.__$missingReferences.push(refObj);
|
|
6630
6738
|
}
|
|
6631
6739
|
}
|
|
6632
6740
|
}
|
|
6633
6741
|
// this might create circular references
|
|
6634
|
-
refObj.obj
|
|
6742
|
+
safeSetProperty(refObj.obj, `__${refObj.key}Resolved`, response);
|
|
6635
6743
|
}
|
|
6636
6744
|
const isValid = report.isValid();
|
|
6637
|
-
if (isValid) {
|
|
6745
|
+
if (isValid && canMutateSchemaObject) {
|
|
6638
6746
|
schema.__$compiled = true;
|
|
6639
6747
|
}
|
|
6640
6748
|
// else {
|
|
@@ -6667,7 +6775,7 @@
|
|
|
6667
6775
|
const response = arr.find((x) => x.id === refObj.ref);
|
|
6668
6776
|
if (response) {
|
|
6669
6777
|
// this might create circular references
|
|
6670
|
-
refObj.obj
|
|
6778
|
+
safeSetProperty(refObj.obj, `__${refObj.key}Resolved`, response);
|
|
6671
6779
|
// it's resolved now so delete it
|
|
6672
6780
|
sch.__$missingReferences.splice(idx2, 1);
|
|
6673
6781
|
}
|
|
@@ -7431,6 +7539,16 @@
|
|
|
7431
7539
|
this.sv = new SchemaValidator(this);
|
|
7432
7540
|
this.options = normalizeOptions(options);
|
|
7433
7541
|
}
|
|
7542
|
+
/**
|
|
7543
|
+
* Internal recursive JSON validation — delegates to the `validate` function
|
|
7544
|
+
* in `json-validation.ts`. Exposed as a method so that per-keyword validator
|
|
7545
|
+
* modules (array, combinators, object) can call back into the core validator
|
|
7546
|
+
* via `this` without importing `json-validation.ts` directly (which would
|
|
7547
|
+
* create a circular dependency).
|
|
7548
|
+
*/
|
|
7549
|
+
_jsonValidate(report, schema, json) {
|
|
7550
|
+
return validate.call(this, report, schema, json);
|
|
7551
|
+
}
|
|
7434
7552
|
getDefaultSchemaId() {
|
|
7435
7553
|
return this.options.version && this.options.version !== 'none'
|
|
7436
7554
|
? VERSION_SCHEMA_URL_MAPPING[this.options.version]
|