feat: replace vm2 sandbox with isolated-vm

Co-authored-by: Fredrik Adelöw <freben@gmail.com>
Co-authored-by: Patrik Oldsberg <poldsberg@gmail.com>
Co-authored-by: Johan Haals <johan.haals@gmail.com>
Signed-off-by: blam <ben@blam.sh>
This commit is contained in:
blam
2023-06-15 12:07:47 +02:00
parent d6599a7223
commit 7e272d18e1
3 changed files with 90 additions and 65 deletions
+1 -1
View File
@@ -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",
@@ -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<string, any> = {};
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<string, unknown> = {};
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;
}