diff --git a/packages/server/src/jsRunner/index.ts b/packages/server/src/jsRunner/index.ts index 20a48916c8..67d5111ea6 100644 --- a/packages/server/src/jsRunner/index.ts +++ b/packages/server/src/jsRunner/index.ts @@ -13,11 +13,16 @@ export function init() { let { vm } = bbCtx if (!vm) { + // Can't copy the native helpers into the isolate. We just ignore them as they are handled properly from the helpersSource + const { helpers, ...ctxToPass } = ctx + vm = new IsolatedVM({ memoryLimit: env.JS_RUNNER_MEMORY_LIMIT, timeout: env.JS_PER_EXECUTION_TIME_LIMIT_MS, perRequestLimit: env.JS_PER_REQUEST_TIME_LIMIT_MS, - }).withHelpers() + }) + .withContext(ctxToPass) + .withHelpers() bbCtx.vm = vm } diff --git a/packages/server/src/jsRunner/vm/index.ts b/packages/server/src/jsRunner/vm/index.ts index d8563cca9a..58740dc759 100644 --- a/packages/server/src/jsRunner/vm/index.ts +++ b/packages/server/src/jsRunner/vm/index.ts @@ -16,47 +16,47 @@ class ExecutionTimeoutError extends Error { } class ModuleHandler { - #modules: { + private modules: { import: string moduleKey: string module: ivm.Module }[] = [] - #generateRandomKey = () => `i${crypto.randomUUID().replace(/-/g, "")}` + private generateRandomKey = () => `i${crypto.randomUUID().replace(/-/g, "")}` registerModule(module: ivm.Module, imports: string) { - this.#modules.push({ - moduleKey: this.#generateRandomKey(), + this.modules.push({ + moduleKey: this.generateRandomKey(), import: imports, module: module, }) } generateImports() { - return this.#modules + return this.modules .map(m => `import ${m.import} from "${m.moduleKey}"`) .join(";") } getModule(key: string) { - const module = this.#modules.find(m => m.moduleKey === key) + const module = this.modules.find(m => m.moduleKey === key) return module?.module } } export class IsolatedVM implements VM { - #isolate: ivm.Isolate - #vm: ivm.Context - #jail: ivm.Reference - #timeout: number - #perRequestLimit?: number + private isolate: ivm.Isolate + private vm: ivm.Context + private jail: ivm.Reference + private timeout: number + private perRequestLimit?: number // By default the wrapper returns itself - #codeWrapper: (code: string) => string = code => code + private codeWrapper: (code: string) => string = code => code - #moduleHandler = new ModuleHandler() + private moduleHandler = new ModuleHandler() - readonly #resultKey = "results" + private readonly resultKey = "results" constructor({ memoryLimit, @@ -67,30 +67,30 @@ export class IsolatedVM implements VM { timeout: number perRequestLimit?: number }) { - this.#isolate = new ivm.Isolate({ memoryLimit }) - this.#vm = this.#isolate.createContextSync() - this.#jail = this.#vm.global - this.#jail.setSync("global", this.#jail.derefInto()) + this.isolate = new ivm.Isolate({ memoryLimit }) + this.vm = this.isolate.createContextSync() + this.jail = this.vm.global + this.jail.setSync("global", this.jail.derefInto()) - this.#addToContext({ - [this.#resultKey]: { out: "" }, + this.addToContext({ + [this.resultKey]: { out: "" }, }) - this.#timeout = timeout - this.#perRequestLimit = perRequestLimit + this.timeout = timeout + this.perRequestLimit = perRequestLimit } withHelpers() { - const urlModule = this.#registerCallbacks({ + const urlModule = this.registerCallbacks({ resolve: url.resolve, parse: url.parse, }) - const querystringModule = this.#registerCallbacks({ + const querystringModule = this.registerCallbacks({ escape: querystring.escape, }) - this.#addToContext({ + this.addToContext({ helpersStripProtocol: new ivm.Callback((str: string) => { var parsed = url.parse(str) as any parsed.protocol = "" @@ -105,19 +105,19 @@ export class IsolatedVM implements VM { } }` const helpersSource = loadBundle(BundleType.HELPERS) - const helpersModule = this.#isolate.compileModuleSync( + const helpersModule = this.isolate.compileModuleSync( `${injectedRequire};${helpersSource}` ) - helpersModule.instantiateSync(this.#vm, specifier => { + helpersModule.instantiateSync(this.vm, specifier => { if (specifier === "crypto") { - const cryptoModule = this.#registerCallbacks({ + const cryptoModule = this.registerCallbacks({ randomUUID: crypto.randomUUID, }) - const module = this.#isolate.compileModuleSync( + const module = this.isolate.compileModuleSync( `export default ${cryptoModule}` ) - module.instantiateSync(this.#vm, specifier => { + module.instantiateSync(this.vm, specifier => { throw new Error(`No imports allowed. Required: ${specifier}`) }) return module @@ -125,18 +125,18 @@ export class IsolatedVM implements VM { throw new Error(`No imports allowed. Required: ${specifier}`) }) - this.#moduleHandler.registerModule(helpersModule, "helpers") + this.moduleHandler.registerModule(helpersModule, "helpers") return this } withContext(context: Record) { - this.#addToContext(context) + this.addToContext(context) return this } withParsingBson(data: any) { - this.#addToContext({ + this.addToContext({ bsonData: bson.BSON.serialize({ data }), }) @@ -145,7 +145,7 @@ export class IsolatedVM implements VM { // 2. Deserialise the data within the isolate, to get the original data // 3. Process script // 4. Stringify the result in order to convert the result from BSON to json - this.#codeWrapper = code => + this.codeWrapper = code => `(function(){ const data = deserialize(bsonData, { validation: { utf8: false } }).data; const result = ${code} @@ -154,7 +154,7 @@ export class IsolatedVM implements VM { const bsonSource = loadBundle(BundleType.BSON) - this.#addToContext({ + this.addToContext({ textDecoderCb: new ivm.Callback( (args: { constructorArgs: any @@ -184,23 +184,23 @@ export class IsolatedVM implements VM { }) } }.toString() - const bsonModule = this.#isolate.compileModuleSync( + const bsonModule = this.isolate.compileModuleSync( `${textDecoderPolyfill};${bsonSource}` ) - bsonModule.instantiateSync(this.#vm, specifier => { + bsonModule.instantiateSync(this.vm, specifier => { throw new Error(`No imports allowed. Required: ${specifier}`) }) - this.#moduleHandler.registerModule(bsonModule, "{deserialize, toJson}") + this.moduleHandler.registerModule(bsonModule, "{deserialize, toJson}") return this } execute(code: string): string { - const perRequestLimit = this.#perRequestLimit + const perRequestLimit = this.perRequestLimit if (perRequestLimit) { - const cpuMs = Number(this.#isolate.cpuTime) / 1e6 + const cpuMs = Number(this.isolate.cpuTime) / 1e6 if (cpuMs > perRequestLimit) { throw new ExecutionTimeoutError( `CPU time limit exceeded (${cpuMs}ms > ${perRequestLimit}ms)` @@ -208,14 +208,14 @@ export class IsolatedVM implements VM { } } - code = `${this.#moduleHandler.generateImports()};results.out=${this.#codeWrapper( + code = `${this.moduleHandler.generateImports()};results.out=${this.codeWrapper( code )};` - const script = this.#isolate.compileModuleSync(code) + const script = this.isolate.compileModuleSync(code) - script.instantiateSync(this.#vm, specifier => { - const module = this.#moduleHandler.getModule(specifier) + script.instantiateSync(this.vm, specifier => { + const module = this.moduleHandler.getModule(specifier) if (module) { return module } @@ -223,13 +223,13 @@ export class IsolatedVM implements VM { throw new Error(`"${specifier}" import not allowed`) }) - script.evaluateSync({ timeout: this.#timeout }) + script.evaluateSync({ timeout: this.timeout }) - const result = this.#getFromContext(this.#resultKey) + const result = this.getFromContext(this.resultKey) return result.out } - #registerCallbacks(functions: Record) { + private registerCallbacks(functions: Record) { const libId = crypto.randomUUID().replace(/-/g, "") const x: Record = {} @@ -237,7 +237,7 @@ export class IsolatedVM implements VM { const key = `f${libId}${funcName}cb` x[funcName] = key - this.#addToContext({ + this.addToContext({ [key]: new ivm.Callback((...params: any[]) => (func as any)(...params)), }) } @@ -251,17 +251,20 @@ export class IsolatedVM implements VM { return mod } - #addToContext(context: Record) { + private addToContext(context: Record) { for (let key in context) { - this.#jail.setSync( + const value = context[key] + this.jail.setSync( key, - new ivm.ExternalCopy(context[key]).copyInto({ release: true }) + typeof value === "function" + ? value + : new ivm.ExternalCopy(value).copyInto({ release: true }) ) } } - #getFromContext(key: string) { - const ref = this.#vm.global.getSync(key, { reference: true }) + private getFromContext(key: string) { + const ref = this.vm.global.getSync(key, { reference: true }) const result = ref.copySync() ref.release() return result diff --git a/packages/server/src/utilities/scriptRunner.ts b/packages/server/src/utilities/scriptRunner.ts index c2add13eeb..e322b3a6ee 100644 --- a/packages/server/src/utilities/scriptRunner.ts +++ b/packages/server/src/utilities/scriptRunner.ts @@ -2,24 +2,25 @@ import env from "../environment" import { IsolatedVM } from "../jsRunner/vm" const JS_TIMEOUT_MS = 1000 + class ScriptRunner { - #code: string - #vm: IsolatedVM + private code: string + private vm: IsolatedVM constructor(script: string, context: any, { parseBson = false } = {}) { - this.#code = `(() => {${script}})();` - this.#vm = new IsolatedVM({ + this.code = `(() => {${script}})();` + this.vm = new IsolatedVM({ memoryLimit: env.JS_RUNNER_MEMORY_LIMIT, timeout: JS_TIMEOUT_MS, }).withContext(context) if (parseBson && context.data) { - this.#vm = this.#vm.withParsingBson(context.data) + this.vm = this.vm.withParsingBson(context.data) } } execute() { - const result = this.#vm.execute(this.#code) + const result = this.vm.execute(this.code) return result } }