From 7e272d18e10e10a73b554dfe8276f10fe01e3073 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 15 Jun 2023 12:07:47 +0200 Subject: [PATCH] feat: replace vm2 sandbox with isolated-vm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: Patrik Oldsberg Co-authored-by: Johan Haals Signed-off-by: blam --- plugins/scaffolder-backend/package.json | 2 +- .../src/lib/templating/SecureTemplater.ts | 126 +++++++++++------- yarn.lock | 27 ++-- 3 files changed, 90 insertions(+), 65 deletions(-) diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 0df6f20277..1bfbb4786c 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -78,6 +78,7 @@ "git-url-parse": "^13.0.0", "globby": "^11.0.0", "isbinaryfile": "^5.0.0", + "isolated-vm": "^4.5.0", "isomorphic-git": "^1.23.0", "jsonschema": "^1.2.6", "knex": "^2.0.0", @@ -93,7 +94,6 @@ "p-queue": "^6.6.2", "prom-client": "^14.0.1", "uuid": "^8.2.0", - "vm2": "^3.9.18", "winston": "^3.2.1", "yaml": "^2.0.0", "zen-observable": "^0.10.0", diff --git a/plugins/scaffolder-backend/src/lib/templating/SecureTemplater.ts b/plugins/scaffolder-backend/src/lib/templating/SecureTemplater.ts index f0ccb6f192..0a553665c9 100644 --- a/plugins/scaffolder-backend/src/lib/templating/SecureTemplater.ts +++ b/plugins/scaffolder-backend/src/lib/templating/SecureTemplater.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { VM } from 'vm2'; +import { Isolate } from 'isolated-vm'; import { resolvePackagePath } from '@backstage/backend-common'; import fs from 'fs-extra'; import { JsonValue } from '@backstage/types'; @@ -45,20 +45,14 @@ const { render, renderCompat } = (() => { }); compatEnv.addFilter('jsonify', compatEnv.getFilter('dump')); - if (typeof templateFilters !== 'undefined') { - for (const [filterName, filterFn] of Object.entries(templateFilters)) { - env.addFilter(filterName, (...args) => JSON.parse(filterFn(...args))); - } + for (const name of JSON.parse(availableTemplateFilters)) { + env.addFilter(name, (...args) => JSON.parse(callFilter(name, args))); } - - if (typeof templateGlobals !== 'undefined') { - for (const [globalName, global] of Object.entries(templateGlobals)) { - if (typeof global === 'function') { - env.addGlobal(globalName, (...args) => JSON.parse(global(...args))); - } else { - env.addGlobal(globalName, JSON.parse(global)); - } - } + for (const [name, value] of Object.entries(JSON.parse(availableTemplateGlobals))) { + env.addGlobal(name, value); + } + for (const name of JSON.parse(availableTemplateCallbacks)) { + env.addGlobal(name, (...args) => JSON.parse(callGlobal(name, args))); } let uninstallCompat = undefined; @@ -116,35 +110,14 @@ export type SecureTemplateRenderer = ( export class SecureTemplater { static async loadRenderer(options: SecureTemplaterOptions = {}) { - const { cookiecutterCompat, templateFilters, templateGlobals } = options; - const sandbox: Record = {}; - - if (templateFilters) { - sandbox.templateFilters = Object.fromEntries( - Object.entries(templateFilters) - .filter(([_, filterFunction]) => !!filterFunction) - .map(([filterName, filterFunction]) => [ - filterName, - (...args: JsonValue[]) => JSON.stringify(filterFunction(...args)), - ]), - ); - } - if (templateGlobals) { - sandbox.templateGlobals = Object.fromEntries( - Object.entries(templateGlobals) - .filter(([_, global]) => !!global) - .map(([globalName, global]) => { - if (typeof global === 'function') { - return [ - globalName, - (...args: JsonValue[]) => JSON.stringify(global(...args)), - ]; - } - return [globalName, JSON.stringify(global)]; - }), - ); - } - const vm = new VM({ sandbox }); + const { + cookiecutterCompat, + templateFilters = {}, + templateGlobals = {}, + } = options; + const isolate = new Isolate({ memoryLimit: 128 }); + const context = await isolate.createContext(); + const contextGlobal = context.global; const nunjucksSource = await fs.readFile( resolvePackagePath( @@ -154,20 +127,75 @@ export class SecureTemplater { 'utf-8', ); - vm.run(mkScript(nunjucksSource)); + const nunjucksScript = await isolate.compileScript( + mkScript(nunjucksSource), + ); + + const availableFilters = Object.keys(templateFilters); + + await contextGlobal.set( + 'availableTemplateFilters', + JSON.stringify(availableFilters), + ); + + const globalCallbacks = []; + const globalValues: Record = {}; + for (const [name, value] of Object.entries(templateGlobals)) { + if (typeof value === 'function') { + globalCallbacks.push(name); + } else { + globalValues[name] = value; + } + } + + await contextGlobal.set( + 'availableTemplateGlobals', + JSON.stringify(globalValues), + ); + await contextGlobal.set( + 'availableTemplateCallbacks', + JSON.stringify(globalCallbacks), + ); + + await contextGlobal.set( + 'callFilter', + (filterName: string, args: JsonValue[]) => { + if (!Object.hasOwn(templateFilters, filterName)) { + return ''; + } + return JSON.stringify(templateFilters[filterName](...args)); + }, + ); + + await contextGlobal.set( + 'callGlobal', + (globalName: string, args: JsonValue[]) => { + if (!Object.hasOwn(templateGlobals, globalName)) { + return ''; + } + const global = templateGlobals[globalName]; + if (typeof global !== 'function') { + return ''; + } + return JSON.stringify(global(...args)); + }, + ); + + await nunjucksScript.run(context); const render: SecureTemplateRenderer = (template, values) => { - if (!vm) { + if (!context) { throw new Error('SecureTemplater has not been initialized'); } - vm.setGlobal('templateStr', template); - vm.setGlobal('templateValues', JSON.stringify(values)); + + contextGlobal.setSync('templateStr', String(template)); + contextGlobal.setSync('templateValues', JSON.stringify(values)); if (cookiecutterCompat) { - return vm.run(`renderCompat(templateStr, templateValues)`); + return context.evalSync(`renderCompat(templateStr, templateValues)`); } - return vm.run(`render(templateStr, templateValues)`); + return context.evalSync(`render(templateStr, templateValues)`); }; return render; } diff --git a/yarn.lock b/yarn.lock index 5e0ef2cc88..d7a0ba23e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8664,6 +8664,7 @@ __metadata: git-url-parse: ^13.0.0 globby: ^11.0.0 isbinaryfile: ^5.0.0 + isolated-vm: ^4.5.0 isomorphic-git: ^1.23.0 jest-when: ^3.1.0 jsonschema: ^1.2.6 @@ -8683,7 +8684,6 @@ __metadata: prom-client: ^14.0.1 supertest: ^6.1.3 uuid: ^8.2.0 - vm2: ^3.9.18 wait-for-expect: ^3.0.2 winston: ^3.2.1 yaml: ^2.0.0 @@ -18616,7 +18616,7 @@ __metadata: languageName: node linkType: hard -"acorn-walk@npm:^8.1.1, acorn-walk@npm:^8.2.0": +"acorn-walk@npm:^8.1.1": version: 8.2.0 resolution: "acorn-walk@npm:8.2.0" checksum: 1715e76c01dd7b2d4ca472f9c58968516a4899378a63ad5b6c2d668bba8da21a71976c14ec5f5b75f887b6317c4ae0b897ab141c831d741dc76024d8745f1ad1 @@ -18632,7 +18632,7 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.2.4, acorn@npm:^8.4.1, acorn@npm:^8.5.0, acorn@npm:^8.7.0, acorn@npm:^8.7.1, acorn@npm:^8.8.0": +"acorn@npm:^8.2.4, acorn@npm:^8.4.1, acorn@npm:^8.5.0, acorn@npm:^8.7.1, acorn@npm:^8.8.0": version: 8.8.0 resolution: "acorn@npm:8.8.0" bin: @@ -28287,6 +28287,15 @@ __metadata: languageName: node linkType: hard +"isolated-vm@npm:^4.5.0": + version: 4.5.0 + resolution: "isolated-vm@npm:4.5.0" + dependencies: + node-gyp: latest + checksum: 86626a72d35ed0ee731e00ad19bc6e8a0433c4f34a5dd5b1d415d425f1f0f6b0d04c6d10e7a588025b2956d562fcf7a69e9e20f96746978d3a4e7862374a3806 + languageName: node + linkType: hard + "isomorphic-dompurify@npm:^0.13.0": version: 0.13.0 resolution: "isomorphic-dompurify@npm:0.13.0" @@ -40969,18 +40978,6 @@ __metadata: languageName: node linkType: hard -"vm2@npm:^3.9.18": - version: 3.9.19 - resolution: "vm2@npm:3.9.19" - dependencies: - acorn: ^8.7.0 - acorn-walk: ^8.2.0 - bin: - vm2: bin/vm2 - checksum: fc6cf553134145cd7bb5246985bf242b056e3fb5ea71e2eef6710b2a5d6c6119cc6bc960435ff62480ee82efb43369be8f4db07b6690916ae7d3b2e714f395d8 - languageName: node - linkType: hard - "vscode-languageserver-types@npm:^3.15.1": version: 3.15.1 resolution: "vscode-languageserver-types@npm:3.15.1"