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 @@ -199,6 +199,7 @@ JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200
# 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)
# CUSTOM_MCP_ALLOWED_ABSOLUTE_SCRIPT_PATHS= #WARNING: Only add scripts YOU control - misconfiguration allows arbitrary code execution. (comma-separated list of absolute file paths that Custom MCP stdio configs may execute. Empty = none allowed.Example: /usr/local/lib/server.js,/app/scripts/brave-search.js)
# 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 @@ -156,6 +156,7 @@ services:
- CUSTOM_MCP_PROTOCOL=${CUSTOM_MCP_PROTOCOL}
- CUSTOM_MCP_ALLOWED_ENV_VARS=${CUSTOM_MCP_ALLOWED_ENV_VARS}
- CUSTOM_MCP_ALLOWED_COMMANDS=${CUSTOM_MCP_ALLOWED_COMMANDS}
- CUSTOM_MCP_ALLOWED_ABSOLUTE_SCRIPT_PATHS=${CUSTOM_MCP_ALLOWED_ABSOLUTE_SCRIPT_PATHS}
- HTTP_DENY_LIST=${HTTP_DENY_LIST}
- HTTP_SECURITY_CHECK=${HTTP_SECURITY_CHECK}
- PATH_TRAVERSAL_SAFETY=${PATH_TRAVERSAL_SAFETY}
Expand Down Expand Up @@ -313,6 +314,7 @@ services:
- CUSTOM_MCP_PROTOCOL=${CUSTOM_MCP_PROTOCOL}
- CUSTOM_MCP_ALLOWED_ENV_VARS=${CUSTOM_MCP_ALLOWED_ENV_VARS}
- CUSTOM_MCP_ALLOWED_COMMANDS=${CUSTOM_MCP_ALLOWED_COMMANDS}
- CUSTOM_MCP_ALLOWED_ABSOLUTE_SCRIPT_PATHS=${CUSTOM_MCP_ALLOWED_ABSOLUTE_SCRIPT_PATHS}
- 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 @@ -141,6 +141,7 @@ services:
- CUSTOM_MCP_PROTOCOL=${CUSTOM_MCP_PROTOCOL}
- CUSTOM_MCP_ALLOWED_ENV_VARS=${CUSTOM_MCP_ALLOWED_ENV_VARS}
- CUSTOM_MCP_ALLOWED_COMMANDS=${CUSTOM_MCP_ALLOWED_COMMANDS}
- CUSTOM_MCP_ALLOWED_ABSOLUTE_SCRIPT_PATHS=${CUSTOM_MCP_ALLOWED_ABSOLUTE_SCRIPT_PATHS}
- 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 @@ -198,6 +198,7 @@ JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200
# 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)
# CUSTOM_MCP_ALLOWED_ABSOLUTE_SCRIPT_PATHS= #WARNING: Only add scripts YOU control - misconfiguration allows arbitrary code execution. (comma-separated list of absolute file paths that Custom MCP stdio configs may execute. Empty = none allowed.Example: /usr/local/lib/server.js,/app/scripts/brave-search.js)
# 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 @@ -140,6 +140,7 @@ services:
- CUSTOM_MCP_PROTOCOL=${CUSTOM_MCP_PROTOCOL}
- CUSTOM_MCP_ALLOWED_ENV_VARS=${CUSTOM_MCP_ALLOWED_ENV_VARS}
- CUSTOM_MCP_ALLOWED_COMMANDS=${CUSTOM_MCP_ALLOWED_COMMANDS}
- CUSTOM_MCP_ALLOWED_ABSOLUTE_SCRIPT_PATHS=${CUSTOM_MCP_ALLOWED_ABSOLUTE_SCRIPT_PATHS}
- HTTP_DENY_LIST=${HTTP_DENY_LIST}
- HTTP_SECURITY_CHECK=${HTTP_SECURITY_CHECK}
- PATH_TRAVERSAL_SAFETY=${PATH_TRAVERSAL_SAFETY}
Expand Down
81 changes: 34 additions & 47 deletions packages/components/nodes/tools/MCP/core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,52 +363,38 @@ describe('MCP Security Validations', () => {
})

