diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bda3b5e11..90a7acafb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -130,8 +130,9 @@ Flowise support different environment variables to configure your instance. You | LOG_PATH | Location where log files are stored | String | `your-path/Flowise/logs` | | LOG_LEVEL | Different levels of logs | Enum String: `error`, `info`, `verbose`, `debug` | `info` | | LOG_JSON_SPACES | Spaces to beautify JSON logs | | 2 | -| TOOL_FUNCTION_BUILTIN_DEP | NodeJS built-in modules to be used for Tool Function | String | | -| TOOL_FUNCTION_EXTERNAL_DEP | External modules to be used for Tool Function | String | | +| TOOL_FUNCTION_BUILTIN_DEP | NodeJS built-in modules to be used for Custom Tool or Function | String | | +| TOOL_FUNCTION_EXTERNAL_DEP | External modules to be used for Custom Tool or Function | String | | +| ALLOW_BUILTIN_DEP | Allow project dependencies to be used for Custom Tool or Function | Boolean | false | | DATABASE_TYPE | Type of database to store the flowise data | Enum String: `sqlite`, `mysql`, `postgres` | `sqlite` | | DATABASE_PATH | Location where database is saved (When DATABASE_TYPE is sqlite) | String | `your-home-dir/.flowise` | | DATABASE_HOST | Host URL or IP address (When DATABASE_TYPE is not sqlite) | String | | diff --git a/docker/.env.example b/docker/.env.example index 7e72923e9..8bb6afc5a 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -39,6 +39,7 @@ LOG_PATH=/root/.flowise/logs # LOG_LEVEL=info #(error | warn | info | verbose | debug) # TOOL_FUNCTION_BUILTIN_DEP=crypto,fs # TOOL_FUNCTION_EXTERNAL_DEP=moment,lodash +# ALLOW_BUILTIN_DEP=false ############################################################################################################ @@ -162,4 +163,13 @@ JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200 # REDIS_KEY= # REDIS_CA= # REDIS_KEEP_ALIVE= -# ENABLE_BULLMQ_DASHBOARD= \ No newline at end of file +# ENABLE_BULLMQ_DASHBOARD= + + +############################################################################################################ +############################################## SECURITY #################################################### +############################################################################################################ + +# HTTP_DENY_LIST= +# CUSTOM_MCP_SECURITY_CHECK=true +# CUSTOM_MCP_PROTOCOL=sse #(stdio | sse) \ No newline at end of file diff --git a/docker/docker-compose-queue-prebuilt.yml b/docker/docker-compose-queue-prebuilt.yml index 3777cd9d1..af881f0f4 100644 --- a/docker/docker-compose-queue-prebuilt.yml +++ b/docker/docker-compose-queue-prebuilt.yml @@ -47,9 +47,10 @@ services: - LOG_PATH=${LOG_PATH} - LOG_LEVEL=${LOG_LEVEL} - # CUSTOM TOOL DEPENDENCIES + # CUSTOM TOOL/FUNCTION DEPENDENCIES - TOOL_FUNCTION_BUILTIN_DEP=${TOOL_FUNCTION_BUILTIN_DEP} - TOOL_FUNCTION_EXTERNAL_DEP=${TOOL_FUNCTION_EXTERNAL_DEP} + - ALLOW_BUILTIN_DEP=${ALLOW_BUILTIN_DEP} # STORAGE - STORAGE_TYPE=${STORAGE_TYPE} @@ -138,6 +139,11 @@ services: - REDIS_CA=${REDIS_CA} - REDIS_KEEP_ALIVE=${REDIS_KEEP_ALIVE} - ENABLE_BULLMQ_DASHBOARD=${ENABLE_BULLMQ_DASHBOARD} + + # SECURITY + - CUSTOM_MCP_SECURITY_CHECK=${CUSTOM_MCP_SECURITY_CHECK} + - CUSTOM_MCP_PROTOCOL=${CUSTOM_MCP_PROTOCOL} + - HTTP_DENY_LIST=${HTTP_DENY_LIST} healthcheck: test: ['CMD', 'curl', '-f', 'http://localhost:${PORT:-3000}/api/v1/ping'] interval: 10s @@ -183,9 +189,10 @@ services: - LOG_PATH=${LOG_PATH} - LOG_LEVEL=${LOG_LEVEL} - # CUSTOM TOOL DEPENDENCIES + # CUSTOM TOOL/FUNCTION DEPENDENCIES - TOOL_FUNCTION_BUILTIN_DEP=${TOOL_FUNCTION_BUILTIN_DEP} - TOOL_FUNCTION_EXTERNAL_DEP=${TOOL_FUNCTION_EXTERNAL_DEP} + - ALLOW_BUILTIN_DEP=${ALLOW_BUILTIN_DEP} # STORAGE - STORAGE_TYPE=${STORAGE_TYPE} @@ -274,6 +281,11 @@ services: - REDIS_CA=${REDIS_CA} - REDIS_KEEP_ALIVE=${REDIS_KEEP_ALIVE} - ENABLE_BULLMQ_DASHBOARD=${ENABLE_BULLMQ_DASHBOARD} + + # SECURITY + - CUSTOM_MCP_SECURITY_CHECK=${CUSTOM_MCP_SECURITY_CHECK} + - CUSTOM_MCP_PROTOCOL=${CUSTOM_MCP_PROTOCOL} + - HTTP_DENY_LIST=${HTTP_DENY_LIST} healthcheck: test: ['CMD', 'curl', '-f', 'http://localhost:${WORKER_PORT:-5566}/healthz'] interval: 10s diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 3f7529983..2ffcfb0b9 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -32,9 +32,10 @@ services: - LOG_PATH=${LOG_PATH} - LOG_LEVEL=${LOG_LEVEL} - # CUSTOM TOOL DEPENDENCIES + # CUSTOM TOOL/FUNCTION DEPENDENCIES - TOOL_FUNCTION_BUILTIN_DEP=${TOOL_FUNCTION_BUILTIN_DEP} - TOOL_FUNCTION_EXTERNAL_DEP=${TOOL_FUNCTION_EXTERNAL_DEP} + - ALLOW_BUILTIN_DEP=${ALLOW_BUILTIN_DEP} # STORAGE - STORAGE_TYPE=${STORAGE_TYPE} @@ -123,6 +124,11 @@ services: - REDIS_CA=${REDIS_CA} - REDIS_KEEP_ALIVE=${REDIS_KEEP_ALIVE} - ENABLE_BULLMQ_DASHBOARD=${ENABLE_BULLMQ_DASHBOARD} + + # SECURITY + - CUSTOM_MCP_SECURITY_CHECK=${CUSTOM_MCP_SECURITY_CHECK} + - CUSTOM_MCP_PROTOCOL=${CUSTOM_MCP_PROTOCOL} + - HTTP_DENY_LIST=${HTTP_DENY_LIST} ports: - '${PORT}:${PORT}' healthcheck: diff --git a/docker/worker/.env.example b/docker/worker/.env.example index 0540cf768..8bc6afd52 100644 --- a/docker/worker/.env.example +++ b/docker/worker/.env.example @@ -39,6 +39,7 @@ LOG_PATH=/root/.flowise/logs # LOG_LEVEL=info #(error | warn | info | verbose | debug) # TOOL_FUNCTION_BUILTIN_DEP=crypto,fs # TOOL_FUNCTION_EXTERNAL_DEP=moment,lodash +# ALLOW_BUILTIN_DEP=false ############################################################################################################ @@ -162,4 +163,13 @@ JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200 # REDIS_KEY= # REDIS_CA= # REDIS_KEEP_ALIVE= -# ENABLE_BULLMQ_DASHBOARD= \ No newline at end of file +# ENABLE_BULLMQ_DASHBOARD= + + +############################################################################################################ +############################################## SECURITY #################################################### +############################################################################################################ + +# HTTP_DENY_LIST= +# CUSTOM_MCP_SECURITY_CHECK=true +# CUSTOM_MCP_PROTOCOL=sse #(stdio | sse) \ No newline at end of file diff --git a/docker/worker/docker-compose.yml b/docker/worker/docker-compose.yml index 4a8924dd2..b49e3dcc6 100644 --- a/docker/worker/docker-compose.yml +++ b/docker/worker/docker-compose.yml @@ -32,9 +32,10 @@ services: - LOG_PATH=${LOG_PATH} - LOG_LEVEL=${LOG_LEVEL} - # CUSTOM TOOL DEPENDENCIES + # CUSTOM TOOL/FUNCTION DEPENDENCIES - TOOL_FUNCTION_BUILTIN_DEP=${TOOL_FUNCTION_BUILTIN_DEP} - TOOL_FUNCTION_EXTERNAL_DEP=${TOOL_FUNCTION_EXTERNAL_DEP} + - ALLOW_BUILTIN_DEP=${ALLOW_BUILTIN_DEP} # STORAGE - STORAGE_TYPE=${STORAGE_TYPE} @@ -123,6 +124,12 @@ services: - REDIS_CA=${REDIS_CA} - REDIS_KEEP_ALIVE=${REDIS_KEEP_ALIVE} - ENABLE_BULLMQ_DASHBOARD=${ENABLE_BULLMQ_DASHBOARD} + + # SECURITY + - CUSTOM_MCP_SECURITY_CHECK=${CUSTOM_MCP_SECURITY_CHECK} + - CUSTOM_MCP_PROTOCOL=${CUSTOM_MCP_PROTOCOL} + - HTTP_DENY_LIST=${HTTP_DENY_LIST} + ports: - '${WORKER_PORT}:${WORKER_PORT}' healthcheck: diff --git a/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/FlowiseChatGoogleGenerativeAI.ts b/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/FlowiseChatGoogleGenerativeAI.ts index dcc3c5c5d..5c21fa4ca 100644 --- a/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/FlowiseChatGoogleGenerativeAI.ts +++ b/packages/components/nodes/chatmodels/ChatGoogleGenerativeAI/FlowiseChatGoogleGenerativeAI.ts @@ -770,6 +770,12 @@ export class LangchainChatGoogleGenerativeAI this.client.systemInstruction = systemInstruction actualPrompt = prompt.slice(1) } + + // Ensure actualPrompt is never empty + if (actualPrompt.length === 0) { + actualPrompt = [{ role: 'user', parts: [{ text: '...' }] }] + } + const parameters = this.invocationParams(options) // Handle streaming @@ -834,6 +840,12 @@ export class LangchainChatGoogleGenerativeAI this.client.systemInstruction = systemInstruction actualPrompt = prompt.slice(1) } + + // Ensure actualPrompt is never empty + if (actualPrompt.length === 0) { + actualPrompt = [{ role: 'user', parts: [{ text: '...' }] }] + } + const parameters = this.invocationParams(options) const request = { ...parameters, diff --git a/packages/components/nodes/documentloaders/ApifyWebsiteContentCrawler/apify-symbol-transparent.svg b/packages/components/nodes/documentloaders/ApifyWebsiteContentCrawler/apify-symbol-transparent.svg index 457caaaaa..c8894c844 100644 --- a/packages/components/nodes/documentloaders/ApifyWebsiteContentCrawler/apify-symbol-transparent.svg +++ b/packages/components/nodes/documentloaders/ApifyWebsiteContentCrawler/apify-symbol-transparent.svg @@ -1,5 +1,12 @@ - - - - + + + + + + + + + + + diff --git a/packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts b/packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts index d38fbcb6f..09015a710 100644 --- a/packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts +++ b/packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts @@ -1,6 +1,6 @@ import { Tool } from '@langchain/core/tools' import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeOptionsValue, INodeParams } from '../../../../src/Interface' -import { MCPToolkit } from '../core' +import { MCPToolkit, validateMCPServerConfig } from '../core' import { getVars, prepareSandboxVars, parseJsonBody } from '../../../../src/utils' import { DataSource } from 'typeorm' import hash from 'object-hash' @@ -74,8 +74,8 @@ class Custom_MCP implements INode { }, placeholder: mcpServerConfig, warning: - process.env.CUSTOM_MCP_SECURITY_CHECK === 'true' - ? 'In next release, only Remote MCP with url is supported. Read more here' + process.env.CUSTOM_MCP_PROTOCOL === 'sse' + ? 'Only Remote MCP with url is supported. Read more here' : undefined }, { @@ -173,6 +173,14 @@ class Custom_MCP implements INode { serverParams = JSON.parse(serverParamsString) } + if (process.env.CUSTOM_MCP_SECURITY_CHECK !== 'false') { + try { + validateMCPServerConfig(serverParams) + } catch (error) { + throw new Error(`Security validation failed: ${error.message}`) + } + } + // Compatible with stdio and SSE let toolkit: MCPToolkit if (process.env.CUSTOM_MCP_PROTOCOL === 'sse') { diff --git a/packages/components/nodes/tools/MCP/Supergateway/SupergatewayMCP.ts b/packages/components/nodes/tools/MCP/Supergateway/SupergatewayMCP.ts index 610ef5b79..1960928e6 100644 --- a/packages/components/nodes/tools/MCP/Supergateway/SupergatewayMCP.ts +++ b/packages/components/nodes/tools/MCP/Supergateway/SupergatewayMCP.ts @@ -1,7 +1,7 @@ import { Tool } from '@langchain/core/tools' import { ICommonObject, INode, INodeData, INodeOptionsValue, INodeParams } from '../../../../src/Interface' import { getNodeModulesPackagePath } from '../../../../src/utils' -import { MCPToolkit, validateArgsForLocalFileAccess } from '../core' +import { MCPToolkit, validateMCPServerConfig } from '../core' class Supergateway_MCP implements INode { label: string @@ -106,9 +106,9 @@ class Supergateway_MCP implements INode { args: [packagePath, ...processedArgs] } - if (process.env.CUSTOM_MCP_SECURITY_CHECK === 'true') { + if (process.env.CUSTOM_MCP_SECURITY_CHECK !== 'false') { try { - validateArgsForLocalFileAccess(processedArgs) + validateMCPServerConfig(serverParams) } catch (error) { throw new Error(`Security validation failed: ${error.message}`) } diff --git a/packages/components/nodes/tools/MCP/core.ts b/packages/components/nodes/tools/MCP/core.ts index b2c9e63f6..12bc4f979 100644 --- a/packages/components/nodes/tools/MCP/core.ts +++ b/packages/components/nodes/tools/MCP/core.ts @@ -219,3 +219,67 @@ export const validateArgsForLocalFileAccess = (args: string[]): void => { } } } + +export const validateCommandInjection = (args: string[]): void => { + const dangerousPatterns = [ + // Shell metacharacters + /[;&|`$(){}[\]<>]/, + // Command chaining + /&&|\|\||;;/, + // Redirections + />>|<<|>/, + // Backticks and command substitution + /`|\$\(/, + // Process substitution + /<\(|>\(/ + ] + + for (const arg of args) { + if (typeof arg !== 'string') continue + + for (const pattern of dangerousPatterns) { + if (pattern.test(arg)) { + throw new Error(`Argument contains potentially dangerous characters: "${arg}"`) + } + } + } +} + +export const validateEnvironmentVariables = (env: Record): void => { + const dangerousEnvVars = ['PATH', 'LD_LIBRARY_PATH', 'DYLD_LIBRARY_PATH'] + + for (const [key, value] of Object.entries(env)) { + if (dangerousEnvVars.includes(key)) { + throw new Error(`Environment variable '${key}' modification is not allowed`) + } + + if (typeof value === 'string' && value.includes('\0')) { + throw new Error(`Environment variable '${key}' contains null byte`) + } + } +} + +export const validateMCPServerConfig = (serverParams: any): void => { + // Validate the entire server configuration + if (!serverParams || typeof serverParams !== 'object') { + throw new Error('Invalid server configuration') + } + + // Command allowlist - only allow specific safe commands + const allowedCommands = ['node', 'npx', 'python', 'python3', 'docker'] + + if (serverParams.command && !allowedCommands.includes(serverParams.command)) { + throw new Error(`Command '${serverParams.command}' is not allowed. Allowed commands: ${allowedCommands.join(', ')}`) + } + + // Validate arguments if present + if (serverParams.args && Array.isArray(serverParams.args)) { + validateArgsForLocalFileAccess(serverParams.args) + validateCommandInjection(serverParams.args) + } + + // Validate environment variables + if (serverParams.env) { + validateEnvironmentVariables(serverParams.env) + } +} diff --git a/packages/components/src/utils.ts b/packages/components/src/utils.ts index fae09790e..05e4e418a 100644 --- a/packages/components/src/utils.ts +++ b/packages/components/src/utils.ts @@ -1543,7 +1543,7 @@ export const executeJavaScriptCode = async ( ? 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 deps = process.env.ALLOW_BUILTIN_DEP === 'true' ? availableDependencies.concat(externalDeps) : externalDeps const defaultNodeVMOptions: any = { console: 'inherit', diff --git a/packages/server/.env.example b/packages/server/.env.example index fe47880b0..84fc6ce78 100644 --- a/packages/server/.env.example +++ b/packages/server/.env.example @@ -39,6 +39,7 @@ PORT=3000 # LOG_LEVEL=info #(error | warn | info | verbose | debug) # TOOL_FUNCTION_BUILTIN_DEP=crypto,fs # TOOL_FUNCTION_EXTERNAL_DEP=moment,lodash +# ALLOW_BUILTIN_DEP=false ############################################################################################################ @@ -170,6 +171,8 @@ JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200 ############################################################################################################ # HTTP_DENY_LIST= +# CUSTOM_MCP_SECURITY_CHECK=true +# CUSTOM_MCP_PROTOCOL=sse #(stdio | sse) ############################################################################################################ diff --git a/packages/server/src/commands/base.ts b/packages/server/src/commands/base.ts index bdffb8f62..d3dfbf430 100644 --- a/packages/server/src/commands/base.ts +++ b/packages/server/src/commands/base.ts @@ -22,6 +22,7 @@ export abstract class BaseCommand extends Command { LOG_LEVEL: Flags.string(), TOOL_FUNCTION_BUILTIN_DEP: Flags.string(), TOOL_FUNCTION_EXTERNAL_DEP: Flags.string(), + ALLOW_BUILTIN_DEP: Flags.string(), NUMBER_OF_PROXIES: Flags.string(), DATABASE_TYPE: Flags.string(), DATABASE_PATH: Flags.string(), @@ -73,7 +74,10 @@ export abstract class BaseCommand extends Command { REDIS_KEY: Flags.string(), REDIS_CA: Flags.string(), REDIS_KEEP_ALIVE: Flags.string(), - ENABLE_BULLMQ_DASHBOARD: Flags.string() + ENABLE_BULLMQ_DASHBOARD: Flags.string(), + CUSTOM_MCP_SECURITY_CHECK: Flags.string(), + CUSTOM_MCP_PROTOCOL: Flags.string(), + HTTP_DENY_LIST: Flags.string() } protected async stopProcess() { @@ -143,9 +147,10 @@ export abstract class BaseCommand extends Command { if (flags.LOG_PATH) process.env.LOG_PATH = flags.LOG_PATH if (flags.LOG_LEVEL) process.env.LOG_LEVEL = flags.LOG_LEVEL - // Tool functions + // Custom tool/function dependencies if (flags.TOOL_FUNCTION_BUILTIN_DEP) process.env.TOOL_FUNCTION_BUILTIN_DEP = flags.TOOL_FUNCTION_BUILTIN_DEP if (flags.TOOL_FUNCTION_EXTERNAL_DEP) process.env.TOOL_FUNCTION_EXTERNAL_DEP = flags.TOOL_FUNCTION_EXTERNAL_DEP + if (flags.ALLOW_BUILTIN_DEP) process.env.ALLOW_BUILTIN_DEP = flags.ALLOW_BUILTIN_DEP // Database config if (flags.DATABASE_TYPE) process.env.DATABASE_TYPE = flags.DATABASE_TYPE @@ -200,5 +205,10 @@ export abstract class BaseCommand extends Command { if (flags.REMOVE_ON_COUNT) process.env.REMOVE_ON_COUNT = flags.REMOVE_ON_COUNT if (flags.REDIS_KEEP_ALIVE) process.env.REDIS_KEEP_ALIVE = flags.REDIS_KEEP_ALIVE if (flags.ENABLE_BULLMQ_DASHBOARD) process.env.ENABLE_BULLMQ_DASHBOARD = flags.ENABLE_BULLMQ_DASHBOARD + + // Security + if (flags.CUSTOM_MCP_SECURITY_CHECK) process.env.CUSTOM_MCP_SECURITY_CHECK = flags.CUSTOM_MCP_SECURITY_CHECK + if (flags.CUSTOM_MCP_PROTOCOL) process.env.CUSTOM_MCP_PROTOCOL = flags.CUSTOM_MCP_PROTOCOL + if (flags.HTTP_DENY_LIST) process.env.HTTP_DENY_LIST = flags.HTTP_DENY_LIST } }