2
0

🔒 Use isolated-vm

This commit is contained in:
Baptiste Arnaud
2024-05-22 11:42:31 +02:00
parent 15b2901f8a
commit 8d66b52a39
14 changed files with 310 additions and 114 deletions

View File

@@ -0,0 +1,55 @@
import { Variable } from './types'
import ivm from 'isolated-vm'
import { parseGuessedValueType } from './parseGuessedValueType'
export const createCodeRunner = ({ variables }: { variables: Variable[] }) => {
const isolate = new ivm.Isolate()
const context = isolate.createContextSync()
const jail = context.global
jail.setSync('global', jail.derefInto())
variables.forEach((v) => {
jail.setSync(v.id, parseTransferrableValue(parseGuessedValueType(v.value)))
})
return (code: string) =>
context.evalClosureSync(
`return (function() {
return new Function($0)();
}())`,
[code],
{ result: { copy: true }, timeout: 10000 }
)
}
export const createHttpReqResponseMappingRunner = (response: any) => {
const isolate = new ivm.Isolate()
const context = isolate.createContextSync()
const jail = context.global
jail.setSync('global', jail.derefInto())
jail.setSync('response', new ivm.ExternalCopy(response).copyInto())
return (expression: string) => {
return context.evalClosureSync(
`globalThis.evaluateExpression = function(expression) {
try {
// Use Function to safely evaluate the expression
const func = new Function('statusCode', 'data', 'return (' + expression + ')');
return func(response.statusCode, response.data);
} catch (err) {
throw new Error('Invalid expression: ' + err.message);
}
};
return evaluateExpression.apply(null, arguments);`,
[expression],
{
result: { copy: true },
timeout: 10000,
}
)
}
}
const parseTransferrableValue = (value: unknown) => {
if (typeof value === 'object') {
return new ivm.ExternalCopy(value).copyInto()
}
return value
}

View File

@@ -4,9 +4,9 @@ import { parseGuessedValueType } from './parseGuessedValueType'
import { isDefined } from '@typebot.io/lib'
import { safeStringify } from '@typebot.io/lib/safeStringify'
import { Variable } from './types'
import vm from 'vm'
import ivm from 'isolated-vm'
const defaultTimeout = 10
const defaultTimeout = 10 * 1000
type Props = {
variables: Variable[]
@@ -19,9 +19,9 @@ export const executeFunction = async ({
body,
args: initialArgs,
}: Props) => {
const parsedBody = `(async function() {${parseVariables(variables, {
const parsedBody = parseVariables(variables, {
fieldToParse: 'id',
})(body)}})()`
})(body)
const args = (
extractVariablesFromText(variables)(body).map((variable) => ({
@@ -40,21 +40,40 @@ export const executeFunction = async ({
updatedVariables[key] = value
}
const context = vm.createContext({
...Object.fromEntries(args.map(({ id, value }) => [id, value])),
setVariable,
fetch,
setTimeout,
const isolate = new ivm.Isolate()
const context = isolate.createContextSync()
const jail = context.global
jail.setSync('global', jail.derefInto())
context.evalClosure(
'globalThis.setVariable = (...args) => $0.apply(undefined, args, { arguments: { copy: true }, promise: true, result: { copy: true, promise: true } })',
[new ivm.Reference(setVariable)]
)
context.evalClosure(
'globalThis.fetch = (...args) => $0.apply(undefined, args, { arguments: { copy: true }, promise: true, result: { copy: true, promise: true } })',
[
new ivm.Reference(async (...args: any[]) => {
// @ts-ignore
const response = await fetch(...args)
return response.text()
}),
]
)
args.forEach(({ id, value }) => {
jail.setSync(id, value)
})
const timeout = new Timeout()
const run = (code: string) =>
context.evalClosure(
`return (async function() {
const AsyncFunction = async function () {}.constructor;
return new AsyncFunction($0)();
}())`,
[code],
{ result: { copy: true, promise: true }, timeout: defaultTimeout }
)
try {
const output: unknown = await timeout.wrap(
await vm.runInContext(parsedBody, context),
defaultTimeout * 1000
)
timeout.clear()
const output = await run(parsedBody)
console.log('Output', output)
return {
output: safeStringify(output) ?? '',
newVariables: Object.entries(updatedVariables)
@@ -86,33 +105,3 @@ export const executeFunction = async ({
}
}
}
class Timeout {
private ids: NodeJS.Timeout[]
constructor() {
this.ids = []
}
private set = (delay: number) =>
new Promise((_, reject) => {
const id = setTimeout(() => {
reject(`Script ${defaultTimeout}s timeout reached`)
this.clear(id)
}, delay)
this.ids.push(id)
})
wrap = (promise: Promise<any>, delay: number) =>
Promise.race([promise, this.set(delay)])
clear = (...ids: NodeJS.Timeout[]) => {
this.ids = this.ids.filter((id) => {
if (ids.includes(id)) {
clearTimeout(id)
return false
}
return true
})
}
}

View File

@@ -5,6 +5,7 @@
"private": true,
"dependencies": {
"@typebot.io/lib": "workspace:*",
"@typebot.io/tsconfig": "workspace:*"
"@typebot.io/tsconfig": "workspace:*",
"isolated-vm": "4.7.2"
}
}

View File

@@ -1,8 +1,7 @@
import { safeStringify } from '@typebot.io/lib/safeStringify'
import { isDefined, isNotDefined } from '@typebot.io/lib/utils'
import { parseGuessedValueType } from './parseGuessedValueType'
import { Variable, VariableWithValue } from './types'
import vm from 'vm'
import { createCodeRunner } from './codeRunners'
export type ParseVariablesOptions = {
fieldToParse?: 'value' | 'id'
@@ -73,18 +72,10 @@ const evaluateInlineCode = (
code: string,
{ variables }: { variables: Variable[] }
) => {
const evaluating = parseVariables(variables, { fieldToParse: 'id' })(
`(function() {
${code.includes('return ') ? code : 'return ' + code}
})()`
)
try {
const sandbox = vm.createContext({
...Object.fromEntries(
variables.map((v) => [v.id, parseGuessedValueType(v.value)])
),
})
return vm.runInContext(evaluating, sandbox)
return createCodeRunner({ variables })(
parseVariables(variables, { fieldToParse: 'id' })(code)
)
} catch (err) {
return parseVariables(variables)(code)
}