Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docker/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions docker/docker-compose-queue-prebuilt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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}
Expand Down
1 change: 1 addition & 0 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
1 change: 1 addition & 0 deletions docker/worker/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions docker/worker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
62 changes: 62 additions & 0 deletions packages/components/nodes/tools/MCP/core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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()
})
})
})
})
18 changes: 15 additions & 3 deletions packages/components/nodes/tools/MCP/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/server/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading