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;
}
+12 -15
View File
@@ -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"