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