Fix Flowise 709 Make Custom MCP stdio command allowlist operator-controlled (#6578)

fix(flowise-709): make Custom MCP stdio command allowlist operator-controlled
This commit is contained in:
yau-wd
2026-06-30 13:57:33 +08:00
committed by GitHub
parent de3a91827f
commit f5d16835bc
8 changed files with 84 additions and 3 deletions
+1
View File
@@ -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)
+2
View File
@@ -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}
+1
View File
@@ -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}
+1
View File
@@ -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)
+1
View File
@@ -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}
@@ -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()
})
})
})
})
+15 -3
View File
@@ -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
+1
View File
@@ -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)