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
|
@@ -211,19 +211,55 @@ const uriTemplateValidator = (uri) => {
|
|
|
211
211
|
}
|
|
212
212
|
return !inExpression;
|
|
213
213
|
};
|
|
214
|
+
const hasValidTildeEscapes = (segment) => {
|
|
215
|
+
for (let i = 0; i < segment.length; i++) {
|
|
216
|
+
if (segment[i] === '~') {
|
|
217
|
+
const next = segment[i + 1];
|
|
218
|
+
if (next !== '0' && next !== '1') {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
i++; // skip the escape character
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return true;
|
|
225
|
+
};
|
|
214
226
|
const jsonPointerValidator = (pointer) => {
|
|
215
227
|
if (typeof pointer !== 'string')
|
|
216
228
|
return true;
|
|
217
229
|
// JSON Pointer: empty, or a sequence of '/'-prefixed reference tokens.
|
|
218
230
|
// In each token, '~' must be escaped as '~0' or '~1'.
|
|
219
|
-
|
|
231
|
+
if (pointer === '')
|
|
232
|
+
return true;
|
|
233
|
+
if (!/^(?:\/[^/]*)+$/.test(pointer))
|
|
234
|
+
return false;
|
|
235
|
+
const tokens = pointer.split('/').slice(1); // first element is empty before leading '/'
|
|
236
|
+
for (const token of tokens) {
|
|
237
|
+
if (!hasValidTildeEscapes(token))
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
return true;
|
|
220
241
|
};
|
|
221
242
|
const relativeJsonPointerValidator = (pointer) => {
|
|
222
243
|
if (typeof pointer !== 'string')
|
|
223
244
|
return true;
|
|
224
245
|
// Relative JSON Pointer: non-negative integer prefix (no leading zeros unless zero),
|
|
225
246
|
// followed by either '#', a JSON Pointer, or nothing.
|
|
226
|
-
|
|
247
|
+
const match = pointer.match(/^(0|[1-9]\d*)(.*)$/);
|
|
248
|
+
if (!match)
|
|
249
|
+
return false;
|
|
250
|
+
const suffix = match[2];
|
|
251
|
+
if (suffix === '' || suffix === '#')
|
|
252
|
+
return true;
|
|
253
|
+
if (!suffix.startsWith('/'))
|
|
254
|
+
return false;
|
|
255
|
+
if (!/^(?:\/[^/]*)+$/.test(suffix))
|
|
256
|
+
return false;
|
|
257
|
+
const tokens = suffix.split('/').slice(1);
|
|
258
|
+
for (const token of tokens) {
|
|
259
|
+
if (!hasValidTildeEscapes(token))
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
return true;
|
|
227
263
|
};
|
|
228
264
|
const timeValidator = (time) => {
|
|
229
265
|
if (typeof time !== 'string')
|
package/dist/json-schema.js
CHANGED
|
@@ -6,8 +6,8 @@ import { getRemotePath, isAbsoluteUri } from './utils/uri.js';
|
|
|
6
6
|
export const NON_SCHEMA_KEYWORDS = ['enum', 'const', 'default', 'examples'];
|
|
7
7
|
/** Returns true if the key is an internal z-schema property (prefixed with `__$`). */
|
|
8
8
|
export const isInternalKey = (key) => key.startsWith('__$');
|
|
9
|
+
import { DEFAULT_MAX_RECURSION_DEPTH } from './utils/constants.js';
|
|
9
10
|
import { isObject } from './utils/what-is.js';
|
|
10
|
-
import { DEFAULT_MAX_RECURSION_DEPTH } from './z-schema-options.js';
|
|
11
11
|
export const getId = (schema) => {
|
|
12
12
|
// Draft-04 uses `id` exclusively — never return `$id` for a draft-04 schema
|
|
13
13
|
if (typeof schema.$schema === 'string' && schema.$schema.includes('draft-04')) {
|
package/dist/report.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { Errors, getValidateError } from './errors.js';
|
|
2
2
|
import { shallowClone } from './utils/clone.js';
|
|
3
|
+
import { MAX_ASYNC_TIMEOUT } from './utils/constants.js';
|
|
3
4
|
import { get } from './utils/json.js';
|
|
4
5
|
import { jsonSymbol, schemaSymbol } from './utils/symbols.js';
|
|
5
6
|
import { isAbsoluteUri } from './utils/uri.js';
|
|
6
7
|
import { isObject } from './utils/what-is.js';
|
|
8
|
+
const ASYNC_TIMEOUT_POLL_MS = 10;
|
|
7
9
|
export class Report {
|
|
8
10
|
asyncTasks = [];
|
|
9
11
|
commonErrorMessage;
|
|
@@ -77,7 +79,8 @@ export class Report {
|
|
|
77
79
|
return this.parentReport.getAncestor(id);
|
|
78
80
|
}
|
|
79
81
|
processAsyncTasks(timeout, callback) {
|
|
80
|
-
const validationTimeout = timeout || 2000;
|
|
82
|
+
const validationTimeout = Math.min(Math.max(timeout || 2000, 0), MAX_ASYNC_TIMEOUT);
|
|
83
|
+
const timeoutAt = Date.now() + validationTimeout;
|
|
81
84
|
let tasksCount = this.asyncTasks.length;
|
|
82
85
|
let timedOut = false;
|
|
83
86
|
const finish = () => {
|
|
@@ -106,13 +109,19 @@ export class Report {
|
|
|
106
109
|
const respondCallback = respond(processFn);
|
|
107
110
|
fn(...fnArgs, respondCallback);
|
|
108
111
|
}
|
|
109
|
-
|
|
110
|
-
if (tasksCount
|
|
112
|
+
const checkTimeout = () => {
|
|
113
|
+
if (timedOut || tasksCount <= 0) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (Date.now() >= timeoutAt) {
|
|
111
117
|
timedOut = true;
|
|
112
118
|
this.addError('ASYNC_TIMEOUT', [tasksCount, validationTimeout]);
|
|
113
119
|
callback(getValidateError({ details: this.errors }), false);
|
|
120
|
+
return;
|
|
114
121
|
}
|
|
115
|
-
|
|
122
|
+
setTimeout(checkTimeout, ASYNC_TIMEOUT_POLL_MS);
|
|
123
|
+
};
|
|
124
|
+
setTimeout(checkTimeout, ASYNC_TIMEOUT_POLL_MS);
|
|
116
125
|
}
|
|
117
126
|
getPath(returnPathAsString) {
|
|
118
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
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
import { getId, isInternalKey, NON_SCHEMA_KEYWORDS } from './json-schema.js';
|
|
2
2
|
import { Report } from './report.js';
|
|
3
|
+
import { DEFAULT_MAX_RECURSION_DEPTH } from './utils/constants.js';
|
|
3
4
|
import { getRemotePath, isAbsoluteUri } from './utils/uri.js';
|
|
4
|
-
import { DEFAULT_MAX_RECURSION_DEPTH } from './z-schema-options.js';
|
|
5
5
|
import { getSchemaReader } from './z-schema-reader.js';
|
|
6
|
+
/** Safely assign a property on `obj`, refusing prototype-polluting keys. */
|
|
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
|
+
}
|
|
16
|
+
/** Reject property names that could pollute Object.prototype (CWE-1321). */
|
|
17
|
+
if (key !== '__proto__' && key !== 'constructor' && key !== 'prototype') {
|
|
18
|
+
obj[key] = value;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
6
21
|
export const collectIds = (obj, maxDepth = DEFAULT_MAX_RECURSION_DEPTH) => {
|
|
7
22
|
const ids = [];
|
|
8
23
|
function walk(node, scope, _depth = 0) {
|
|
@@ -168,7 +183,8 @@ const resolveReference = (base, ref) => {
|
|
|
168
183
|
}
|
|
169
184
|
let baseDir = baseNoFrag;
|
|
170
185
|
if (!baseDir.endsWith('/')) {
|
|
171
|
-
|
|
186
|
+
const lastSlash = baseDir.lastIndexOf('/');
|
|
187
|
+
baseDir = lastSlash === -1 ? '' : baseDir.slice(0, lastSlash + 1);
|
|
172
188
|
}
|
|
173
189
|
return baseDir + ref;
|
|
174
190
|
};
|
|
@@ -239,8 +255,14 @@ export class SchemaCompiler {
|
|
|
239
255
|
this.collectAndCacheIds(schema);
|
|
240
256
|
}
|
|
241
257
|
}
|
|
258
|
+
const canMutateSchemaObject = schema !== Object.prototype &&
|
|
259
|
+
schema !== Function.prototype &&
|
|
260
|
+
schema !== Array.prototype;
|
|
242
261
|
// if we have an id than it should be cached already (if this instance has compiled it)
|
|
243
|
-
if (
|
|
262
|
+
if (canMutateSchemaObject &&
|
|
263
|
+
schema.__$compiled &&
|
|
264
|
+
schema.id &&
|
|
265
|
+
this.validator.scache.checkCacheForUri(schema.id) === false) {
|
|
244
266
|
schema.__$compiled = undefined;
|
|
245
267
|
}
|
|
246
268
|
// do not re-compile schemas
|
|
@@ -248,7 +270,7 @@ export class SchemaCompiler {
|
|
|
248
270
|
return true;
|
|
249
271
|
}
|
|
250
272
|
// v8 - if $schema is not present, set $schema to default
|
|
251
|
-
if (!schema.$schema && this.validator.options.version !== 'none') {
|
|
273
|
+
if (canMutateSchemaObject && !schema.$schema && this.validator.options.version !== 'none') {
|
|
252
274
|
schema.$schema = this.validator.getDefaultSchemaId();
|
|
253
275
|
}
|
|
254
276
|
if (schema.id && typeof schema.id === 'string' && !options?.noCache) {
|
|
@@ -263,7 +285,9 @@ export class SchemaCompiler {
|
|
|
263
285
|
}
|
|
264
286
|
// delete all __$missingReferences from previous compilation attempts
|
|
265
287
|
const isValidExceptReferences = report.isValid();
|
|
266
|
-
|
|
288
|
+
if (canMutateSchemaObject) {
|
|
289
|
+
delete schema.__$missingReferences;
|
|
290
|
+
}
|
|
267
291
|
// collect all references that need to be resolved - $ref and $schema
|
|
268
292
|
const useRefObjectScope = this.validator.options.version === 'draft2019-09' || this.validator.options.version === 'draft2020-12';
|
|
269
293
|
const refs = collectReferences(schema, undefined, undefined, undefined, { useRefObjectScope }, this.validator.options.maxRecursionDepth);
|
|
@@ -317,17 +341,17 @@ export class SchemaCompiler {
|
|
|
317
341
|
report.addError('UNRESOLVABLE_REFERENCE', [refObj.ref]);
|
|
318
342
|
report.path = report.path.slice(0, -refObj.path.length);
|
|
319
343
|
// pusblish unresolved references out
|
|
320
|
-
if (isValidExceptReferences) {
|
|
344
|
+
if (isValidExceptReferences && canMutateSchemaObject) {
|
|
321
345
|
schema.__$missingReferences = schema.__$missingReferences || [];
|
|
322
346
|
schema.__$missingReferences.push(refObj);
|
|
323
347
|
}
|
|
324
348
|
}
|
|
325
349
|
}
|
|
326
350
|
// this might create circular references
|
|
327
|
-
refObj.obj
|
|
351
|
+
safeSetProperty(refObj.obj, `__${refObj.key}Resolved`, response);
|
|
328
352
|
}
|
|
329
353
|
const isValid = report.isValid();
|
|
330
|
-
if (isValid) {
|
|
354
|
+
if (isValid && canMutateSchemaObject) {
|
|
331
355
|
schema.__$compiled = true;
|
|
332
356
|
}
|
|
333
357
|
// else {
|
|
@@ -360,7 +384,7 @@ export class SchemaCompiler {
|
|
|
360
384
|
const response = arr.find((x) => x.id === refObj.ref);
|
|
361
385
|
if (response) {
|
|
362
386
|
// this might create circular references
|
|
363
|
-
refObj.obj
|
|
387
|
+
safeSetProperty(refObj.obj, `__${refObj.key}Resolved`, response);
|
|
364
388
|
// it's resolved now so delete it
|
|
365
389
|
sch.__$missingReferences.splice(idx2, 1);
|
|
366
390
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default maximum recursion depth for deeply nested schema/data traversal.
|
|
3
|
+
* Used as the default for {@link ZSchemaOptions.maxRecursionDepth} and
|
|
4
|
+
* internal helpers like `deepClone` and `collectIds`.
|
|
5
|
+
*/
|
|
6
|
+
export declare const DEFAULT_MAX_RECURSION_DEPTH = 100;
|
|
7
|
+
/**
|
|
8
|
+
* Maximum allowed value for {@link ZSchemaOptions.asyncTimeout} in milliseconds.
|
|
9
|
+
* Values exceeding this limit are clamped during option normalization to
|
|
10
|
+
* prevent resource exhaustion (CWE-400).
|
|
11
|
+
*/
|
|
12
|
+
export declare const MAX_ASYNC_TIMEOUT = 60000;
|
|
13
|
+
/**
|
|
14
|
+
* Maximum allowed length for a JSON Schema `pattern` regular expression string.
|
|
15
|
+
* Patterns exceeding this limit are rejected by {@link compileSchemaRegex} to
|
|
16
|
+
* mitigate Regular Expression Denial-of-Service (CWE-1333) and regex injection
|
|
17
|
+
* (CWE-95).
|
|
18
|
+
*/
|
|
19
|
+
export declare const MAX_SCHEMA_REGEX_LENGTH = 10000;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { ValidateError } from './errors.js';
|
|
2
2
|
import type { FormatValidatorFn } from './format-validators.js';
|
|
3
|
-
import type { JsonSchema } from './json-schema-versions.js';
|
|
3
|
+
import type { JsonSchema, JsonSchemaInternal } from './json-schema-versions.js';
|
|
4
4
|
import type { ZSchemaOptions } from './z-schema-options.js';
|
|
5
5
|
import { type Errors } from './errors.js';
|
|
6
|
+
import { Report } from './report.js';
|
|
6
7
|
import { SchemaCache } from './schema-cache.js';
|
|
7
8
|
import { SchemaCompiler } from './schema-compiler.js';
|
|
8
9
|
import { SchemaValidator } from './schema-validator.js';
|
|
@@ -28,6 +29,14 @@ export declare class ZSchemaBase {
|
|
|
28
29
|
validateOptions: ValidateOptions;
|
|
29
30
|
options: ZSchemaOptions;
|
|
30
31
|
constructor(options: ZSchemaOptions | undefined, token: symbol);
|
|
32
|
+
/**
|
|
33
|
+
* Internal recursive JSON validation — delegates to the `validate` function
|
|
34
|
+
* in `json-validation.ts`. Exposed as a method so that per-keyword validator
|
|
35
|
+
* modules (array, combinators, object) can call back into the core validator
|
|
36
|
+
* via `this` without importing `json-validation.ts` directly (which would
|
|
37
|
+
* create a circular dependency).
|
|
38
|
+
*/
|
|
39
|
+
_jsonValidate(report: Report, schema: boolean | JsonSchemaInternal, json: unknown): boolean;
|
|
31
40
|
getDefaultSchemaId(): string;
|
|
32
41
|
_validate(json: unknown, schema: JsonSchema | string, options: ValidateOptions, callback: ValidateCallback): void;
|
|
33
42
|
_validate(json: unknown, schema: JsonSchema | string, callback: ValidateCallback): void;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { FormatValidatorFn } from './format-validators.js';
|
|
2
2
|
import type { Report } from './report.js';
|
|
3
3
|
import { type JsonSchemaVersion } from './json-schema-versions.js';
|
|
4
|
-
|
|
4
|
+
import { DEFAULT_MAX_RECURSION_DEPTH, MAX_ASYNC_TIMEOUT } from './utils/constants.js';
|
|
5
|
+
export { DEFAULT_MAX_RECURSION_DEPTH, MAX_ASYNC_TIMEOUT };
|
|
5
6
|
export interface ZSchemaOptions {
|
|
6
7
|
version?: JsonSchemaVersion | 'none';
|
|
7
8
|
asyncTimeout?: number;
|
package/dist/utils/clone.js
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default maximum recursion depth for deeply nested schema/data traversal.
|
|
3
|
+
* Used as the default for {@link ZSchemaOptions.maxRecursionDepth} and
|
|
4
|
+
* internal helpers like `deepClone` and `collectIds`.
|
|
5
|
+
*/
|
|
6
|
+
export const DEFAULT_MAX_RECURSION_DEPTH = 100;
|
|
7
|
+
/**
|
|
8
|
+
* Maximum allowed value for {@link ZSchemaOptions.asyncTimeout} in milliseconds.
|
|
9
|
+
* Values exceeding this limit are clamped during option normalization to
|
|
10
|
+
* prevent resource exhaustion (CWE-400).
|
|
11
|
+
*/
|
|
12
|
+
export const MAX_ASYNC_TIMEOUT = 60_000;
|
|
13
|
+
/**
|
|
14
|
+
* Maximum allowed length for a JSON Schema `pattern` regular expression string.
|
|
15
|
+
* Patterns exceeding this limit are rejected by {@link compileSchemaRegex} to
|
|
16
|
+
* mitigate Regular Expression Denial-of-Service (CWE-1333) and regex injection
|
|
17
|
+
* (CWE-95).
|
|
18
|
+
*/
|
|
19
|
+
export const MAX_SCHEMA_REGEX_LENGTH = 10_000;
|
package/dist/utils/json.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_MAX_RECURSION_DEPTH } from '
|
|
1
|
+
import { DEFAULT_MAX_RECURSION_DEPTH } from './constants.js';
|
|
2
2
|
import { isObject } from './what-is.js';
|
|
3
3
|
export const areEqual = (json1, json2, options, _depth = 0) => {
|
|
4
4
|
const caseInsensitiveComparison = options?.caseInsensitiveComparison || false;
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
// Shared regex compilation helper for JSON Schema patterns
|
|
2
2
|
// Returns { ok: true, value: RegExp } or { ok: false, error: { pattern, message } }
|
|
3
|
+
import { MAX_SCHEMA_REGEX_LENGTH } from './constants.js';
|
|
3
4
|
export function compileSchemaRegex(pattern) {
|
|
5
|
+
if (pattern.length > MAX_SCHEMA_REGEX_LENGTH) {
|
|
6
|
+
return {
|
|
7
|
+
ok: false,
|
|
8
|
+
error: {
|
|
9
|
+
pattern,
|
|
10
|
+
message: `Pattern length ${pattern.length} exceeds maximum allowed length of ${MAX_SCHEMA_REGEX_LENGTH}`,
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
}
|
|
4
14
|
const unicodePropertyEscape = /\\[pP]{/;
|
|
5
15
|
const nonBmpCharacter = /[\u{10000}-\u{10FFFF}]/u;
|
|
6
16
|
const surrogatePairEscape = /\\uD[89AB][0-9A-Fa-f]{2}\\uD[CDEF][0-9A-Fa-f]{2}/;
|
|
@@ -9,6 +19,7 @@ export function compileSchemaRegex(pattern) {
|
|
|
9
19
|
if (needsUnicode) {
|
|
10
20
|
// Try compiling with 'u' flag only
|
|
11
21
|
try {
|
|
22
|
+
// lgtm[js/regex-injection] JSON Schema `pattern` is intentionally regex syntax and constrained by MAX_SCHEMA_REGEX_LENGTH.
|
|
12
23
|
const re = new RegExp(pattern, 'u');
|
|
13
24
|
return { ok: true, value: re };
|
|
14
25
|
}
|
|
@@ -24,6 +35,7 @@ export function compileSchemaRegex(pattern) {
|
|
|
24
35
|
}
|
|
25
36
|
else {
|
|
26
37
|
try {
|
|
38
|
+
// lgtm[js/regex-injection] JSON Schema `pattern` is intentionally regex syntax and constrained by MAX_SCHEMA_REGEX_LENGTH.
|
|
27
39
|
const re = new RegExp(pattern);
|
|
28
40
|
return { ok: true, value: re };
|
|
29
41
|
}
|
package/dist/validation/array.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { validate } from '../json-validation.js';
|
|
2
1
|
import { isUniqueArray } from '../utils/array.js';
|
|
3
2
|
import { cacheValidationResult, deferOrRunSync, shouldSkipValidate } from './shared.js';
|
|
4
3
|
// ---------------------------------------------------------------------------
|
|
@@ -100,7 +99,7 @@ export function containsValidator(report, schema, json) {
|
|
|
100
99
|
for (let idx = 0; idx < json.length; idx++) {
|
|
101
100
|
const subReport = new Report_(report);
|
|
102
101
|
subReports.push(subReport);
|
|
103
|
-
|
|
102
|
+
this._jsonValidate(subReport, containsSchema, json[idx]);
|
|
104
103
|
cacheValidationResult(report, containsSchema, json[idx], subReport.errors.length === 0);
|
|
105
104
|
}
|
|
106
105
|
const addContainsErrorIfNeeded = () => {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { validate } from '../json-validation.js';
|
|
2
1
|
import { Report } from '../report.js';
|
|
3
2
|
import { cacheValidationResult, deferOrRunSync } from './shared.js';
|
|
4
3
|
// ---------------------------------------------------------------------------
|
|
@@ -7,7 +6,7 @@ import { cacheValidationResult, deferOrRunSync } from './shared.js';
|
|
|
7
6
|
export function allOfValidator(report, schema, json) {
|
|
8
7
|
// http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.5.3.2
|
|
9
8
|
for (let i = 0; i < schema.allOf.length; i++) {
|
|
10
|
-
const validateResult =
|
|
9
|
+
const validateResult = this._jsonValidate(report, schema.allOf[i], json);
|
|
11
10
|
if (this.options.breakOnFirstError && validateResult === false) {
|
|
12
11
|
break;
|
|
13
12
|
}
|
|
@@ -22,7 +21,7 @@ export function anyOfValidator(report, schema, json) {
|
|
|
22
21
|
for (let i = 0; i < schema.anyOf.length; i++) {
|
|
23
22
|
const subReport = new Report(report);
|
|
24
23
|
subReports.push(subReport);
|
|
25
|
-
|
|
24
|
+
this._jsonValidate(subReport, schema.anyOf[i], json);
|
|
26
25
|
cacheValidationResult(report, schema.anyOf[i], json, subReport.errors.length === 0);
|
|
27
26
|
}
|
|
28
27
|
// Aggregate async tasks and decide when ready
|
|
@@ -48,7 +47,7 @@ export function oneOfValidator(report, schema, json) {
|
|
|
48
47
|
for (let i = 0; i < schema.oneOf.length; i++) {
|
|
49
48
|
const subReport = new Report(report);
|
|
50
49
|
subReports.push(subReport);
|
|
51
|
-
|
|
50
|
+
this._jsonValidate(subReport, schema.oneOf[i], json);
|
|
52
51
|
cacheValidationResult(report, schema.oneOf[i], json, subReport.errors.length === 0);
|
|
53
52
|
}
|
|
54
53
|
// Aggregate async tasks and decide when ready
|
|
@@ -73,7 +72,7 @@ export function oneOfValidator(report, schema, json) {
|
|
|
73
72
|
export function notValidator(report, schema, json) {
|
|
74
73
|
// http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.5.6.2
|
|
75
74
|
const subReport = new Report(report);
|
|
76
|
-
if (
|
|
75
|
+
if (this._jsonValidate(subReport, schema.not, json) === true) {
|
|
77
76
|
report.addError('NOT_PASSED', undefined, undefined, schema, 'not');
|
|
78
77
|
}
|
|
79
78
|
}
|
|
@@ -91,13 +90,13 @@ export function ifValidator(report, schema, json) {
|
|
|
91
90
|
return;
|
|
92
91
|
}
|
|
93
92
|
const conditionReport = new Report(report);
|
|
94
|
-
|
|
93
|
+
this._jsonValidate(conditionReport, conditionSchema, json);
|
|
95
94
|
cacheValidationResult(report, conditionSchema, json, conditionReport.errors.length === 0);
|
|
96
95
|
const branchSchema = conditionReport.errors.length === 0 ? thenSchema : elseSchema;
|
|
97
96
|
if (branchSchema === undefined) {
|
|
98
97
|
return;
|
|
99
98
|
}
|
|
100
|
-
|
|
99
|
+
this._jsonValidate(report, branchSchema, json);
|
|
101
100
|
}
|
|
102
101
|
export function thenValidator() {
|
|
103
102
|
// handled by if
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { validate } from '../json-validation.js';
|
|
2
1
|
import { difference } from '../utils/array.js';
|
|
3
2
|
import { compileSchemaRegex } from '../utils/schema-regex.js';
|
|
4
3
|
import { isObject } from '../utils/what-is.js';
|
|
@@ -153,7 +152,7 @@ export function dependenciesValidator(report, schema, json) {
|
|
|
153
152
|
}
|
|
154
153
|
else {
|
|
155
154
|
// if dependency is a schema, validate against this schema
|
|
156
|
-
|
|
155
|
+
this._jsonValidate(report, dependencyDefinition, json);
|
|
157
156
|
}
|
|
158
157
|
}
|
|
159
158
|
}
|
|
@@ -172,7 +171,7 @@ export function dependentSchemasValidator(report, schema, json) {
|
|
|
172
171
|
for (const dependencyName of keys) {
|
|
173
172
|
if (Object.hasOwn(json, dependencyName)) {
|
|
174
173
|
const dependencySchema = schema.dependentSchemas[dependencyName];
|
|
175
|
-
|
|
174
|
+
this._jsonValidate(report, dependencySchema, json);
|
|
176
175
|
}
|
|
177
176
|
}
|
|
178
177
|
}
|
|
@@ -225,7 +224,7 @@ export function propertyNamesValidator(report, schema, json) {
|
|
|
225
224
|
for (const key of keys) {
|
|
226
225
|
const subReport = new Report_(report);
|
|
227
226
|
subReports.push(subReport);
|
|
228
|
-
|
|
227
|
+
this._jsonValidate(subReport, propertyNamesSchema, key);
|
|
229
228
|
}
|
|
230
229
|
const addPropertyNameErrors = () => {
|
|
231
230
|
for (let idx = 0; idx < keys.length; idx++) {
|
|
@@ -93,21 +93,13 @@ export function formatValidator(report, schema, json) {
|
|
|
93
93
|
const result = formatValidatorFn.call(this, json);
|
|
94
94
|
if (result instanceof Promise) {
|
|
95
95
|
// Promise-based async
|
|
96
|
-
const timeoutMs = this.options.asyncTimeout || 2000;
|
|
97
96
|
const promiseResult = result;
|
|
98
97
|
report.addAsyncTaskWithPath(async (callback) => {
|
|
99
98
|
try {
|
|
100
|
-
const
|
|
101
|
-
setTimeout(() => reject(new Error('Async timeout')), timeoutMs);
|
|
102
|
-
});
|
|
103
|
-
const resolved = await Promise.race([promiseResult, timeoutPromise]);
|
|
99
|
+
const resolved = await promiseResult;
|
|
104
100
|
callback(resolved);
|
|
105
101
|
}
|
|
106
|
-
catch (
|
|
107
|
-
if (error.message === 'Async timeout') {
|
|
108
|
-
// Don't call callback, let global timeout handle it
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
102
|
+
catch (_error) {
|
|
111
103
|
callback(false);
|
|
112
104
|
}
|
|
113
105
|
}, [], function (resolvedResult) {
|
package/dist/z-schema-base.js
CHANGED
|
@@ -33,6 +33,16 @@ export class ZSchemaBase {
|
|
|
33
33
|
this.sv = new SchemaValidator(this);
|
|
34
34
|
this.options = normalizeOptions(options);
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Internal recursive JSON validation — delegates to the `validate` function
|
|
38
|
+
* in `json-validation.ts`. Exposed as a method so that per-keyword validator
|
|
39
|
+
* modules (array, combinators, object) can call back into the core validator
|
|
40
|
+
* via `this` without importing `json-validation.ts` directly (which would
|
|
41
|
+
* create a circular dependency).
|
|
42
|
+
*/
|
|
43
|
+
_jsonValidate(report, schema, json) {
|
|
44
|
+
return validateJson.call(this, report, schema, json);
|
|
45
|
+
}
|
|
36
46
|
getDefaultSchemaId() {
|
|
37
47
|
return this.options.version && this.options.version !== 'none'
|
|
38
48
|
? VERSION_SCHEMA_URL_MAPPING[this.options.version]
|
package/dist/z-schema-options.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CURRENT_DEFAULT_SCHEMA_VERSION } from './json-schema-versions.js';
|
|
2
2
|
import { shallowClone } from './utils/clone.js';
|
|
3
|
-
|
|
3
|
+
import { DEFAULT_MAX_RECURSION_DEPTH, MAX_ASYNC_TIMEOUT } from './utils/constants.js';
|
|
4
|
+
export { DEFAULT_MAX_RECURSION_DEPTH, MAX_ASYNC_TIMEOUT };
|
|
4
5
|
export const defaultOptions = {
|
|
5
6
|
// default version to validate against
|
|
6
7
|
version: CURRENT_DEFAULT_SCHEMA_VERSION,
|
|
@@ -80,6 +81,10 @@ export const normalizeOptions = (options) => {
|
|
|
80
81
|
else {
|
|
81
82
|
normalized = shallowClone(defaultOptions);
|
|
82
83
|
}
|
|
84
|
+
// Clamp asyncTimeout to prevent resource exhaustion (CWE-400)
|
|
85
|
+
if (normalized.asyncTimeout != null) {
|
|
86
|
+
normalized.asyncTimeout = Math.min(Math.max(normalized.asyncTimeout, 0), MAX_ASYNC_TIMEOUT);
|
|
87
|
+
}
|
|
83
88
|
if (normalized.strictMode === true) {
|
|
84
89
|
normalized.forceAdditional = true;
|
|
85
90
|
normalized.forceItems = true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "z-schema",
|
|
3
|
-
"version": "12.0.
|
|
3
|
+
"version": "12.0.2",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=22.0.0"
|
|
6
6
|
},
|
|
@@ -74,6 +74,8 @@
|
|
|
74
74
|
"build:tests": "tsc --noEmit --project test/tsconfig.json",
|
|
75
75
|
"copy:module-json": "node -e \"fs.copyFileSync('./src/package.json', './dist/package.json')\"",
|
|
76
76
|
"copy:schemas": "node ./scripts/copy-schemas.mts",
|
|
77
|
+
"coverage:artifacts": "node ./scripts/generate-test-coverage-report.mts",
|
|
78
|
+
"coverage:update-badge": "node ./scripts/update-readme-coverage-badge.mts",
|
|
77
79
|
"prepublishOnly": "npm run clean && npm run build && npm test",
|
|
78
80
|
"test": "vitest run",
|
|
79
81
|
"test:coverage": "vitest run --coverage",
|
package/src/format-validators.ts
CHANGED
|
@@ -241,18 +241,47 @@ const uriTemplateValidator: FormatValidatorFn = (uri: unknown) => {
|
|
|
241
241
|
return !inExpression;
|
|
242
242
|
};
|
|
243
243
|
|
|
244
|
+
const hasValidTildeEscapes = (segment: string): boolean => {
|
|
245
|
+
for (let i = 0; i < segment.length; i++) {
|
|
246
|
+
if (segment[i] === '~') {
|
|
247
|
+
const next = segment[i + 1];
|
|
248
|
+
if (next !== '0' && next !== '1') {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
i++; // skip the escape character
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return true;
|
|
255
|
+
};
|
|
256
|
+
|
|
244
257
|
const jsonPointerValidator: FormatValidatorFn = (pointer: unknown) => {
|
|
245
258
|
if (typeof pointer !== 'string') return true;
|
|
246
259
|
// JSON Pointer: empty, or a sequence of '/'-prefixed reference tokens.
|
|
247
260
|
// In each token, '~' must be escaped as '~0' or '~1'.
|
|
248
|
-
|
|
261
|
+
if (pointer === '') return true;
|
|
262
|
+
if (!/^(?:\/[^/]*)+$/.test(pointer)) return false;
|
|
263
|
+
const tokens = pointer.split('/').slice(1); // first element is empty before leading '/'
|
|
264
|
+
for (const token of tokens) {
|
|
265
|
+
if (!hasValidTildeEscapes(token)) return false;
|
|
266
|
+
}
|
|
267
|
+
return true;
|
|
249
268
|
};
|
|
250
269
|
|
|
251
270
|
const relativeJsonPointerValidator: FormatValidatorFn = (pointer: unknown) => {
|
|
252
271
|
if (typeof pointer !== 'string') return true;
|
|
253
272
|
// Relative JSON Pointer: non-negative integer prefix (no leading zeros unless zero),
|
|
254
273
|
// followed by either '#', a JSON Pointer, or nothing.
|
|
255
|
-
|
|
274
|
+
const match = pointer.match(/^(0|[1-9]\d*)(.*)$/);
|
|
275
|
+
if (!match) return false;
|
|
276
|
+
const suffix = match[2];
|
|
277
|
+
if (suffix === '' || suffix === '#') return true;
|
|
278
|
+
if (!suffix.startsWith('/')) return false;
|
|
279
|
+
if (!/^(?:\/[^/]*)+$/.test(suffix)) return false;
|
|
280
|
+
const tokens = suffix.split('/').slice(1);
|
|
281
|
+
for (const token of tokens) {
|
|
282
|
+
if (!hasValidTildeEscapes(token)) return false;
|
|
283
|
+
}
|
|
284
|
+
return true;
|
|
256
285
|
};
|
|
257
286
|
|
|
258
287
|
const timeValidator: FormatValidatorFn = (time: unknown) => {
|