Skip to content

Commit

Permalink
🔒 Use isolated-vm
Browse files Browse the repository at this point in the history
  • Loading branch information
baptisteArno committed May 22, 2024
1 parent 15b2901 commit 8d66b52
Show file tree
Hide file tree
Showing 14 changed files with 310 additions and 114 deletions.
15 changes: 4 additions & 11 deletions apps/builder/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -49,21 +49,14 @@ const nextConfig = {
},
experimental: {
outputFileTracingRoot: join(__dirname, '../../'),
serverComponentsExternalPackages: ['isolated-vm'],
},
webpack: (config, { nextRuntime }) => {
if (nextRuntime === 'nodejs') return config
webpack: (config, { isServer }) => {
if (isServer) return config

if (nextRuntime === 'edge') {
config.resolve.alias['minio'] = false
config.resolve.alias['got'] = false
config.resolve.alias['qrcode'] = false
return config
}
// These packages are imports from the integrations definition files that can be ignored for the client.
config.resolve.alias['minio'] = false
config.resolve.alias['got'] = false
config.resolve.alias['openai'] = false
config.resolve.alias['qrcode'] = false
config.resolve.alias['isolated-vm'] = false
return config
},
headers: async () => {
Expand Down
9 changes: 5 additions & 4 deletions apps/builder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
"format:check": "prettier --check ./src --ignore-path ../../.prettierignore"
},
"dependencies": {
"@typebot.io/theme": "workspace:*",
"@braintree/sanitize-url": "7.0.1",
"@chakra-ui/anatomy": "2.1.1",
"@chakra-ui/react": "2.7.1",
Expand Down Expand Up @@ -45,6 +44,7 @@
"@typebot.io/env": "workspace:*",
"@typebot.io/js": "workspace:*",
"@typebot.io/nextjs": "workspace:*",
"@typebot.io/theme": "workspace:*",
"@udecode/cn": "29.0.1",
"@udecode/plate-basic-marks": "30.5.3",
"@udecode/plate-common": "30.4.5",
Expand All @@ -68,9 +68,10 @@
"framer-motion": "10.3.0",
"google-auth-library": "8.9.0",
"google-spreadsheet": "4.1.1",
"ky": "1.2.3",
"immer": "10.0.2",
"isolated-vm": "4.7.2",
"jsonwebtoken": "9.0.1",
"ky": "1.2.3",
"libphonenumber-js": "1.10.37",
"micro": "10.0.1",
"micro-cors": "0.1.1",
Expand Down Expand Up @@ -123,13 +124,13 @@
"@types/qs": "6.9.7",
"@types/react": "18.2.15",
"@types/tinycolor2": "1.4.3",
"dotenv": "16.4.5",
"dotenv-cli": "7.4.1",
"eslint": "8.44.0",
"eslint-config-custom": "workspace:*",
"next-runtime-env": "1.6.2",
"superjson": "1.12.4",
"typescript": "5.4.5",
"zod": "3.22.4",
"dotenv": "16.4.5"
"zod": "3.22.4"
}
}
5 changes: 5 additions & 0 deletions apps/docs/editor/blocks/integrations/openai.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ As you can see, the code block expects the body of the Javascript function. You

If you'd like to set variables directly in this code block, you can use the [`setVariable` function](../logic/script#setvariable-function).

