🔒 Use isolated-vm
This commit is contained in:
@ -11,7 +11,7 @@ import { SessionState } from '@typebot.io/schemas/features/chat/sessionState'
|
||||
import { ExecuteIntegrationResponse } from '../../../types'
|
||||
import { parseVariables } from '@typebot.io/variables/parseVariables'
|
||||
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
|
||||
import vm from 'vm'
|
||||
import { createHttpReqResponseMappingRunner } from '@typebot.io/variables/codeRunners'
|
||||
|
||||
type Props = {
|
||||
state: SessionState
|
||||
@ -50,19 +50,21 @@ export const resumeWebhookExecution = ({
|
||||
}
|
||||
)
|
||||
|
||||
let run: (varMapping: string) => unknown
|
||||
if (block.options?.responseVariableMapping) {
|
||||
run = createHttpReqResponseMappingRunner(response)
|
||||
}
|
||||
const newVariables = block.options?.responseVariableMapping?.reduce<
|
||||
VariableWithUnknowValue[]
|
||||
>((newVariables, varMapping) => {
|
||||
if (!varMapping?.bodyPath || !varMapping.variableId) return newVariables
|
||||
if (!varMapping?.bodyPath || !varMapping.variableId || !run)
|
||||
return newVariables
|
||||
const existingVariable = typebot.variables.find(byId(varMapping.variableId))
|
||||
if (!existingVariable) return newVariables
|
||||
const sandbox = vm.createContext({
|
||||
data: response,
|
||||
})
|
||||
|
||||
try {
|
||||
const value: unknown = vm.runInContext(
|
||||
`data.${parseVariables(typebot.variables)(varMapping?.bodyPath)}`,
|
||||
sandbox
|
||||
const value: unknown = run(
|
||||
parseVariables(typebot.variables)(varMapping?.bodyPath)
|
||||
)
|
||||
return [...newVariables, { ...existingVariable, value }]
|
||||
} catch (err) {
|
||||
|
@ -8,7 +8,6 @@ import {
|
||||
import { byId, isEmpty } from '@typebot.io/lib'
|
||||
import { ExecuteLogicResponse } from '../../../types'
|
||||
import { parseScriptToExecuteClientSideAction } from '../script/executeScript'
|
||||
import { parseGuessedValueType } from '@typebot.io/variables/parseGuessedValueType'
|
||||
import { parseVariables } from '@typebot.io/variables/parseVariables'
|
||||
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
@ -19,7 +18,7 @@ import {
|
||||
} from '@typebot.io/logic/computeResultTranscript'
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
import { sessionOnlySetVariableOptions } from '@typebot.io/schemas/features/blocks/logic/setVariable/constants'
|
||||
import vm from 'vm'
|
||||
import { createCodeRunner } from '@typebot.io/variables/codeRunners'
|
||||
|
||||
export const executeSetVariable = async (
|
||||
state: SessionState,
|
||||
@ -97,17 +96,11 @@ const evaluateSetVariableExpression =
|
||||
if (isSingleVariable) return parseVariables(variables)(str)
|
||||
// To avoid octal number evaluation
|
||||
if (!isNaN(str as unknown as number) && /0[^.].+/.test(str)) return str
|
||||
const evaluating = parseVariables(variables, { fieldToParse: 'id' })(
|
||||
`(function() {${str.includes('return ') ? str : 'return ' + str}})()`
|
||||
)
|
||||
try {
|
||||
const sandbox = vm.createContext({
|
||||
...Object.fromEntries(
|
||||
variables.map((v) => [v.id, parseGuessedValueType(v.value)])
|
||||
),
|
||||
fetch,
|
||||
})
|
||||
return vm.runInContext(evaluating, sandbox)
|
||||
const body = parseVariables(variables, { fieldToParse: 'id' })(str)
|
||||
return createCodeRunner({ variables })(
|
||||
body.includes('return ') ? body : `return ${body}`
|
||||
)
|
||||
} catch (err) {
|
||||
return parseVariables(variables)(str)
|
||||
}
|
||||
|
@ -32,7 +32,8 @@
|
||||
"nodemailer": "6.9.8",
|
||||
"openai": "4.47.1",
|
||||
"qs": "6.11.2",
|
||||
"stripe": "12.13.0"
|
||||
"stripe": "12.13.0",
|
||||
"isolated-vm": "4.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typebot.io/forge": "workspace:*",
|
||||
|
55
packages/variables/codeRunners.ts
Normal file
55
packages/variables/codeRunners.ts
Normal 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
|
||||
}
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@typebot.io/lib": "workspace:*",
|
||||
"@typebot.io/tsconfig": "workspace:*"
|
||||
"@typebot.io/tsconfig": "workspace:*",
|
||||
"isolated-vm": "4.7.2"
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
Reference in New Issue
Block a user