From e23e92b4d541cd7cf5c4e90074b26f734ff119f8 Mon Sep 17 00:00:00 2001 From: Yehia Amer Date: Tue, 14 Jul 2026 00:11:13 +0300 Subject: [PATCH] fix: evaluate property default initializers via AST instead of vm2 Property initializers used as defaults were evaluated by executing them in a vm2 sandbox. This (a) runs code originating from the input .ts file, the concern raised in #628, and (b) depends on vm2, which has an unpatched sandbox-escape advisory (#631, GHSA-99p7-6v5w-7xg8). Read the literal value directly from the TypeScript AST instead, for the forms representable in a JSON Schema default: string, number (incl. negative), boolean, null, and arrays of these. Nothing is executed, so a malicious initializer yields `undefined` (and the existing "unknown initializer" warning) rather than running. Behaviour for valid literal defaults is unchanged. Adds a test fixture covering the supported forms, including single-quoted strings and negative numbers. Closes #631 --- api.md | 17 +++++ package.json | 1 - .../default-properties-initializer/main.ts | 11 +++ .../schema.json | 67 +++++++++++++++++++ test/schema.test.ts | 2 + typescript-json-schema.ts | 57 +++++++++++----- yarn.lock | 12 +--- 7 files changed, 138 insertions(+), 29 deletions(-) create mode 100644 test/programs/default-properties-initializer/main.ts create mode 100644 test/programs/default-properties-initializer/schema.json diff --git a/api.md b/api.md index 9a73afdf..eeda3518 100644 --- a/api.md +++ b/api.md @@ -691,6 +691,23 @@ class MyObject { ``` +## [default-properties-initializer](./test/programs/default-properties-initializer) + +```ts +export class MyObject { + varString: string = "foo"; + varSingleQuoted: string = 'bar'; + varNumber: number = 123; + varFloat: number = 3.21; + varNegative: number = -5; + varBoolean: boolean = true; + varNull: null = null; + varArray: number[] = [1, 2, 3]; + varStringArray: string[] = ["a", "b"]; +} +``` + + ## [enums-compiled-compute](./test/programs/enums-compiled-compute) ```ts diff --git a/package.json b/package.json index 5c61a11d..ed79b9ca 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,6 @@ "safe-stable-stringify": "^2.5.0", "ts-node": "^10.9.2", "typescript": "~5.9.3", - "vm2": "^3.11.3", "yargs": "^18.0.0" }, "devDependencies": { diff --git a/test/programs/default-properties-initializer/main.ts b/test/programs/default-properties-initializer/main.ts new file mode 100644 index 00000000..88049ca5 --- /dev/null +++ b/test/programs/default-properties-initializer/main.ts @@ -0,0 +1,11 @@ +export class MyObject { + varString: string = "foo"; + varSingleQuoted: string = 'bar'; + varNumber: number = 123; + varFloat: number = 3.21; + varNegative: number = -5; + varBoolean: boolean = true; + varNull: null = null; + varArray: number[] = [1, 2, 3]; + varStringArray: string[] = ["a", "b"]; +} diff --git a/test/programs/default-properties-initializer/schema.json b/test/programs/default-properties-initializer/schema.json new file mode 100644 index 00000000..41df6430 --- /dev/null +++ b/test/programs/default-properties-initializer/schema.json @@ -0,0 +1,67 @@ +{ + "type": "object", + "properties": { + "varString": { + "type": "string", + "default": "foo" + }, + "varSingleQuoted": { + "type": "string", + "default": "bar" + }, + "varNumber": { + "type": "number", + "default": 123 + }, + "varFloat": { + "type": "number", + "default": 3.21 + }, + "varNegative": { + "type": "number", + "default": -5 + }, + "varBoolean": { + "type": "boolean", + "default": true + }, + "varNull": { + "type": "null", + "default": null + }, + "varArray": { + "type": "array", + "items": { + "type": "number" + }, + "default": [ + 1, + 2, + 3 + ] + }, + "varStringArray": { + "type": "array", + "items": { + "type": "string" + }, + "default": [ + "a", + "b" + ] + } + }, + "additionalProperties": false, + "required": [ + "varArray", + "varBoolean", + "varFloat", + "varNegative", + "varNull", + "varNumber", + "varSingleQuoted", + "varString", + "varStringArray" + ], + "$schema": "http://json-schema.org/draft-07/schema#" +} diff --git a/test/schema.test.ts b/test/schema.test.ts index 35008349..90929e84 100644 --- a/test/schema.test.ts +++ b/test/schema.test.ts @@ -423,6 +423,8 @@ describe("schema", () => { assertSchema("default-properties", "MyObject"); + assertSchema("default-properties-initializer", "MyObject"); + // not supported yet #116 // assertSchema("interface-extra-props", "MyObject"); diff --git a/typescript-json-schema.ts b/typescript-json-schema.ts index c39f7468..a3e048d2 100644 --- a/typescript-json-schema.ts +++ b/typescript-json-schema.ts @@ -9,8 +9,6 @@ export { Program, CompilerOptions, Symbol } from "typescript"; export { ts }; -const { VM } = require("vm2"); - const REGEX_FILE_NAME_OR_SPACE = /(\bimport\(".*?"\)|".*?")\.| /g; const REGEX_TSCONFIG_NAME = /^.*\.json$/; const REGEX_TJS_JSDOC = /^-([\w]+)\s+(\S|\S[\s\S]*\S)\s*$/g; @@ -250,6 +248,40 @@ function parseValue(symbol: ts.Symbol, key: string, value: string): any { } } +/** + * Evaluate a property initializer expression to its literal value, without + * executing any code. Only the literal forms that can be represented in a JSON + * Schema `default` are supported: string, number, boolean, null and arrays of + * those. Returns `undefined` for anything that is not a supported literal. + */ +function evaluateLiteralInitializer(node: ts.Node): PrimitiveType | any[] | undefined { + if (ts.isStringLiteralLike(node)) { + return node.text; + } else if (ts.isNumericLiteral(node)) { + return Number(node.text); + } else if (ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.MinusToken) { + const operand = evaluateLiteralInitializer(node.operand); + return typeof operand === "number" ? -operand : undefined; + } else if (node.kind === ts.SyntaxKind.TrueKeyword) { + return true; + } else if (node.kind === ts.SyntaxKind.FalseKeyword) { + return false; + } else if (node.kind === ts.SyntaxKind.NullKeyword) { + return null; + } else if (ts.isArrayLiteralExpression(node)) { + const values: any[] = []; + for (const element of node.elements) { + const value = evaluateLiteralInitializer(element); + if (value === undefined) { + return undefined; + } + values.push(value); + } + return values; + } + return undefined; +} + function extractLiteralValue(typ: ts.Type): PrimitiveType | undefined { let str = (typ).value; if (str === undefined) { @@ -866,22 +898,11 @@ export class JsonSchemaGenerator { } else if ((initial).kind && (initial).kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral) { definition.default = initial.getText(); } else { - try { - const vm = new VM(); - const val = vm.run("sandboxvar=" + initial.getText()) as any; - if ( - val === null || - typeof val === "string" || - typeof val === "number" || - typeof val === "boolean" || - Object.prototype.toString.call(val) === "[object Array]" - ) { - definition.default = val; - } else if (val) { - console.warn("unknown initializer for property " + propertyName + ": " + val); - } - } catch (e) { - console.warn("exception evaluating initializer for property " + propertyName); + const val = evaluateLiteralInitializer(initial); + if (val !== undefined) { + definition.default = val; + } else { + console.warn("unknown initializer for property " + propertyName + ": " + initial.getText()); } } } diff --git a/yarn.lock b/yarn.lock index 6efd6e11..2a0e50c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -108,14 +108,14 @@ dependencies: undici-types "~7.16.0" -acorn-walk@^8.1.1, acorn-walk@^8.3.4: +acorn-walk@^8.1.1: version "8.3.5" resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz#8a6b8ca8fc5b34685af15dabb44118663c296496" integrity sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw== dependencies: acorn "^8.11.0" -acorn@^8.11.0, acorn@^8.15.0, acorn@^8.4.1: +acorn@^8.11.0, acorn@^8.4.1: version "8.16.0" resolved "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz#4ce79c89be40afe7afe8f3adb902a1f1ce9ac08a" integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw== @@ -1006,14 +1006,6 @@ v8-compile-cache-lib@^3.0.1: resolved "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== -vm2@^3.11.3: - version "3.11.3" - resolved "https://registry.yarnpkg.com/vm2/-/vm2-3.11.3.tgz#5323018a93ae2862c9d52b07b658ebb8d74903f0" - integrity sha512-DO1TTKuOc+veL11VNOvJwRab80mghFKE40Av3bl6pdXs11bdiDMuR73owy+dS2EsTZEvRUeBkkBuDVRjV/RgEw== - dependencies: - acorn "^8.15.0" - acorn-walk "^8.3.4" - which@^2.0.1: version "2.0.2" resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"