2
0

🔒 Use vm instead of Function in Node.js (#1509)

This commit is contained in:
Baptiste Arnaud
2024-05-14 14:17:40 +02:00
committed by GitHub
parent 1afa25a015
commit 75c44d61d5
6 changed files with 42 additions and 24 deletions

View File

@ -17,7 +17,6 @@ import { getChatCompletionStream } from '@typebot.io/bot-engine/blocks/integrati
import { ChatCompletionOpenAIOptions } from '@typebot.io/schemas/features/blocks/integrations/openai/schema' import { ChatCompletionOpenAIOptions } from '@typebot.io/schemas/features/blocks/integrations/openai/schema'
import { isForgedBlockType } from '@typebot.io/schemas/features/blocks/forged/helpers' import { isForgedBlockType } from '@typebot.io/schemas/features/blocks/forged/helpers'
export const runtime = 'edge'
export const preferredRegion = 'lhr1' export const preferredRegion = 'lhr1'
export const dynamic = 'force-dynamic' export const dynamic = 'force-dynamic'

View File

@ -11,6 +11,7 @@ import { SessionState } from '@typebot.io/schemas/features/chat/sessionState'
import { ExecuteIntegrationResponse } from '../../../types' import { ExecuteIntegrationResponse } from '../../../types'
import { parseVariables } from '@typebot.io/variables/parseVariables' import { parseVariables } from '@typebot.io/variables/parseVariables'
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession' import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
import vm from 'vm'
type Props = { type Props = {
state: SessionState state: SessionState
@ -55,12 +56,14 @@ export const resumeWebhookExecution = ({
if (!varMapping?.bodyPath || !varMapping.variableId) return newVariables if (!varMapping?.bodyPath || !varMapping.variableId) return newVariables
const existingVariable = typebot.variables.find(byId(varMapping.variableId)) const existingVariable = typebot.variables.find(byId(varMapping.variableId))
if (!existingVariable) return newVariables if (!existingVariable) return newVariables
const func = Function( const sandbox = vm.createContext({
'data', data: response.data,
`return data.${parseVariables(typebot.variables)(varMapping?.bodyPath)}` })
)
try { try {
const value: unknown = func(response) const value: unknown = vm.runInContext(
parseVariables(typebot.variables)(varMapping?.bodyPath),
sandbox
)
return [...newVariables, { ...existingVariable, value }] return [...newVariables, { ...existingVariable, value }]
} catch (err) { } catch (err) {
return newVariables return newVariables

View File

@ -7,6 +7,7 @@ import { parseVariables } from '@typebot.io/variables/parseVariables'
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession' import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
import { createId } from '@paralleldrive/cuid2' import { createId } from '@paralleldrive/cuid2'
import { utcToZonedTime, format as tzFormat } from 'date-fns-tz' import { utcToZonedTime, format as tzFormat } from 'date-fns-tz'
import vm from 'vm'
export const executeSetVariable = ( export const executeSetVariable = (
state: SessionState, state: SessionState,
@ -67,11 +68,16 @@ const evaluateSetVariableExpression =
// To avoid octal number evaluation // To avoid octal number evaluation
if (!isNaN(str as unknown as number) && /0[^.].+/.test(str)) return str if (!isNaN(str as unknown as number) && /0[^.].+/.test(str)) return str
const evaluating = parseVariables(variables, { fieldToParse: 'id' })( const evaluating = parseVariables(variables, { fieldToParse: 'id' })(
str.includes('return ') ? str : `return ${str}` `(function() {${str.includes('return ') ? str : 'return ' + str}})()`
) )
try { try {
const func = Function(...variables.map((v) => v.id), evaluating) const sandbox = vm.createContext({
return func(...variables.map((v) => parseGuessedValueType(v.value))) ...Object.fromEntries(
variables.map((v) => [v.id, parseGuessedValueType(v.value)])
),
fetch,
})
return vm.runInContext(evaluating, sandbox)
} catch (err) { } catch (err) {
return parseVariables(variables)(str) return parseVariables(variables)(str)
} }

View File

@ -75,7 +75,7 @@ export const runChatCompletionStream = async ({
continue continue
} }
const { output } = await executeFunction({ const { output, newVariables } = await executeFunction({
variables: variables.list(), variables: variables.list(),
args: args:
typeof toolCall.func.arguments === 'string' typeof toolCall.func.arguments === 'string'
@ -84,8 +84,7 @@ export const runChatCompletionStream = async ({
body: toolDefinition.code, body: toolDefinition.code,
}) })
// TO-DO: enable once we're out of edge runtime. newVariables?.forEach((v) => variables.set(v.id, v.value))
// newVariables?.forEach((v) => variables.set(v.id, v.value))
const newMessages = appendToolCallMessage({ const newMessages = appendToolCallMessage({
tool_call_id: toolCall.id, tool_call_id: toolCall.id,

View File

@ -4,6 +4,7 @@ import { parseGuessedValueType } from './parseGuessedValueType'
import { isDefined } from '@typebot.io/lib' import { isDefined } from '@typebot.io/lib'
import { safeStringify } from '@typebot.io/lib/safeStringify' import { safeStringify } from '@typebot.io/lib/safeStringify'
import { Variable } from './types' import { Variable } from './types'
import vm from 'vm'
const defaultTimeout = 10 const defaultTimeout = 10
@ -18,7 +19,9 @@ export const executeFunction = async ({
body, body,
args: initialArgs, args: initialArgs,
}: Props) => { }: Props) => {
const parsedBody = parseVariables(variables, { fieldToParse: 'id' })(body) const parsedBody = `(async function() {${parseVariables(variables, {
fieldToParse: 'id',
})(body)}})()`
const args = ( const args = (
extractVariablesFromText(variables)(body).map((variable) => ({ extractVariablesFromText(variables)(body).map((variable) => ({
@ -30,22 +33,25 @@ export const executeFunction = async ({
? Object.entries(initialArgs).map(([id, value]) => ({ id, value })) ? Object.entries(initialArgs).map(([id, value]) => ({ id, value }))
: [] : []
) )
const func = AsyncFunction(
...args.map(({ id }) => id),
'setVariable',
parsedBody
)
let updatedVariables: Record<string, any> = {} let updatedVariables: Record<string, any> = {}
const setVariable = (key: string, value: any) => { const setVariable = (key: string, value: any) => {
updatedVariables[key] = value updatedVariables[key] = value
} }
const context = vm.createContext({
...Object.fromEntries(args.map(({ id, value }) => [id, value])),
setVariable,
fetch,
setTimeout,
})
const timeout = new Timeout() const timeout = new Timeout()
try { try {
const output: unknown = await timeout.wrap( const output: unknown = await timeout.wrap(
func(...args.map(({ value }) => value), setVariable), await vm.runInContext(parsedBody, context),
defaultTimeout * 1000 defaultTimeout * 1000
) )
timeout.clear() timeout.clear()
@ -81,8 +87,6 @@ export const executeFunction = async ({
} }
} }
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor
class Timeout { class Timeout {
private ids: NodeJS.Timeout[] private ids: NodeJS.Timeout[]

View File

@ -2,6 +2,7 @@ import { safeStringify } from '@typebot.io/lib/safeStringify'
import { isDefined, isNotDefined } from '@typebot.io/lib/utils' import { isDefined, isNotDefined } from '@typebot.io/lib/utils'
import { parseGuessedValueType } from './parseGuessedValueType' import { parseGuessedValueType } from './parseGuessedValueType'
import { Variable, VariableWithValue } from './types' import { Variable, VariableWithValue } from './types'
import vm from 'vm'
export type ParseVariablesOptions = { export type ParseVariablesOptions = {
fieldToParse?: 'value' | 'id' fieldToParse?: 'value' | 'id'
@ -73,11 +74,17 @@ const evaluateInlineCode = (
{ variables }: { variables: Variable[] } { variables }: { variables: Variable[] }
) => { ) => {
const evaluating = parseVariables(variables, { fieldToParse: 'id' })( const evaluating = parseVariables(variables, { fieldToParse: 'id' })(
code.includes('return ') ? code : `return ${code}` `(async function() {${
code.includes('return ') ? code : 'return ' + code
}})()`
) )
try { try {
const func = Function(...variables.map((v) => v.id), evaluating) const sandbox = vm.createContext({
return func(...variables.map((v) => parseGuessedValueType(v.value))) ...Object.fromEntries(
variables.map((v) => [v.id, parseGuessedValueType(v.value)])
),
})
return vm.runInContext(evaluating, sandbox)
} catch (err) { } catch (err) {
return parseVariables(variables)(code) return parseVariables(variables)(code)
} }