describe('validateArgsForLocalFileAccess', () => {
it('should block absolute paths', () => {
expect(() => {
validateArgsForLocalFileAccess(['/etc/passwd'])
}).toThrow('Argument contains potential local file access')

expect(() => {
validateArgsForLocalFileAccess(['C:\\Windows\\System32'])
}).toThrow('Argument contains potential local file access')
})
const ORIGINAL_ENV = process.env.CUSTOM_MCP_ALLOWED_ABSOLUTE_SCRIPT_PATHS

it('should block double-slash absolute paths', () => {
expect(() => {
validateArgsForLocalFileAccess(['//etc/passwd'])
}).toThrow('Argument contains potential local file access')
afterEach(() => {
if (ORIGINAL_ENV === undefined) {
delete process.env.CUSTOM_MCP_ALLOWED_ABSOLUTE_SCRIPT_PATHS
} else {
process.env.CUSTOM_MCP_ALLOWED_ABSOLUTE_SCRIPT_PATHS = ORIGINAL_ENV
}
})

it('should block path traversal', () => {
expect(() => {
validateArgsForLocalFileAccess(['../../../etc/passwd'])
}).toThrow('Argument contains potential local file access')
it('should block when not configured', () => {
delete process.env.CUSTOM_MCP_ALLOWED_ABSOLUTE_SCRIPT_PATHS

expect(() => {
validateArgsForLocalFileAccess(['..\\..\\Windows'])
}).toThrow('Argument contains potential local file access')
validateArgsForLocalFileAccess(['/usr/local/lib/server.js'])
}).toThrow('Configure CUSTOM_MCP_ALLOWED_ABSOLUTE_SCRIPT_PATHS')
})

it('should block dangerous file extensions', () => {
expect(() => {
validateArgsForLocalFileAccess(['malware.exe'])
}).toThrow('Argument contains potential local file access')
it('should allow script in allow-list', () => {
process.env.CUSTOM_MCP_ALLOWED_ABSOLUTE_SCRIPT_PATHS = '/usr/local/lib/server.js'

expect(() => {
validateArgsForLocalFileAccess(['script.sh'])
}).toThrow('Argument contains potential local file access')
validateArgsForLocalFileAccess(['/usr/local/lib/server.js'])
}).not.toThrow()
})

it('should block null bytes', () => {
expect(() => {
validateArgsForLocalFileAccess(['file\0.txt'])
}).toThrow('Argument contains null byte')
})
it('should block script not in allow-list', () => {
process.env.CUSTOM_MCP_ALLOWED_ABSOLUTE_SCRIPT_PATHS = '/usr/local/lib/server.js'

it('should allow safe arguments', () => {
expect(() => {
validateArgsForLocalFileAccess(['@modelcontextprotocol/server-github', 'safe-arg'])
}).not.toThrow()
validateArgsForLocalFileAccess(['root/.flowise/malicious.js'])
}).toThrow('not in allowed list')
})
})

Expand Down Expand Up @@ -462,11 +448,15 @@ describe('MCP Security Validations', () => {

describe('validateMCPServerConfig', () => {
const originalAllowedCommands = process.env.CUSTOM_MCP_ALLOWED_COMMANDS
const originalAllowedScriptPaths = process.env.CUSTOM_MCP_ALLOWED_ABSOLUTE_SCRIPT_PATHS

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'
// Allow common script paths for tests
process.env.CUSTOM_MCP_ALLOWED_ABSOLUTE_SCRIPT_PATHS =
'@modelcontextprotocol/server-filesystem,workspace,safe-arg,mcp-server.js,server.js,/usr/local/lib/server.js'
})

afterEach(() => {
Expand All @@ -475,6 +465,12 @@ describe('MCP Security Validations', () => {
} else {
process.env.CUSTOM_MCP_ALLOWED_COMMANDS = originalAllowedCommands
}

if (originalAllowedScriptPaths === undefined) {
delete process.env.CUSTOM_MCP_ALLOWED_ABSOLUTE_SCRIPT_PATHS
} else {
process.env.CUSTOM_MCP_ALLOWED_ABSOLUTE_SCRIPT_PATHS = originalAllowedScriptPaths
}
})

it('should validate complete server configuration', () => {
Expand All @@ -498,30 +494,21 @@ describe('MCP Security Validations', () => {
it('should block dangerous command flags', () => {
expect(() => {
validateMCPServerConfig({
command: 'npx',
args: ['-c', 'malicious command']
command: 'node',
args: ['server.js', '-e', 'malicious code']
})
}).toThrow("Argument '-c' is not allowed for command 'npx'")
}).toThrow("Argument '-e' is not allowed for command 'node'")
})

it('should block command injection in args', () => {
expect(() => {
validateMCPServerConfig({
command: 'npx',
args: ['arg1; malicious']
command: 'node',
args: ['server.js', 'arg1; malicious']
})
}).toThrow('Argument contains potentially dangerous characters')
})

it('should block path traversal in args', () => {
expect(() => {
validateMCPServerConfig({
command: 'npx',
args: ['../../../etc/passwd']
})
}).toThrow('Argument contains potential local file access')
})

it('should block environment variables that are not on the allow-list', () => {
expect(() => {
validateMCPServerConfig({
Expand Down
48 changes: 8 additions & 40 deletions packages/components/nodes/tools/MCP/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,49 +179,17 @@ export async function MCPTool({
}

export const validateArgsForLocalFileAccess = (args: string[]): void => {
const dangerousPatterns = [
// Absolute paths
/^\//, // Unix absolute paths starting with /
/^[a-zA-Z]:\\/, // Windows absolute paths like C:\

// Relative paths that could escape current directory
/\.\.\//, // Parent directory traversal with ../
/\.\.\\/, // Parent directory traversal with ..\
/^\.\./, // Starting with ..

// Local file access patterns
/^\.\//, // Current directory with ./
/^~\//, // Home directory with ~/
/^file:\/\//, // File protocol

// Common file extensions that shouldn't be accessed
/\.(exe|bat|cmd|sh|ps1|vbs|scr|com|pif|dll|sys)$/i,

// File flags and options that could access local files
/^--?(?:file|input|output|config|load|save|import|export|read|write)=/i,
/^--?(?:file|input|output|config|load|save|import|export|read|write)$/i
]
const allowedScriptPaths = (process.env.CUSTOM_MCP_ALLOWED_ABSOLUTE_SCRIPT_PATHS ?? '')
.split(',')
.map((s) => s.trim())
.filter(Boolean)

for (const arg of args) {
if (typeof arg !== 'string') continue
const scriptArg = args[0]

// Check for dangerous patterns
for (const pattern of dangerousPatterns) {
if (pattern.test(arg)) {
throw new Error(`Argument contains potential local file access: "${arg}"`)
}
}
if (allowedScriptPaths.length === 0)
throw new Error('Custom MCP script execution disabled. Configure CUSTOM_MCP_ALLOWED_ABSOLUTE_SCRIPT_PATHS environment variable.')

// Check for null bytes
if (arg.includes('\0')) {
throw new Error(`Argument contains null byte: "${arg}"`)
}

// Check for very long paths that might be used for buffer overflow attacks
if (arg.length > 1000) {
throw new Error(`Argument is suspiciously long (${arg.length} characters): "${arg.substring(0, 100)}..."`)
}
}
if (!allowedScriptPaths.includes(scriptArg)) throw new Error('Custom MCP script path not in allowed list.')
}

export const validateCommandInjection = (args: string[]): void => {
Expand Down
1 change: 1 addition & 0 deletions packages/server/.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_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)
# CUSTOM_MCP_ALLOWED_ABSOLUTE_SCRIPT_PATHS= #WARNING: Only add scripts YOU control - misconfiguration allows arbitrary code execution. (comma-separated list of absolute file paths that Custom MCP stdio configs may execute. Empty = none allowed.Example: /usr/local/lib/server.js,/app/scripts/brave-search.js)
# 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 packages/server/src/commands/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ export abstract class BaseCommand extends Command {
CUSTOM_MCP_SECURITY_CHECK: Flags.string(),
CUSTOM_MCP_PROTOCOL: Flags.string(),
CUSTOM_MCP_ALLOWED_ENV_VARS: Flags.string(),
CUSTOM_MCP_ALLOWED_COMMANDS: Flags.string(),
CUSTOM_MCP_ALLOWED_ABSOLUTE_SCRIPT_PATHS: Flags.string(),
HTTP_DENY_LIST: Flags.string(),
HTTP_SECURITY_CHECK: Flags.string(),
PATH_TRAVERSAL_SAFETY: Flags.string(),
Expand Down
Loading