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/src/json-schema.ts
CHANGED
|
@@ -12,8 +12,8 @@ export const NON_SCHEMA_KEYWORDS = ['enum', 'const', 'default', 'examples'] as c
|
|
|
12
12
|
|
|
13
13
|
/** Returns true if the key is an internal z-schema property (prefixed with `__$`). */
|
|
14
14
|
export const isInternalKey = (key: string): boolean => key.startsWith('__$');
|
|
15
|
+
import { DEFAULT_MAX_RECURSION_DEPTH } from './utils/constants.js';
|
|
15
16
|
import { isObject } from './utils/what-is.js';
|
|
16
|
-
import { DEFAULT_MAX_RECURSION_DEPTH } from './z-schema-options.js';
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Properties present in ALL JSON Schema drafts (04 through 2020-12) with
|
package/src/report.ts
CHANGED
|
@@ -5,11 +5,14 @@ import type { ZSchemaOptions } from './z-schema-options.js';
|
|
|
5
5
|
|
|
6
6
|
import { Errors, getValidateError } from './errors.js';
|
|
7
7
|
import { shallowClone } from './utils/clone.js';
|
|
8
|
+
import { MAX_ASYNC_TIMEOUT } from './utils/constants.js';
|
|
8
9
|
import { get } from './utils/json.js';
|
|
9
10
|
import { jsonSymbol, schemaSymbol } from './utils/symbols.js';
|
|
10
11
|
import { isAbsoluteUri } from './utils/uri.js';
|
|
11
12
|
import { isObject } from './utils/what-is.js';
|
|
12
13
|
|
|
14
|
+
const ASYNC_TIMEOUT_POLL_MS = 10;
|
|
15
|
+
|
|
13
16
|
export interface SchemaErrorDetail {
|
|
14
17
|
/**
|
|
15
18
|
* Example: "Expected type string but found type array"
|
|
@@ -158,7 +161,8 @@ export class Report {
|
|
|
158
161
|
}
|
|
159
162
|
|
|
160
163
|
processAsyncTasks(timeout: number | undefined, callback: ValidateCallback) {
|
|
161
|
-
const validationTimeout = timeout || 2000;
|
|
164
|
+
const validationTimeout = Math.min(Math.max(timeout || 2000, 0), MAX_ASYNC_TIMEOUT);
|
|
165
|
+
const timeoutAt = Date.now() + validationTimeout;
|
|
162
166
|
let tasksCount = this.asyncTasks.length;
|
|
163
167
|
let timedOut = false;
|
|
164
168
|
|
|
@@ -192,13 +196,19 @@ export class Report {
|
|
|
192
196
|
fn(...fnArgs, respondCallback);
|
|
193
197
|
}
|
|
194
198
|
|
|
195
|
-
|
|
196
|
-
if (tasksCount
|
|
199
|
+
const checkTimeout = () => {
|
|
200
|
+
if (timedOut || tasksCount <= 0) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (Date.now() >= timeoutAt) {
|
|
197
204
|
timedOut = true;
|
|
198
205
|
this.addError('ASYNC_TIMEOUT', [tasksCount, validationTimeout]);
|
|
199
206
|
callback(getValidateError({ details: this.errors }), false);
|
|
207
|
+
return;
|
|
200
208
|
}
|
|
201
|
-
|
|
209
|
+
setTimeout(checkTimeout, ASYNC_TIMEOUT_POLL_MS);
|
|
210
|
+
};
|
|
211
|
+
setTimeout(checkTimeout, ASYNC_TIMEOUT_POLL_MS);
|
|
202
212
|
}
|
|
203
213
|
|
|
204
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
|
@@ -3,10 +3,26 @@ import type { ZSchemaBase } from './z-schema-base.js';
|
|
|
3
3
|
|
|
4
4
|
import { getId, isInternalKey, NON_SCHEMA_KEYWORDS } from './json-schema.js';
|
|
5
5
|
import { Report } from './report.js';
|
|
6
|
+
import { DEFAULT_MAX_RECURSION_DEPTH } from './utils/constants.js';
|
|
6
7
|
import { getRemotePath, isAbsoluteUri } from './utils/uri.js';
|
|
7
|
-
import { DEFAULT_MAX_RECURSION_DEPTH } from './z-schema-options.js';
|
|
8
8
|
import { getSchemaReader } from './z-schema-reader.js';
|
|
9
9
|
|
|
10
|
+
/** Safely assign a property on `obj`, refusing prototype-polluting keys. */
|
|
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
|
+
}
|
|
20
|
+
/** Reject property names that could pollute Object.prototype (CWE-1321). */
|
|
21
|
+
if (key !== '__proto__' && key !== 'constructor' && key !== 'prototype') {
|
|
22
|
+
obj[key] = value;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
10
26
|
interface Id {
|
|
11
27
|
id: string;
|
|
12
28
|
type: 'absolute' | 'relative' | 'root';
|
|
@@ -213,7 +229,8 @@ const resolveReference = (base: string | undefined, ref: string) => {
|
|
|
213
229
|
|
|
214
230
|
let baseDir = baseNoFrag;
|
|
215
231
|
if (!baseDir.endsWith('/')) {
|
|
216
|
-
|
|
232
|
+
const lastSlash = baseDir.lastIndexOf('/');
|
|
233
|
+
baseDir = lastSlash === -1 ? '' : baseDir.slice(0, lastSlash + 1);
|
|
217
234
|
}
|
|
218
235
|
return baseDir + ref;
|
|
219
236
|
};
|
|
@@ -291,8 +308,18 @@ export class SchemaCompiler {
|
|
|
291
308
|
}
|
|
292
309
|
}
|
|
293
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
|
+
|
|
294
316
|
// if we have an id than it should be cached already (if this instance has compiled it)
|
|
295
|
-
if (
|
|
317
|
+
if (
|
|
318
|
+
canMutateSchemaObject &&
|
|
319
|
+
schema.__$compiled &&
|
|
320
|
+
schema.id &&
|
|
321
|
+
this.validator.scache.checkCacheForUri(schema.id) === false
|
|
322
|
+
) {
|
|
296
323
|
schema.__$compiled = undefined;
|
|
297
324
|
}
|
|
298
325
|
|
|
@@ -302,7 +329,7 @@ export class SchemaCompiler {
|
|
|
302
329
|
}
|
|
303
330
|
|
|
304
331
|
// v8 - if $schema is not present, set $schema to default
|
|
305
|
-
if (!schema.$schema && this.validator.options.version !== 'none') {
|
|
332
|
+
if (canMutateSchemaObject && !schema.$schema && this.validator.options.version !== 'none') {
|
|
306
333
|
schema.$schema = this.validator.getDefaultSchemaId();
|
|
307
334
|
}
|
|
308
335
|
|
|
@@ -320,7 +347,9 @@ export class SchemaCompiler {
|
|
|
320
347
|
|
|
321
348
|
// delete all __$missingReferences from previous compilation attempts
|
|
322
349
|
const isValidExceptReferences = report.isValid();
|
|
323
|
-
|
|
350
|
+
if (canMutateSchemaObject) {
|
|
351
|
+
delete schema.__$missingReferences;
|
|
352
|
+
}
|
|
324
353
|
|
|
325
354
|
// collect all references that need to be resolved - $ref and $schema
|
|
326
355
|
const useRefObjectScope =
|
|
@@ -384,18 +413,18 @@ export class SchemaCompiler {
|
|
|
384
413
|
report.path = report.path.slice(0, -refObj.path.length);
|
|
385
414
|
|
|
386
415
|
// pusblish unresolved references out
|
|
387
|
-
if (isValidExceptReferences) {
|
|
416
|
+
if (isValidExceptReferences && canMutateSchemaObject) {
|
|
388
417
|
schema.__$missingReferences = schema.__$missingReferences || [];
|
|
389
418
|
schema.__$missingReferences.push(refObj);
|
|
390
419
|
}
|
|
391
420
|
}
|
|
392
421
|
}
|
|
393
422
|
// this might create circular references
|
|
394
|
-
refObj.obj
|
|
423
|
+
safeSetProperty(refObj.obj as unknown as Record<string, unknown>, `__${refObj.key}Resolved`, response);
|
|
395
424
|
}
|
|
396
425
|
|
|
397
426
|
const isValid = report.isValid();
|
|
398
|
-
if (isValid) {
|
|
427
|
+
if (isValid && canMutateSchemaObject) {
|
|
399
428
|
schema.__$compiled = true;
|
|
400
429
|
}
|
|
401
430
|
// else {
|
|
@@ -436,7 +465,7 @@ export class SchemaCompiler {
|
|
|
436
465
|
const response = arr.find((x) => x.id === refObj.ref);
|
|
437
466
|
if (response) {
|
|
438
467
|
// this might create circular references
|
|
439
|
-
refObj.obj
|
|
468
|
+
safeSetProperty(refObj.obj as unknown as Record<string, unknown>, `__${refObj.key}Resolved`, response);
|
|
440
469
|
// it's resolved now so delete it
|
|
441
470
|
sch.__$missingReferences.splice(idx2, 1);
|
|
442
471
|
}
|
package/src/utils/clone.ts
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
/**
|
|
9
|
+
* Maximum allowed value for {@link ZSchemaOptions.asyncTimeout} in milliseconds.
|
|
10
|
+
* Values exceeding this limit are clamped during option normalization to
|
|
11
|
+
* prevent resource exhaustion (CWE-400).
|
|
12
|
+
*/
|
|
13
|
+
export const MAX_ASYNC_TIMEOUT = 60_000;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Maximum allowed length for a JSON Schema `pattern` regular expression string.
|
|
17
|
+
* Patterns exceeding this limit are rejected by {@link compileSchemaRegex} to
|
|
18
|
+
* mitigate Regular Expression Denial-of-Service (CWE-1333) and regex injection
|
|
19
|
+
* (CWE-95).
|
|
20
|
+
*/
|
|
21
|
+
export const MAX_SCHEMA_REGEX_LENGTH = 10_000;
|
package/src/utils/json.ts
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
// Shared regex compilation helper for JSON Schema patterns
|
|
2
2
|
// Returns { ok: true, value: RegExp } or { ok: false, error: { pattern, message } }
|
|
3
3
|
|
|
4
|
+
import { MAX_SCHEMA_REGEX_LENGTH } from './constants.js';
|
|
5
|
+
|
|
4
6
|
export function compileSchemaRegex(
|
|
5
7
|
pattern: string
|
|
6
8
|
): { ok: true; value: RegExp } | { ok: false; error: { pattern: string; message: string } } {
|
|
9
|
+
if (pattern.length > MAX_SCHEMA_REGEX_LENGTH) {
|
|
10
|
+
return {
|
|
11
|
+
ok: false,
|
|
12
|
+
error: {
|
|
13
|
+
pattern,
|
|
14
|
+
message: `Pattern length ${pattern.length} exceeds maximum allowed length of ${MAX_SCHEMA_REGEX_LENGTH}`,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
7
19
|
const unicodePropertyEscape = /\\[pP]{/;
|
|
8
20
|
const nonBmpCharacter = /[\u{10000}-\u{10FFFF}]/u;
|
|
9
21
|
const surrogatePairEscape = /\\uD[89AB][0-9A-Fa-f]{2}\\uD[CDEF][0-9A-Fa-f]{2}/;
|
|
@@ -13,6 +25,7 @@ export function compileSchemaRegex(
|
|
|
13
25
|
if (needsUnicode) {
|
|
14
26
|
// Try compiling with 'u' flag only
|
|
15
27
|
try {
|
|
28
|
+
// lgtm[js/regex-injection] JSON Schema `pattern` is intentionally regex syntax and constrained by MAX_SCHEMA_REGEX_LENGTH.
|
|
16
29
|
const re = new RegExp(pattern, 'u');
|
|
17
30
|
return { ok: true, value: re };
|
|
18
31
|
} catch (e: any) {
|
|
@@ -26,6 +39,7 @@ export function compileSchemaRegex(
|
|
|
26
39
|
}
|
|
27
40
|
} else {
|
|
28
41
|
try {
|
|
42
|
+
// lgtm[js/regex-injection] JSON Schema `pattern` is intentionally regex syntax and constrained by MAX_SCHEMA_REGEX_LENGTH.
|
|
29
43
|
const re = new RegExp(pattern);
|
|
30
44
|
return { ok: true, value: re };
|
|
31
45
|
} catch (e: any) {
|
package/src/validation/array.ts
CHANGED
|
@@ -2,7 +2,6 @@ import type { JsonSchemaInternal } from '../json-schema-versions.js';
|
|
|
2
2
|
import type { Report } from '../report.js';
|
|
3
3
|
import type { ZSchemaBase } from '../z-schema-base.js';
|
|
4
4
|
|
|
5
|
-
import { validate } from '../json-validation.js';
|
|
6
5
|
import { isUniqueArray } from '../utils/array.js';
|
|
7
6
|
import { cacheValidationResult, deferOrRunSync, shouldSkipValidate } from './shared.js';
|
|
8
7
|
|
|
@@ -121,7 +120,7 @@ export function containsValidator(this: ZSchemaBase, report: Report, schema: Jso
|
|
|
121
120
|
for (let idx = 0; idx < json.length; idx++) {
|
|
122
121
|
const subReport = new Report_(report);
|
|
123
122
|
subReports.push(subReport);
|
|
124
|
-
|
|
123
|
+
this._jsonValidate(subReport, containsSchema as any, json[idx]);
|
|
125
124
|
cacheValidationResult(report, containsSchema, json[idx], subReport.errors.length === 0);
|
|
126
125
|
}
|
|
127
126
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { JsonSchemaInternal } from '../json-schema-versions.js';
|
|
2
2
|
import type { ZSchemaBase } from '../z-schema-base.js';
|
|
3
3
|
|
|
4
|
-
import { validate } from '../json-validation.js';
|
|
5
4
|
import { Report } from '../report.js';
|
|
6
5
|
import { cacheValidationResult, deferOrRunSync } from './shared.js';
|
|
7
6
|
|
|
@@ -12,7 +11,7 @@ import { cacheValidationResult, deferOrRunSync } from './shared.js';
|
|
|
12
11
|
export function allOfValidator(this: ZSchemaBase, report: Report, schema: JsonSchemaInternal, json: unknown) {
|
|
13
12
|
// http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.5.3.2
|
|
14
13
|
for (let i = 0; i < schema.allOf!.length; i++) {
|
|
15
|
-
const validateResult =
|
|
14
|
+
const validateResult = this._jsonValidate(report, schema.allOf![i], json);
|
|
16
15
|
if (this.options.breakOnFirstError && validateResult === false) {
|
|
17
16
|
break;
|
|
18
17
|
}
|
|
@@ -30,7 +29,7 @@ export function anyOfValidator(this: ZSchemaBase, report: Report, schema: JsonSc
|
|
|
30
29
|
for (let i = 0; i < schema.anyOf!.length; i++) {
|
|
31
30
|
const subReport = new Report(report);
|
|
32
31
|
subReports.push(subReport);
|
|
33
|
-
|
|
32
|
+
this._jsonValidate(subReport, schema.anyOf![i], json);
|
|
34
33
|
cacheValidationResult(report, schema.anyOf![i], json, subReport.errors.length === 0);
|
|
35
34
|
}
|
|
36
35
|
|
|
@@ -61,7 +60,7 @@ export function oneOfValidator(this: ZSchemaBase, report: Report, schema: JsonSc
|
|
|
61
60
|
for (let i = 0; i < schema.oneOf!.length; i++) {
|
|
62
61
|
const subReport = new Report(report);
|
|
63
62
|
subReports.push(subReport);
|
|
64
|
-
|
|
63
|
+
this._jsonValidate(subReport, schema.oneOf![i], json);
|
|
65
64
|
cacheValidationResult(report, schema.oneOf![i], json, subReport.errors.length === 0);
|
|
66
65
|
}
|
|
67
66
|
|
|
@@ -89,7 +88,7 @@ export function oneOfValidator(this: ZSchemaBase, report: Report, schema: JsonSc
|
|
|
89
88
|
export function notValidator(this: ZSchemaBase, report: Report, schema: JsonSchemaInternal, json: unknown) {
|
|
90
89
|
// http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.5.6.2
|
|
91
90
|
const subReport = new Report(report);
|
|
92
|
-
if (
|
|
91
|
+
if (this._jsonValidate(subReport, schema.not!, json) === true) {
|
|
93
92
|
report.addError('NOT_PASSED', undefined, undefined, schema, 'not');
|
|
94
93
|
}
|
|
95
94
|
}
|
|
@@ -112,7 +111,7 @@ export function ifValidator(this: ZSchemaBase, report: Report, schema: JsonSchem
|
|
|
112
111
|
}
|
|
113
112
|
|
|
114
113
|
const conditionReport = new Report(report);
|
|
115
|
-
|
|
114
|
+
this._jsonValidate(conditionReport, conditionSchema as any, json);
|
|
116
115
|
cacheValidationResult(report, conditionSchema, json, conditionReport.errors.length === 0);
|
|
117
116
|
|
|
118
117
|
const branchSchema = conditionReport.errors.length === 0 ? thenSchema : elseSchema;
|
|
@@ -120,7 +119,7 @@ export function ifValidator(this: ZSchemaBase, report: Report, schema: JsonSchem
|
|
|
120
119
|
return;
|
|
121
120
|
}
|
|
122
121
|
|
|
123
|
-
|
|
122
|
+
this._jsonValidate(report, branchSchema as any, json);
|
|
124
123
|
}
|
|
125
124
|
|
|
126
125
|
export function thenValidator() {
|
package/src/validation/object.ts
CHANGED
|
@@ -2,7 +2,6 @@ import type { JsonSchemaInternal } from '../json-schema-versions.js';
|
|
|
2
2
|
import type { Report } from '../report.js';
|
|
3
3
|
import type { ZSchemaBase } from '../z-schema-base.js';
|
|
4
4
|
|
|
5
|
-
import { validate } from '../json-validation.js';
|
|
6
5
|
import { difference } from '../utils/array.js';
|
|
7
6
|
import { compileSchemaRegex } from '../utils/schema-regex.js';
|
|
8
7
|
import { isObject } from '../utils/what-is.js';
|
|
@@ -200,7 +199,7 @@ export function dependenciesValidator(this: ZSchemaBase, report: Report, schema:
|
|
|
200
199
|
}
|
|
201
200
|
} else {
|
|
202
201
|
// if dependency is a schema, validate against this schema
|
|
203
|
-
|
|
202
|
+
this._jsonValidate(report, dependencyDefinition, json);
|
|
204
203
|
}
|
|
205
204
|
}
|
|
206
205
|
}
|
|
@@ -228,7 +227,7 @@ export function dependentSchemasValidator(
|
|
|
228
227
|
for (const dependencyName of keys) {
|
|
229
228
|
if (Object.hasOwn(json, dependencyName)) {
|
|
230
229
|
const dependencySchema = schema.dependentSchemas[dependencyName];
|
|
231
|
-
|
|
230
|
+
this._jsonValidate(report, dependencySchema, json);
|
|
232
231
|
}
|
|
233
232
|
}
|
|
234
233
|
}
|
|
@@ -303,7 +302,7 @@ export function propertyNamesValidator(this: ZSchemaBase, report: Report, schema
|
|
|
303
302
|
for (const key of keys) {
|
|
304
303
|
const subReport = new Report_(report);
|
|
305
304
|
subReports.push(subReport);
|
|
306
|
-
|
|
305
|
+
this._jsonValidate(subReport, propertyNamesSchema as any, key);
|
|
307
306
|
}
|
|
308
307
|
|
|
309
308
|
const addPropertyNameErrors = () => {
|
package/src/validation/string.ts
CHANGED
|
@@ -107,21 +107,13 @@ export function formatValidator(this: ZSchemaBase, report: Report, schema: JsonS
|
|
|
107
107
|
const result = formatValidatorFn.call(this, json);
|
|
108
108
|
if (result instanceof Promise) {
|
|
109
109
|
// Promise-based async
|
|
110
|
-
const timeoutMs = this.options.asyncTimeout || 2000;
|
|
111
110
|
const promiseResult = result;
|
|
112
111
|
report.addAsyncTaskWithPath(
|
|
113
112
|
async (callback) => {
|
|
114
113
|
try {
|
|
115
|
-
const
|
|
116
|
-
setTimeout(() => reject(new Error('Async timeout')), timeoutMs);
|
|
117
|
-
});
|
|
118
|
-
const resolved = await Promise.race([promiseResult, timeoutPromise]);
|
|
114
|
+
const resolved = await promiseResult;
|
|
119
115
|
callback(resolved);
|
|
120
|
-
} catch (
|
|
121
|
-
if ((error as Error).message === 'Async timeout') {
|
|
122
|
-
// Don't call callback, let global timeout handle it
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
116
|
+
} catch (_error) {
|
|
125
117
|
callback(false);
|
|
126
118
|
}
|
|
127
119
|
},
|
package/src/z-schema-base.ts
CHANGED
|
@@ -54,6 +54,17 @@ export class ZSchemaBase {
|
|
|
54
54
|
this.options = normalizeOptions(options);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Internal recursive JSON validation — delegates to the `validate` function
|
|
59
|
+
* in `json-validation.ts`. Exposed as a method so that per-keyword validator
|
|
60
|
+
* modules (array, combinators, object) can call back into the core validator
|
|
61
|
+
* via `this` without importing `json-validation.ts` directly (which would
|
|
62
|
+
* create a circular dependency).
|
|
63
|
+
*/
|
|
64
|
+
_jsonValidate(report: Report, schema: boolean | JsonSchemaInternal, json: unknown): boolean {
|
|
65
|
+
return validateJson.call(this, report, schema, json);
|
|
66
|
+
}
|
|
67
|
+
|
|
57
68
|
getDefaultSchemaId(): string {
|
|
58
69
|
return this.options.version && this.options.version !== 'none'
|
|
59
70
|
? VERSION_SCHEMA_URL_MAPPING[this.options.version]
|
package/src/z-schema-options.ts
CHANGED
|
@@ -3,8 +3,9 @@ import type { Report } from './report.js';
|
|
|
3
3
|
|
|
4
4
|
import { CURRENT_DEFAULT_SCHEMA_VERSION, type JsonSchemaVersion } from './json-schema-versions.js';
|
|
5
5
|
import { shallowClone } from './utils/clone.js';
|
|
6
|
+
import { DEFAULT_MAX_RECURSION_DEPTH, MAX_ASYNC_TIMEOUT } from './utils/constants.js';
|
|
6
7
|
|
|
7
|
-
export
|
|
8
|
+
export { DEFAULT_MAX_RECURSION_DEPTH, MAX_ASYNC_TIMEOUT };
|
|
8
9
|
|
|
9
10
|
export interface ZSchemaOptions {
|
|
10
11
|
version?: JsonSchemaVersion | 'none';
|
|
@@ -119,6 +120,11 @@ export const normalizeOptions = (options?: ZSchemaOptions) => {
|
|
|
119
120
|
normalized = shallowClone(defaultOptions);
|
|
120
121
|
}
|
|
121
122
|
|
|
123
|
+
// Clamp asyncTimeout to prevent resource exhaustion (CWE-400)
|
|
124
|
+
if (normalized.asyncTimeout != null) {
|
|
125
|
+
normalized.asyncTimeout = Math.min(Math.max(normalized.asyncTimeout, 0), MAX_ASYNC_TIMEOUT);
|
|
126
|
+
}
|
|
127
|
+
|
|
122
128
|
if (normalized.strictMode === true) {
|
|
123
129
|
normalized.forceAdditional = true;
|
|
124
130
|
normalized.forceItems = true;
|