From e5734208e95e748b3dff2a56238bde244171f8a6 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Fri, 8 May 2026 12:08:45 +0200 Subject: [PATCH] Hardened tb-js-executor sandbox script invocation (JVN#16937365) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The args array passed into the sandbox carried the host realm prototype chain, so a script could reach the host Function constructor via args.constructor.constructor and execute arbitrary code in the host process (read files, run shell commands, dump env vars). Construct args inside the sandbox context using vm.runInContext('[]'), then populate with string primitives. The resulting array's prototype chain belongs to the sandbox realm, so constructor traversal cannot escape. Strings are primitives and safe to cross the realm boundary. Affects use_sandbox=true path only. The use_sandbox=false path (invokeFunction) is intentionally left as-is and explicitly marked as dangerous-by-design — it compiles and runs user-supplied scripts in the host realm via vm.compileFunction (parsingContext only isolates parsing, not execution). It remains as a documented performance trade-off for trusted, non-public clusters; a startup WARN is logged when script.use_sandbox=false, and an operator-facing yaml comment sits next to the setting in config/default.yml. Reported by Hiroki Imai, LAC Co., Ltd. --- msa/js-executor/api/jsExecutor.ts | 25 ++++++++++++++++++++++--- msa/js-executor/config/default.yml | 6 ++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/msa/js-executor/api/jsExecutor.ts b/msa/js-executor/api/jsExecutor.ts index 855ad99932..ab916a2760 100644 --- a/msa/js-executor/api/jsExecutor.ts +++ b/msa/js-executor/api/jsExecutor.ts @@ -15,14 +15,22 @@ /// import vm, { Script } from 'vm'; +import { _logger } from '../config/logger'; export type TbScript = Script | Function; export class JsExecutor { useSandbox: boolean; + private logger = _logger('JsExecutor'); constructor(useSandbox: boolean) { this.useSandbox = useSandbox; + if (!useSandbox) { + this.logger.warn( + 'script.use_sandbox=false: dangerous by design — user-supplied scripts run in the host realm with no isolation. ' + + 'Use only as a performance trade-off in trusted, non-public clusters.' + ); + } } compileScript(code: string): Promise { @@ -56,9 +64,15 @@ export class JsExecutor { private invokeScript(script: Script, args: string[], timeout: number | undefined): Promise { return new Promise((resolve, reject) => { try { - const sandbox = Object.create(null); - sandbox.args = args; - const result = script.runInNewContext(sandbox, {timeout: timeout}); + const sandbox = vm.createContext(Object.create(null)); + // Construct args inside the sandbox context so it inherits sandbox-realm + // prototypes; prevents prototype-based escapes from the host realm. + const ctxArgs = vm.runInContext('[]', sandbox) as string[]; + for (let i = 0; i < args.length; i++) { + ctxArgs[i] = String(args[i]); + } + sandbox.args = ctxArgs; + const result = script.runInContext(sandbox, {timeout: timeout}); resolve(result); } catch (err) { reject(err); @@ -67,6 +81,11 @@ export class JsExecutor { } + // DANGEROUS BY DESIGN: the non-sandbox path. vm.compileFunction's + // parsingContext only isolates *parsing*, not *execution* — the resulting + // function runs in the host realm with full access to host globals + // (process, require, etc.). Enabled only via script.use_sandbox=false as + // a performance trade-off in trusted clusters. private createFunction(code: string): Promise { return new Promise((resolve, reject) => { try { diff --git a/msa/js-executor/config/default.yml b/msa/js-executor/config/default.yml index 5939b0e29a..9a33190069 100644 --- a/msa/js-executor/config/default.yml +++ b/msa/js-executor/config/default.yml @@ -50,6 +50,12 @@ logger: filename: "tb-js-executor-%DATE%.log" script: + # WARNING: setting this to "false" is DANGEROUS BY DESIGN. The non-sandbox + # path compiles and runs user-supplied scripts in the host realm via + # vm.compileFunction; it provides no isolation and exposes the host process + # (file system, environment variables, child_process, etc.) to script + # authors. Use "false" only as a performance trade-off in trusted, + # non-public clusters where every script author is fully trusted. use_sandbox: "true" memory_usage_trace_frequency: "1000" script_body_trace_frequency: "10000"