mirror of
https://github.com/FlowiseAI/Flowise.git
synced 2026-10-12 02:02:17 +00:00
Fix Flowise-552 Add Allow List Execution Path (#6658)
fix(flowise-552): add allow list execution path
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -363,53 +363,39 @@ describe('MCP Security Validations', () => {
|
||||
})
|
||||
|
||||
describe('validateArgsForLocalFileAccess', () => {
|
||||
it('should block absolute paths', () => {
|
||||
expect(() => {
|
||||
validateArgsForLocalFileAccess(['/etc/passwd'])
|
||||
}).toThrow('Argument contains potential local file access')
|
||||
const ORIGINAL_ENV = process.env.CUSTOM_MCP_ALLOWED_ABSOLUTE_SCRIPT_PATHS
|
||||
|
||||
expect(() => {
|
||||
validateArgsForLocalFileAccess(['C:\\Windows\\System32'])
|
||||
}).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 double-slash absolute paths', () => {
|
||||
it('should block when not configured', () => {
|
||||
delete process.env.CUSTOM_MCP_ALLOWED_ABSOLUTE_SCRIPT_PATHS
|
||||
|
||||
expect(() => {
|
||||
validateArgsForLocalFileAccess(['//etc/passwd'])
|
||||
}).toThrow('Argument contains potential local file access')
|
||||
validateArgsForLocalFileAccess(['/usr/local/lib/server.js'])
|
||||
}).toThrow('Configure CUSTOM_MCP_ALLOWED_ABSOLUTE_SCRIPT_PATHS')
|
||||
})
|
||||
|
||||
it('should block path traversal', () => {
|
||||
expect(() => {
|
||||
validateArgsForLocalFileAccess(['../../../etc/passwd'])
|
||||
}).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(['..\\..\\Windows'])
|
||||
}).toThrow('Argument contains potential local file access')
|
||||
})
|
||||
|
||||
it('should block dangerous file extensions', () => {
|
||||
expect(() => {
|
||||
validateArgsForLocalFileAccess(['malware.exe'])
|
||||
}).toThrow('Argument contains potential local file access')
|
||||
|
||||
expect(() => {
|
||||
validateArgsForLocalFileAccess(['script.sh'])
|
||||
}).toThrow('Argument contains potential local file access')
|
||||
})
|
||||
|
||||
it('should block null bytes', () => {
|
||||
expect(() => {
|
||||
validateArgsForLocalFileAccess(['file\0.txt'])
|
||||
}).toThrow('Argument contains null byte')
|
||||
})
|
||||
|
||||
it('should allow safe arguments', () => {
|
||||
expect(() => {
|
||||
validateArgsForLocalFileAccess(['@modelcontextprotocol/server-github', 'safe-arg'])
|
||||
validateArgsForLocalFileAccess(['/usr/local/lib/server.js'])
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('should block script not in allow-list', () => {
|
||||
process.env.CUSTOM_MCP_ALLOWED_ABSOLUTE_SCRIPT_PATHS = '/usr/local/lib/server.js'
|
||||
|
||||
expect(() => {
|
||||
validateArgsForLocalFileAccess(['root/.flowise/malicious.js'])
|
||||
}).toThrow('not in allowed list')
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateEnvironmentVariables', () => {
|
||||
@@ -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(() => {
|
||||
@@ -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', () => {
|
||||
@@ -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({
|
||||
|
||||
@@ -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:\
|
||||
const allowedScriptPaths = (process.env.CUSTOM_MCP_ALLOWED_ABSOLUTE_SCRIPT_PATHS ?? '')
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
// Relative paths that could escape current directory
|
||||
/\.\.\//, // Parent directory traversal with ../
|
||||
/\.\.\\/, // Parent directory traversal with ..\
|
||||
/^\.\./, // Starting with ..
|
||||
const scriptArg = args[0]
|
||||
|
||||
// Local file access patterns
|
||||
/^\.\//, // Current directory with ./
|
||||
/^~\//, // Home directory with ~/
|
||||
/^file:\/\//, // File protocol
|
||||
if (allowedScriptPaths.length === 0)
|
||||
throw new Error('Custom MCP script execution disabled. Configure CUSTOM_MCP_ALLOWED_ABSOLUTE_SCRIPT_PATHS environment variable.')
|
||||
|
||||
// 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
|
||||
]
|
||||
|
||||
for (const arg of args) {
|
||||
if (typeof arg !== 'string') continue
|
||||
|
||||
// Check for dangerous patterns
|
||||
for (const pattern of dangerousPatterns) {
|
||||
if (pattern.test(arg)) {
|
||||
throw new Error(`Argument contains potential local file access: "${arg}"`)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user