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.
@@ -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
- setTimeout(() => {
196
- if (tasksCount > 0) {
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
- }, validationTimeout);
209
+ setTimeout(checkTimeout, ASYNC_TIMEOUT_POLL_MS);
210
+ };
211
+ setTimeout(checkTimeout, ASYNC_TIMEOUT_POLL_MS);
202
212
  }
203
213
 
204
214
  getPath(returnPathAsString?: boolean) {
@@ -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 = getRemotePath(uri);
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 = getRemotePath(uri);
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 = getRemotePath(uri);
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 = getRemotePath(uri);
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 = getRemotePath(uri);
148
+ const remotePath = getSafeRemotePath(uri);
134
149
  const queryPath = getQueryPath(uri);
135
150
  let result: JsonSchemaInternal | undefined;
136
151
  let resolvedFromAncestor = false;
@@ -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
- baseDir = baseDir.replace(/[^/]*$/, '');
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 (schema.__$compiled && schema.id && this.validator.scache.checkCacheForUri(schema.id) === false) {
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
- delete schema.__$missingReferences;
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[`__${refObj.key}Resolved`] = response;
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[`__${refObj.key}Resolved`] = response;
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
  }
@@ -1,4 +1,4 @@
1
- import { DEFAULT_MAX_RECURSION_DEPTH } from '../z-schema-options.js';
1
+ import { DEFAULT_MAX_RECURSION_DEPTH } from './constants.js';
2
2
  import { copyProp } from './properties.js';
3
3
 
4
4
  export const shallowClone = <T>(src: T): T => {
@@ -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,4 +1,4 @@
1
- import { DEFAULT_MAX_RECURSION_DEPTH } from '../z-schema-options.js';
1
+ import { DEFAULT_MAX_RECURSION_DEPTH } from './constants.js';
2
2
  import { isObject } from './what-is.js';
3
3
 
4
4
  interface AreEqualOptions {
@@ -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) {
@@ -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
- validate.call(this, subReport, containsSchema as any, json[idx]);
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 = validate.call(this, report, schema.allOf![i], json);
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
- validate.call(this, subReport, schema.anyOf![i], json);
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
- validate.call(this, subReport, schema.oneOf![i], json);
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 (validate.call(this, subReport, schema.not!, json) === true) {
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
- validate.call(this, conditionReport, conditionSchema as any, json);
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
- validate.call(this, report, branchSchema as any, json);
122
+ this._jsonValidate(report, branchSchema as any, json);
124
123
  }
125
124
 
126
125
  export function thenValidator() {
@@ -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
- validate.call(this, report, dependencyDefinition, json);
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
- validate.call(this, report, dependencySchema, json);
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
- validate.call(this, subReport, propertyNamesSchema as any, key);
305
+ this._jsonValidate(subReport, propertyNamesSchema as any, key);
307
306
  }
308
307
 
309
308
  const addPropertyNameErrors = () => {
@@ -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 timeoutPromise = new Promise<never>((_, reject) => {
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 (error) {
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
  },
@@ -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]
@@ -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 const DEFAULT_MAX_RECURSION_DEPTH = 100;
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;