<Warning>
A function is executed on the server so it comes with [some limitations listed
here](../logic/script#limitations-on-scripts-executed-on-server).
</Warning>

## Ask assistant

This action allows you to talk with your [OpenAI assistant](https://platform.openai.com/assistants). All you have to do is to provide its ID.
Expand Down
33 changes: 32 additions & 1 deletion apps/docs/editor/blocks/logic/script.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ You need to write `console.log({{My variable}})` instead of `console.log("{{My v

If you want to set a variable value with Javascript, the [Set variable block](./set-variable) is more appropriate for most cases.

However, if you'd like to set variables with the script blocks, you can use the `setVariable` function in your script:
However, if you'd like to set variables in a Script block, you can use the `setVariable` function in your script:

```js
if({{My variable}} === 'foo') {
Expand All @@ -34,6 +34,37 @@ if({{My variable}} === 'foo') {

The `setVariable` function is only available in script executed on the server, so it won't work if the `Execute on client?` is checked.

## Limitations on scripts executed on server

Because the script is executed on a isolated and secured environment, there are some limitations.

- Global functions like `console.log`, `setTimeout`, `setInterval`, etc. are not available
- The `fetch` function behavior is slightly different from the native `fetch` function. You just have to skip the `await response.text()` or `await response.json()` part.

```js
// ❌ This throws an error
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1')
const data = await response.text()

// ✅ This works
const data = await fetch('https://jsonplaceholder.typicode.com/todos/1')
```

`response` will always be a `string` even if the the request returns a JSON object. If you know that the response is a JSON object, you can parse it using `JSON.parse(response)`.

```js
// ❌ This throws an error
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1')
const data = await response.json()
// ✅ This works
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1')
const data = JSON.parse(response)
```

- You can't use `import` or `require` to import external libraries
- You don't have access to browser APIs like `window`, `document`, `localStorage`, etc. If you need to use browser APIs, you should check the `Execute on client?` option so that the script is executed on the user's browser.
## Examples
### Reload page
Expand Down
15 changes: 4 additions & 11 deletions apps/viewer/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -50,21 +50,14 @@ const nextConfig = {
output: 'standalone',
experimental: {
outputFileTracingRoot: join(__dirname, '../../'),
serverComponentsExternalPackages: ['isolated-vm'],
},
webpack: (config, { nextRuntime }) => {
if (nextRuntime === 'nodejs') return config
webpack: (config, { isServer }) => {
if (isServer) return config

if (nextRuntime === 'edge') {
config.resolve.alias['minio'] = false
config.resolve.alias['got'] = false
config.resolve.alias['qrcode'] = false
return config
}
// These packages are imports from the integrations definition files that can be ignored for the client.
config.resolve.alias['minio'] = false
config.resolve.alias['got'] = false
config.resolve.alias['openai'] = false
config.resolve.alias['qrcode'] = false
config.resolve.alias['isolated-vm'] = false
return config
},
async redirects() {
Expand Down
10 changes: 5 additions & 5 deletions apps/viewer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"cors": "2.8.5",
"google-spreadsheet": "4.1.1",
"got": "12.6.0",
"isolated-vm": "4.7.2",
"ky": "1.2.3",
"next": "14.1.0",
"nextjs-cors": "2.1.2",
Expand All @@ -37,7 +38,6 @@
"stripe": "12.13.0"
},
"devDependencies": {
"dotenv": "16.4.5",
"@faire/mjml-react": "3.3.0",
"@paralleldrive/cuid2": "2.2.1",
"@playwright/test": "1.43.1",
Expand All @@ -46,6 +46,8 @@
"@typebot.io/forge": "workspace:*",
"@typebot.io/forge-repository": "workspace:*",
"@typebot.io/lib": "workspace:*",
"@typebot.io/playwright": "workspace:*",
"@typebot.io/results": "workspace:*",
"@typebot.io/schemas": "workspace:*",
"@typebot.io/tsconfig": "workspace:*",
"@typebot.io/variables": "workspace:*",
Expand All @@ -55,17 +57,15 @@
"@types/papaparse": "5.3.7",
"@types/qs": "6.9.7",
"@types/react": "18.2.15",
"dotenv-cli": "7.4.1",
"dotenv": "16.4.5",
"dotenv-cli": "7.4.1",
"eslint": "8.44.0",
"eslint-config-custom": "workspace:*",
"google-auth-library": "8.9.0",
"next-runtime-env": "1.6.2",
"papaparse": "5.4.1",
"superjson": "1.12.4",
"typescript": "5.4.5",
"zod": "3.22.4",
"@typebot.io/playwright": "workspace:*",
"@typebot.io/results": "workspace:*"
"zod": "3.22.4"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
17 changes: 5 additions & 12 deletions packages/bot-engine/blocks/logic/setVariable/executeSetVariable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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,
Expand Down Expand Up @@ -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)
}
Expand Down
3 changes: 2 additions & 1 deletion packages/bot-engine/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*",
Expand Down
55 changes: 55 additions & 0 deletions packages/variables/codeRunners.ts
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 8d66b52

Please sign in to comment.