mirror of
https://github.com/FlowiseAI/Flowise.git
synced 2026-10-12 02:02:17 +00:00
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:
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user