From 0e1cb13157c937a0d453ab186ef14f24f33e3c6a Mon Sep 17 00:00:00 2001 From: yau-wd Date: Wed, 3 Jun 2026 18:00:19 +0800 Subject: [PATCH] fix(flowise-552): default Custom MCP to SSE and switch env vars from deny-list to allow-list --- docker/.env.example | 3 +- docker/docker-compose-queue-prebuilt.yml | 2 + docker/docker-compose.yml | 1 + docker/worker/.env.example | 3 +- docker/worker/docker-compose.yml | 1 + .../nodes/tools/MCP/CustomMCP/CustomMCP.ts | 15 ++--- .../components/nodes/tools/MCP/core.test.ts | 61 ++++++++++++++++--- packages/components/nodes/tools/MCP/core.ts | 21 +++++-- packages/server/.env.example | 3 +- packages/server/src/commands/base.ts | 1 + 10 files changed, 84 insertions(+), 27 deletions(-) diff --git a/docker/.env.example b/docker/.env.example index f40572a5ea8..05df3fa9148 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -196,7 +196,8 @@ JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200 # HTTP_SECURITY_CHECK=true # PATH_TRAVERSAL_SAFETY=true # CUSTOM_MCP_SECURITY_CHECK=true -# CUSTOM_MCP_PROTOCOL=sse #(stdio | sse) +# CUSTOM_MCP_PROTOCOL=sse #(stdio | sse) 'stdio' can run arbitrary commands on your server, enable only if you trust all users +# CUSTOM_MCP_ALLOWED_ENV_VARS= #(comma-separated list of env var names a Custom MCP stdio config may set, e.g. BRAVE_API_KEY,GITHUB_TOKEN. Empty = none allowed) # TRUST_PROXY=true #(true | false | 1 | loopback| linklocal | uniquelocal | IP addresses | loopback, IP addresses) # OAUTH2_SECURITY_CHECK=true # OAUTH2_ALLOWED_TOKEN_DOMAINS= #(comma-separated list of additional OAuth2 provider domains to allow, e.g. keycloak.mycompany.com,auth.custom-idp.com) diff --git a/docker/docker-compose-queue-prebuilt.yml b/docker/docker-compose-queue-prebuilt.yml index b31d5c0986e..7026fade8c4 100644 --- a/docker/docker-compose-queue-prebuilt.yml +++ b/docker/docker-compose-queue-prebuilt.yml @@ -154,6 +154,7 @@ services: # SECURITY - CUSTOM_MCP_SECURITY_CHECK=${CUSTOM_MCP_SECURITY_CHECK} - CUSTOM_MCP_PROTOCOL=${CUSTOM_MCP_PROTOCOL} + - CUSTOM_MCP_ALLOWED_ENV_VARS=${CUSTOM_MCP_ALLOWED_ENV_VARS} - HTTP_DENY_LIST=${HTTP_DENY_LIST} - HTTP_SECURITY_CHECK=${HTTP_SECURITY_CHECK} - PATH_TRAVERSAL_SAFETY=${PATH_TRAVERSAL_SAFETY} @@ -309,6 +310,7 @@ services: # SECURITY - CUSTOM_MCP_SECURITY_CHECK=${CUSTOM_MCP_SECURITY_CHECK} - CUSTOM_MCP_PROTOCOL=${CUSTOM_MCP_PROTOCOL} + - CUSTOM_MCP_ALLOWED_ENV_VARS=${CUSTOM_MCP_ALLOWED_ENV_VARS} - HTTP_DENY_LIST=${HTTP_DENY_LIST} - HTTP_SECURITY_CHECK=${HTTP_SECURITY_CHECK} - PATH_TRAVERSAL_SAFETY=${PATH_TRAVERSAL_SAFETY} diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 03793fabc11..79cb31281f8 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -139,6 +139,7 @@ services: # SECURITY - CUSTOM_MCP_SECURITY_CHECK=${CUSTOM_MCP_SECURITY_CHECK} - CUSTOM_MCP_PROTOCOL=${CUSTOM_MCP_PROTOCOL} + - CUSTOM_MCP_ALLOWED_ENV_VARS=${CUSTOM_MCP_ALLOWED_ENV_VARS} - HTTP_DENY_LIST=${HTTP_DENY_LIST} - HTTP_SECURITY_CHECK=${HTTP_SECURITY_CHECK} - PATH_TRAVERSAL_SAFETY=${PATH_TRAVERSAL_SAFETY} diff --git a/docker/worker/.env.example b/docker/worker/.env.example index 3f92375bee6..685926bee05 100644 --- a/docker/worker/.env.example +++ b/docker/worker/.env.example @@ -195,7 +195,8 @@ JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200 # HTTP_SECURITY_CHECK=true # PATH_TRAVERSAL_SAFETY=true # CUSTOM_MCP_SECURITY_CHECK=true -# CUSTOM_MCP_PROTOCOL=sse #(stdio | sse) +# CUSTOM_MCP_PROTOCOL=sse #(stdio | sse) 'stdio' can run arbitrary commands on your server, enable only if you trust all users +# CUSTOM_MCP_ALLOWED_ENV_VARS= #(comma-separated list of env var names a Custom MCP stdio config may set, e.g. BRAVE_API_KEY,GITHUB_TOKEN. Empty = none allowed) # TRUST_PROXY=true #(true | false | 1 | loopback| linklocal | uniquelocal | IP addresses | loopback, IP addresses) # OAUTH2_SECURITY_CHECK=true # OAUTH2_ALLOWED_TOKEN_DOMAINS= #(comma-separated list of additional OAuth2 provider domains to allow, e.g. keycloak.mycompany.com,auth.custom-idp.com) diff --git a/docker/worker/docker-compose.yml b/docker/worker/docker-compose.yml index 94818234e8b..af836a68866 100644 --- a/docker/worker/docker-compose.yml +++ b/docker/worker/docker-compose.yml @@ -138,6 +138,7 @@ services: # SECURITY - CUSTOM_MCP_SECURITY_CHECK=${CUSTOM_MCP_SECURITY_CHECK} - CUSTOM_MCP_PROTOCOL=${CUSTOM_MCP_PROTOCOL} + - CUSTOM_MCP_ALLOWED_ENV_VARS=${CUSTOM_MCP_ALLOWED_ENV_VARS} - HTTP_DENY_LIST=${HTTP_DENY_LIST} - HTTP_SECURITY_CHECK=${HTTP_SECURITY_CHECK} - PATH_TRAVERSAL_SAFETY=${PATH_TRAVERSAL_SAFETY} diff --git a/packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts b/packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts index eb65ecbdf7e..2a063158e49 100644 --- a/packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts +++ b/packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts @@ -1,9 +1,9 @@ import { Tool } from '@langchain/core/tools' +import hash from 'object-hash' +import { DataSource } from 'typeorm' import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeOptionsValue, INodeParams } from '../../../../src/Interface' +import { getVars, parseJsonBody, prepareSandboxVars } from '../../../../src/utils' import { MCPToolkit, validateMCPServerConfig } from '../core' -import { getVars, prepareSandboxVars, parseJsonBody } from '../../../../src/utils' -import { DataSource } from 'typeorm' -import hash from 'object-hash' const mcpServerConfig = `{ "command": "npx", @@ -183,13 +183,8 @@ class Custom_MCP implements INode { // Compatible with stdio and SSE let toolkit: MCPToolkit - if (process.env.CUSTOM_MCP_PROTOCOL === 'sse') { - toolkit = new MCPToolkit(serverParams, 'sse') - } else if (serverParams?.command === undefined) { - toolkit = new MCPToolkit(serverParams, 'sse') - } else { - toolkit = new MCPToolkit(serverParams, 'stdio') - } + if (process.env.CUSTOM_MCP_PROTOCOL === 'stdio' && serverParams!.command) toolkit = new MCPToolkit(serverParams, 'stdio') + else toolkit = new MCPToolkit(serverParams, 'sse') await toolkit.initialize() diff --git a/packages/components/nodes/tools/MCP/core.test.ts b/packages/components/nodes/tools/MCP/core.test.ts index 2810ee9a75d..3fcf44d31af 100644 --- a/packages/components/nodes/tools/MCP/core.test.ts +++ b/packages/components/nodes/tools/MCP/core.test.ts @@ -413,23 +413,47 @@ describe('MCP Security Validations', () => { }) describe('validateEnvironmentVariables', () => { - it('should block dangerous environment variables', () => { + const originalAllowList = process.env.CUSTOM_MCP_ALLOWED_ENV_VARS + + afterEach(() => { + if (originalAllowList === undefined) { + delete process.env.CUSTOM_MCP_ALLOWED_ENV_VARS + } else { + process.env.CUSTOM_MCP_ALLOWED_ENV_VARS = originalAllowList + } + }) + + it('should block all environment variables when the allow-list is empty', () => { + delete process.env.CUSTOM_MCP_ALLOWED_ENV_VARS + + expect(() => { + validateEnvironmentVariables({ API_KEY: 'key123' }) + }).toThrow("Environment variable 'API_KEY' is not allowed") + }) + + it('should block variables that are not on the allow-list', () => { + process.env.CUSTOM_MCP_ALLOWED_ENV_VARS = 'API_KEY' + expect(() => { validateEnvironmentVariables({ PATH: '/malicious/path' }) - }).toThrow("Environment variable 'PATH' modification is not allowed") + }).toThrow("Environment variable 'PATH' is not allowed") expect(() => { - validateEnvironmentVariables({ NODE_OPTIONS: '--inspect' }) - }).toThrow("Environment variable 'NODE_OPTIONS' modification is not allowed") + validateEnvironmentVariables({ PYTHONWARNINGS: 'module::antigravity.' }) + }).toThrow("Environment variable 'PYTHONWARNINGS' is not allowed") }) - it('should block null bytes in values', () => { + it('should block null bytes in values of allow-listed variables', () => { + process.env.CUSTOM_MCP_ALLOWED_ENV_VARS = 'CUSTOM_VAR' + expect(() => { validateEnvironmentVariables({ CUSTOM_VAR: 'value\0malicious' }) }).toThrow("Environment variable 'CUSTOM_VAR' contains null byte") }) - it('should allow safe environment variables', () => { + it('should allow variables that are on the allow-list', () => { + process.env.CUSTOM_MCP_ALLOWED_ENV_VARS = 'CUSTOM_VAR,API_KEY' + expect(() => { validateEnvironmentVariables({ CUSTOM_VAR: 'safe-value', API_KEY: 'key123' }) }).not.toThrow() @@ -482,14 +506,35 @@ describe('MCP Security Validations', () => { }).toThrow('Argument contains potential local file access') }) - it('should block dangerous environment variables', () => { + it('should block environment variables that are not on the allow-list', () => { expect(() => { validateMCPServerConfig({ command: 'npx', args: ['safe-arg'], env: { PATH: '/malicious' } }) - }).toThrow("Environment variable 'PATH' modification is not allowed") + }).toThrow("Environment variable 'PATH' is not allowed") + }) + + it('should allow environment variables that are on the allow-list', () => { + const original = process.env.CUSTOM_MCP_ALLOWED_ENV_VARS + process.env.CUSTOM_MCP_ALLOWED_ENV_VARS = 'API_TOKEN' + + try { + expect(() => { + validateMCPServerConfig({ + command: 'npx', + args: ['safe-arg'], + env: { API_TOKEN: 'secret123' } + }) + }).not.toThrow() + } finally { + if (original === undefined) { + delete process.env.CUSTOM_MCP_ALLOWED_ENV_VARS + } else { + process.env.CUSTOM_MCP_ALLOWED_ENV_VARS = original + } + } }) it('should reject invalid server params', () => { diff --git a/packages/components/nodes/tools/MCP/core.ts b/packages/components/nodes/tools/MCP/core.ts index 5cd552a3f8a..82cca4dfcb7 100644 --- a/packages/components/nodes/tools/MCP/core.ts +++ b/packages/components/nodes/tools/MCP/core.ts @@ -1,9 +1,9 @@ -import { CallToolRequest, CallToolResultSchema, ListToolsResult, ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js' +import { BaseToolkit, tool, Tool } from '@langchain/core/tools' import { Client } from '@modelcontextprotocol/sdk/client/index.js' +import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js' import { StdioClientTransport, StdioServerParameters } from '@modelcontextprotocol/sdk/client/stdio.js' -import { BaseToolkit, tool, Tool } from '@langchain/core/tools' import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js' -import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js' +import { CallToolRequest, CallToolResultSchema, ListToolsResult, ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js' import { checkDenyList, secureFetch } from '../../../src/httpSecurity' export class MCPToolkit extends BaseToolkit { @@ -249,12 +249,21 @@ export const validateCommandInjection = (args: string[]): void => { } } +/** + * Validates user-supplied env vars against the operator-controlled allow-list in + * `CUSTOM_MCP_ALLOWED_ENV_VARS` (comma-separated names). Empty = none allowed. + */ export const validateEnvironmentVariables = (env: Record): void => { - const dangerousEnvVars = ['PATH', 'LD_LIBRARY_PATH', 'DYLD_LIBRARY_PATH', 'NODE_OPTIONS'] + const allowedEnvVars = new Set( + (process.env.CUSTOM_MCP_ALLOWED_ENV_VARS ?? '') + .split(',') + .map((s) => s.trim()) + .filter(Boolean) + ) for (const [key, value] of Object.entries(env)) { - if (dangerousEnvVars.includes(key)) { - throw new Error(`Environment variable '${key}' modification is not allowed`) + if (!allowedEnvVars.has(key)) { + throw new Error(`Environment variable '${key}' is not allowed. Permitted: ${[...allowedEnvVars].join(', ') || '(none)'}`) } if (typeof value === 'string' && value.includes('\0')) { diff --git a/packages/server/.env.example b/packages/server/.env.example index ad755d530c8..7e99390a393 100644 --- a/packages/server/.env.example +++ b/packages/server/.env.example @@ -195,7 +195,8 @@ JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200 # HTTP_SECURITY_CHECK=true # PATH_TRAVERSAL_SAFETY=true # CUSTOM_MCP_SECURITY_CHECK=true -# CUSTOM_MCP_PROTOCOL=sse #(stdio | sse) +# CUSTOM_MCP_PROTOCOL=sse #(stdio | sse) 'stdio' can run arbitrary commands on your server, enable only if you trust all users +# CUSTOM_MCP_ALLOWED_ENV_VARS= #(comma-separated list of env var names a Custom MCP stdio config may set, e.g. BRAVE_API_KEY,GITHUB_TOKEN. Empty = none allowed) # TRUST_PROXY=true #(true | false | 1 | loopback| linklocal | uniquelocal | IP addresses | loopback, IP addresses) # OAUTH2_SECURITY_CHECK=true # OAUTH2_ALLOWED_TOKEN_DOMAINS= #(comma-separated list of additional OAuth2 provider domains to allow, e.g. keycloak.mycompany.com,auth.custom-idp.com) diff --git a/packages/server/src/commands/base.ts b/packages/server/src/commands/base.ts index 9c1c25d159b..5ff23b04cc2 100644 --- a/packages/server/src/commands/base.ts +++ b/packages/server/src/commands/base.ts @@ -107,6 +107,7 @@ export abstract class BaseCommand extends Command { // Security CUSTOM_MCP_SECURITY_CHECK: Flags.string(), CUSTOM_MCP_PROTOCOL: Flags.string(), + CUSTOM_MCP_ALLOWED_ENV_VARS: Flags.string(), HTTP_DENY_LIST: Flags.string(), HTTP_SECURITY_CHECK: Flags.string(), PATH_TRAVERSAL_SAFETY: Flags.string(),