Feat/Enhance security validation for MCP configurations (#5232)

feat: enhance security validation for MCP configurations

- Added environment variable checks for CUSTOM_MCP_SECURITY_CHECK, CUSTOM_MCP_PROTOCOL, and HTTP_DENY_LIST across various Docker and application files.
- Implemented validation functions in MCP core to prevent command injection and ensure safe environment variable usage
This commit is contained in:
Henry Heng
2025-09-18 14:37:31 +01:00
committed by GitHub
parent 42152dd036
commit 41131dfac3
10 changed files with 130 additions and 9 deletions
@@ -1,6 +1,6 @@
import { Tool } from '@langchain/core/tools'
import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeOptionsValue, INodeParams } from '../../../../src/Interface'
import { MCPToolkit } from '../core'
import { MCPToolkit, validateMCPServerConfig } from '../core'
import { getVars, prepareSandboxVars } from '../../../../src/utils'
import { DataSource } from 'typeorm'
import hash from 'object-hash'
@@ -75,8 +75,8 @@ class Custom_MCP implements INode {
},
placeholder: mcpServerConfig,
warning:
process.env.CUSTOM_MCP_SECURITY_CHECK === 'true'
? 'In next release, only Remote MCP with url is supported. Read more <a href="https://docs.flowiseai.com/tutorials/tools-and-mcp#streamable-http-recommended" target="_blank">here</a>'
process.env.CUSTOM_MCP_PROTOCOL === 'sse'
? 'Only Remote MCP with url is supported. Read more <a href="https://docs.flowiseai.com/tutorials/tools-and-mcp#streamable-http-recommended" target="_blank">here</a>'
: undefined
},
{
@@ -174,6 +174,14 @@ class Custom_MCP implements INode {
serverParams = JSON.parse(serverParamsString)
}
if (process.env.CUSTOM_MCP_SECURITY_CHECK !== 'false') {
try {
validateMCPServerConfig(serverParams)
} catch (error) {
throw new Error(`Security validation failed: ${error.message}`)
}
}
// Compatible with stdio and SSE
let toolkit: MCPToolkit
if (process.env.CUSTOM_MCP_PROTOCOL === 'sse') {
@@ -1,7 +1,7 @@
import { Tool } from '@langchain/core/tools'
import { ICommonObject, INode, INodeData, INodeOptionsValue, INodeParams } from '../../../../src/Interface'
import { getNodeModulesPackagePath } from '../../../../src/utils'
import { MCPToolkit, validateArgsForLocalFileAccess } from '../core'
import { MCPToolkit, validateMCPServerConfig } from '../core'
class Supergateway_MCP implements INode {
label: string
@@ -106,9 +106,9 @@ class Supergateway_MCP implements INode {
args: [packagePath, ...processedArgs]
}
if (process.env.CUSTOM_MCP_SECURITY_CHECK === 'true') {
if (process.env.CUSTOM_MCP_SECURITY_CHECK !== 'false') {
try {
validateArgsForLocalFileAccess(processedArgs)
validateMCPServerConfig(serverParams)
} catch (error) {
throw new Error(`Security validation failed: ${error.message}`)
}
@@ -219,3 +219,67 @@ export const validateArgsForLocalFileAccess = (args: string[]): void => {
}
}
}
export const validateCommandInjection = (args: string[]): void => {
const dangerousPatterns = [
// Shell metacharacters
/[;&|`$(){}[\]<>]/,
// Command chaining
/&&|\|\||;;/,
// Redirections
/>>|<<|>/,
// Backticks and command substitution
/`|\$\(/,
// Process substitution
/<\(|>\(/
]
for (const arg of args) {
if (typeof arg !== 'string') continue
for (const pattern of dangerousPatterns) {
if (pattern.test(arg)) {
throw new Error(`Argument contains potentially dangerous characters: "${arg}"`)
}
}
}
}
export const validateEnvironmentVariables = (env: Record<string, any>): void => {
const dangerousEnvVars = ['PATH', 'LD_LIBRARY_PATH', 'DYLD_LIBRARY_PATH']
for (const [key, value] of Object.entries(env)) {
if (dangerousEnvVars.includes(key)) {
throw new Error(`Environment variable '${key}' modification is not allowed`)
}
if (typeof value === 'string' && value.includes('\0')) {
throw new Error(`Environment variable '${key}' contains null byte`)
}
}
}
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']
if (serverParams.command && !allowedCommands.includes(serverParams.command)) {
throw new Error(`Command '${serverParams.command}' is not allowed. Allowed commands: ${allowedCommands.join(', ')}`)
}
// Validate arguments if present
if (serverParams.args && Array.isArray(serverParams.args)) {
validateArgsForLocalFileAccess(serverParams.args)
validateCommandInjection(serverParams.args)
}
// Validate environment variables
if (serverParams.env) {
validateEnvironmentVariables(serverParams.env)
}
}