From f5d16835bc76e86a223bedfdd44aa6610370fd58 Mon Sep 17 00:00:00 2001 From: yau-wd Date: Tue, 30 Jun 2026 13:57:33 +0800 Subject: [PATCH] Fix Flowise 709 Make Custom MCP stdio command allowlist operator-controlled (#6578) fix(flowise-709): make Custom MCP stdio command allowlist operator-controlled --- docker/.env.example | 1 + docker/docker-compose-queue-prebuilt.yml | 2 + docker/docker-compose.yml | 1 + docker/worker/.env.example | 1 + docker/worker/docker-compose.yml | 1 + .../components/nodes/tools/MCP/core.test.ts | 62 +++++++++++++++++++ packages/components/nodes/tools/MCP/core.ts | 18 +++++- packages/server/.env.example | 1 + 8 files changed, 84 insertions(+), 3 deletions(-) diff --git a/docker/.env.example b/docker/.env.example index 49205192668..e134e35c0d5 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -198,6 +198,7 @@ JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200 # CUSTOM_MCP_SECURITY_CHECK=true # 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) +# CUSTOM_MCP_ALLOWED_COMMANDS= #(comma-separated list of commands a Custom MCP stdio config may run. Empty = none allowed. Set only the commands you need from: node|npx|python|python3|docker) # 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 7026fade8c4..ac4df92aeb5 100644 --- a/docker/docker-compose-queue-prebuilt.yml +++ b/docker/docker-compose-queue-prebuilt.yml @@ -155,6 +155,7 @@ services: - CUSTOM_MCP_SECURITY_CHECK=${CUSTOM_MCP_SECURITY_CHECK} - CUSTOM_MCP_PROTOCOL=${CUSTOM_MCP_PROTOCOL} - CUSTOM_MCP_ALLOWED_ENV_VARS=${CUSTOM_MCP_ALLOWED_ENV_VARS} + - CUSTOM_MCP_ALLOWED_COMMANDS=${CUSTOM_MCP_ALLOWED_COMMANDS} - HTTP_DENY_LIST=${HTTP_DENY_LIST} - HTTP_SECURITY_CHECK=${HTTP_SECURITY_CHECK} - PATH_TRAVERSAL_SAFETY=${PATH_TRAVERSAL_SAFETY} @@ -311,6 +312,7 @@ services: - CUSTOM_MCP_SECURITY_CHECK=${CUSTOM_MCP_SECURITY_CHECK} - CUSTOM_MCP_PROTOCOL=${CUSTOM_MCP_PROTOCOL} - CUSTOM_MCP_ALLOWED_ENV_VARS=${CUSTOM_MCP_ALLOWED_ENV_VARS} + - CUSTOM_MCP_ALLOWED_COMMANDS=${CUSTOM_MCP_ALLOWED_COMMANDS} - 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 79cb31281f8..cea031df421 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -140,6 +140,7 @@ services: - CUSTOM_MCP_SECURITY_CHECK=${CUSTOM_MCP_SECURITY_CHECK} - CUSTOM_MCP_PROTOCOL=${CUSTOM_MCP_PROTOCOL} - CUSTOM_MCP_ALLOWED_ENV_VARS=${CUSTOM_MCP_ALLOWED_ENV_VARS} + - CUSTOM_MCP_ALLOWED_COMMANDS=${CUSTOM_MCP_ALLOWED_COMMANDS} - 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 577bb9e9b20..f8096b138a7 100644 --- a/docker/worker/.env.example +++ b/docker/worker/.env.example @@ -197,6 +197,7 @@ JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200 # CUSTOM_MCP_SECURITY_CHECK=true # 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) +# CUSTOM_MCP_ALLOWED_COMMANDS= #(comma-separated list of commands a Custom MCP stdio config may run. Empty = none allowed. Set only the commands you need from: node|npx|python|python3|docker) # 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 af836a68866..9f9de83c889 100644 --- a/docker/worker/docker-compose.yml +++ b/docker/worker/docker-compose.yml @@ -139,6 +139,7 @@ services: - CUSTOM_MCP_SECURITY_CHECK=${CUSTOM_MCP_SECURITY_CHECK} - CUSTOM_MCP_PROTOCOL=${CUSTOM_MCP_PROTOCOL} - CUSTOM_MCP_ALLOWED_ENV_VARS=${CUSTOM_MCP_ALLOWED_ENV_VARS} + - CUSTOM_MCP_ALLOWED_COMMANDS=${CUSTOM_MCP_ALLOWED_COMMANDS} - 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/core.test.ts b/packages/components/nodes/tools/MCP/core.test.ts index 3fcf44d31af..c1b5daf0b5a 100644 --- a/packages/components/nodes/tools/MCP/core.test.ts +++ b/packages/components/nodes/tools/MCP/core.test.ts @@ -461,6 +461,22 @@ describe('MCP Security Validations', () => { }) describe('validateMCPServerConfig', () => { + const originalAllowedCommands = process.env.CUSTOM_MCP_ALLOWED_COMMANDS + + beforeEach(() => { + // These tests assume the operator has permitted the common interpreters. + // The default (empty) allow-list is exercised separately below. + process.env.CUSTOM_MCP_ALLOWED_COMMANDS = 'node,npx,python,python3,docker' + }) + + afterEach(() => { + if (originalAllowedCommands === undefined) { + delete process.env.CUSTOM_MCP_ALLOWED_COMMANDS + } else { + process.env.CUSTOM_MCP_ALLOWED_COMMANDS = originalAllowedCommands + } + }) + it('should validate complete server configuration', () => { expect(() => { validateMCPServerConfig({ @@ -555,5 +571,51 @@ describe('MCP Security Validations', () => { }) }).not.toThrow() }) + + describe('command allow-list (CUSTOM_MCP_ALLOWED_COMMANDS)', () => { + it('should block every command when the allow-list is empty', () => { + delete process.env.CUSTOM_MCP_ALLOWED_COMMANDS + + expect(() => { + validateMCPServerConfig({ + command: 'npx', + args: ['@modelcontextprotocol/server-filesystem'] + }) + }).toThrow("Command 'npx' is not allowed. Permitted: (none)") + }) + + it('should block commands that are not on the allow-list', () => { + process.env.CUSTOM_MCP_ALLOWED_COMMANDS = 'python3' + + expect(() => { + validateMCPServerConfig({ + command: 'npx', + args: ['safe-arg'] + }) + }).toThrow("Command 'npx' is not allowed. Permitted: python3") + }) + + it('should allow commands that are on the allow-list', () => { + process.env.CUSTOM_MCP_ALLOWED_COMMANDS = 'npx,docker' + + expect(() => { + validateMCPServerConfig({ + command: 'npx', + args: ['@modelcontextprotocol/server-filesystem'] + }) + }).not.toThrow() + }) + + it('should ignore surrounding whitespace in the allow-list entries', () => { + process.env.CUSTOM_MCP_ALLOWED_COMMANDS = ' node , npx ' + + expect(() => { + validateMCPServerConfig({ + command: 'npx', + args: ['server.js'] + }) + }).not.toThrow() + }) + }) }) }) diff --git a/packages/components/nodes/tools/MCP/core.ts b/packages/components/nodes/tools/MCP/core.ts index 82cca4dfcb7..8ba771f243a 100644 --- a/packages/components/nodes/tools/MCP/core.ts +++ b/packages/components/nodes/tools/MCP/core.ts @@ -371,17 +371,29 @@ export const validateCommandFlags = (command: string, args: string[]): void => { } } +/** + * Validates a user-supplied MCP server configuration against operator-controlled allow-lists. + * + * For stdio configs, the command must appear in the `CUSTOM_MCP_ALLOWED_COMMANDS` allow-list + * (comma-separated, empty = none allowed). The list is empty by default, so no command can run + * until an operator explicitly opts in. To enable local/custom stdio MCP servers, set + * `CUSTOM_MCP_PROTOCOL=stdio` and `CUSTOM_MCP_ALLOWED_COMMANDS` in your env file + * (see docker/.env.example, docker/worker/.env.example, packages/server/.env.example). + */ 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'] + // Command allowlist - operator-controlled via CUSTOM_MCP_ALLOWED_COMMANDS (empty = none allowed) + const allowedCommands = (process.env.CUSTOM_MCP_ALLOWED_COMMANDS ?? '') + .split(',') + .map((s) => s.trim()) + .filter(Boolean) if (serverParams.command && !allowedCommands.includes(serverParams.command)) { - throw new Error(`Command '${serverParams.command}' is not allowed. Allowed commands: ${allowedCommands.join(', ')}`) + throw new Error(`Command '${serverParams.command}' is not allowed. Permitted: ${allowedCommands.join(', ') || '(none)'}`) } // Validate arguments if present diff --git a/packages/server/.env.example b/packages/server/.env.example index e30563510cc..28a89414dd4 100644 --- a/packages/server/.env.example +++ b/packages/server/.env.example @@ -197,6 +197,7 @@ JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200 # CUSTOM_MCP_SECURITY_CHECK=true # 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) +# CUSTOM_MCP_ALLOWED_COMMANDS= #(comma-separated list of commands a Custom MCP stdio config may run. Empty = none allowed. Set only the commands you need from: node|npx|python|python3|docker) # 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)