Refactor/Update code execution sandbox implementation across components (#4904)

refactor: Update code execution sandbox implementation across components

- Replaced NodeVM usage with a new createCodeExecutionSandbox function for improved sandbox management.
- Enhanced JavaScript code execution with executeJavaScriptCode function, allowing for better handling of libraries and output streaming.
- Updated multiple components to utilize the new sandboxing approach, ensuring consistent execution environment.
- Added validation for UUIDs and URLs in various tools to enhance input safety.
- Refactored input handling in CustomFunction and IfElseFunction to streamline variable management.
This commit is contained in:
Henry Heng
2025-07-21 00:09:01 +01:00
committed by GitHub
parent 9a06a85a8d
commit dca91b979b
24 changed files with 550 additions and 488 deletions
+226
View File
@@ -16,6 +16,8 @@ import { GetSecretValueCommand, SecretsManagerClient, SecretsManagerClientConfig
import { customGet } from '../nodes/sequentialagents/commonUtils'
import { TextSplitter } from 'langchain/text_splitter'
import { DocumentLoader } from 'langchain/document_loaders/base'
import { NodeVM } from '@flowiseai/nodevm'
import { Sandbox } from '@e2b/code-interpreter'
export const numberOrExpressionRegex = '^(\\d+\\.?\\d*|{{.*}})$' //return true if string consists only numbers OR expression {{}}
export const notEmptyRegex = '(.|\\s)*\\S(.|\\s)*' //return true if string is not empty or blank
@@ -1348,3 +1350,227 @@ export const stripHTMLFromToolInput = (input: string) => {
cleanedInput = cleanedInput.replace(/\\_/g, '_')
return cleanedInput
}
// Helper function to convert require statements to ESM imports
const convertRequireToImport = (requireLine: string): string | null => {
// Remove leading/trailing whitespace and get the indentation
const indent = requireLine.match(/^(\s*)/)?.[1] || ''
const trimmed = requireLine.trim()
// Match patterns like: const/let/var name = require('module')
const defaultRequireMatch = trimmed.match(/^(const|let|var)\s+(\w+)\s*=\s*require\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/)
if (defaultRequireMatch) {
const [, , varName, moduleName] = defaultRequireMatch
return `${indent}import ${varName} from '${moduleName}';`
}
// Match patterns like: const { name1, name2 } = require('module')
const destructureMatch = trimmed.match(/^(const|let|var)\s+\{\s*([^}]+)\s*\}\s*=\s*require\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/)
if (destructureMatch) {
const [, , destructuredVars, moduleName] = destructureMatch
return `${indent}import { ${destructuredVars.trim()} } from '${moduleName}';`
}
// Match patterns like: const name = require('module').property
const propertyMatch = trimmed.match(/^(const|let|var)\s+(\w+)\s*=\s*require\s*\(\s*['"`]([^'"`]+)['"`]\s*\)\.(\w+)/)
if (propertyMatch) {
const [, , varName, moduleName, property] = propertyMatch
return `${indent}import { ${property} as ${varName} } from '${moduleName}';`
}
// If no pattern matches, return null to skip conversion
return null
}
/**
* Execute JavaScript code using either Sandbox or NodeVM
* @param {string} code - The JavaScript code to execute
* @param {ICommonObject} sandbox - The sandbox object with variables
* @param {ICommonObject} options - Execution options
* @returns {Promise<any>} - The execution result
*/
export const executeJavaScriptCode = async (
code: string,
sandbox: ICommonObject,
options: {
timeout?: number
useSandbox?: boolean
libraries?: string[]
streamOutput?: (output: string) => void
nodeVMOptions?: ICommonObject
} = {}
): Promise<any> => {
const { timeout = 10000, useSandbox = true, streamOutput, libraries = [], nodeVMOptions = {} } = options
const shouldUseSandbox = useSandbox && process.env.E2B_APIKEY
if (shouldUseSandbox) {
try {
const variableDeclarations = []
if (sandbox['$vars']) {
variableDeclarations.push(`const $vars = ${JSON.stringify(sandbox['$vars'])};`)
}
if (sandbox['$flow']) {
variableDeclarations.push(`const $flow = ${JSON.stringify(sandbox['$flow'])};`)
}
// Add other sandbox variables
for (const [key, value] of Object.entries(sandbox)) {
if (
key !== '$vars' &&
key !== '$flow' &&
key !== 'util' &&
key !== 'Symbol' &&
key !== 'child_process' &&
key !== 'fs' &&
key !== 'process'
) {
variableDeclarations.push(`const ${key} = ${JSON.stringify(value)};`)
}
}
// Handle import statements properly - they must be at the top
const lines = code.split('\n')
const importLines = []
const otherLines = []
for (const line of lines) {
const trimmedLine = line.trim()
// Skip node-fetch imports since Node.js has built-in fetch
if (trimmedLine.includes('node-fetch') || trimmedLine.includes("'fetch'") || trimmedLine.includes('"fetch"')) {
continue // Skip this line entirely
}
// Check for existing ES6 imports and exports
if (trimmedLine.startsWith('import ') || trimmedLine.startsWith('export ')) {
importLines.push(line)
}
// Check for CommonJS require statements and convert them to ESM imports
else if (/^(const|let|var)\s+.*=\s*require\s*\(/.test(trimmedLine)) {
const convertedImport = convertRequireToImport(trimmedLine)
if (convertedImport) {
importLines.push(convertedImport)
}
} else {
otherLines.push(line)
}
}
const sbx = await Sandbox.create({ apiKey: process.env.E2B_APIKEY })
// Install libraries
for (const library of libraries) {
await sbx.commands.run(`npm install ${library}`)
}
// Separate imports from the rest of the code for proper ES6 module structure
const codeWithImports = [
...importLines,
`module.exports = async function() {`,
...variableDeclarations,
...otherLines,
`}()`
].join('\n')
const execution = await sbx.runCode(codeWithImports, { language: 'js' })
let output = ''
if (execution.text) output = execution.text
if (!execution.text && execution.logs.stdout.length) output = execution.logs.stdout.join('\n')
if (execution.error) {
throw new Error(`${execution.error.name}: ${execution.error.value}`)
}
if (execution.logs.stderr.length) {
throw new Error(execution.logs.stderr.join('\n'))
}
// Stream output if streaming function provided
if (streamOutput && output) {
streamOutput(output)
}
// Clean up sandbox
sbx.kill()
return output
} catch (e) {
throw new Error(`Sandbox Execution Error: ${e}`)
}
} else {
const builtinDeps = process.env.TOOL_FUNCTION_BUILTIN_DEP
? defaultAllowBuiltInDep.concat(process.env.TOOL_FUNCTION_BUILTIN_DEP.split(','))
: defaultAllowBuiltInDep
const externalDeps = process.env.TOOL_FUNCTION_EXTERNAL_DEP ? process.env.TOOL_FUNCTION_EXTERNAL_DEP.split(',') : []
const deps = availableDependencies.concat(externalDeps)
const defaultNodeVMOptions: any = {
console: 'inherit',
sandbox,
require: {
external: { modules: deps },
builtin: builtinDeps
},
eval: false,
wasm: false,
timeout
}
// Merge with custom nodeVMOptions if provided
const finalNodeVMOptions = { ...defaultNodeVMOptions, ...nodeVMOptions }
const vm = new NodeVM(finalNodeVMOptions)
try {
const response = await vm.run(`module.exports = async function() {${code}}()`, __dirname)
let finalOutput = response
if (typeof response === 'object') {
finalOutput = JSON.stringify(response, null, 2)
}
// Stream output if streaming function provided
if (streamOutput && finalOutput) {
streamOutput(finalOutput)
}
return finalOutput
} catch (e) {
throw new Error(`NodeVM Execution Error: ${e}`)
}
}
}
/**
* Create a standard sandbox object for code execution
* @param {string} input - The input string
* @param {ICommonObject} variables - Variables from getVars
* @param {ICommonObject} flow - Flow object with chatflowId, sessionId, etc.
* @param {ICommonObject} additionalSandbox - Additional sandbox variables
* @returns {ICommonObject} - The sandbox object
*/
export const createCodeExecutionSandbox = (
input: string,
variables: IVariable[],
flow: ICommonObject,
additionalSandbox: ICommonObject = {}
): ICommonObject => {
const sandbox: ICommonObject = {
$input: input,
util: undefined,
Symbol: undefined,
child_process: undefined,
fs: undefined,
process: undefined,
...additionalSandbox
}
sandbox['$vars'] = prepareSandboxVars(variables)
sandbox['$flow'] = flow
return sandbox
}
+14
View File
@@ -9,6 +9,20 @@ export const isValidUUID = (uuid: string): boolean => {
return uuidV4Pattern.test(uuid)
}
/**
* Validates if a string is a valid URL
* @param {string} url The string to validate
* @returns {boolean} True if valid URL, false otherwise
*/
export const isValidURL = (url: string): boolean => {
try {
new URL(url)
return true
} catch {
return false
}
}
/**
* Validates if a string contains path traversal attempts
* @param {string} path The string to validate