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:
parent
42152dd036
commit
41131dfac3
|
|
@ -163,4 +163,13 @@ JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200
|
||||||
# REDIS_KEY=
|
# REDIS_KEY=
|
||||||
# REDIS_CA=
|
# REDIS_CA=
|
||||||
# REDIS_KEEP_ALIVE=
|
# REDIS_KEEP_ALIVE=
|
||||||
# ENABLE_BULLMQ_DASHBOARD=
|
# ENABLE_BULLMQ_DASHBOARD=
|
||||||
|
|
||||||
|
|
||||||
|
############################################################################################################
|
||||||
|
############################################## SECURITY ####################################################
|
||||||
|
############################################################################################################
|
||||||
|
|
||||||
|
# HTTP_DENY_LIST=
|
||||||
|
# CUSTOM_MCP_SECURITY_CHECK=true
|
||||||
|
# CUSTOM_MCP_PROTOCOL=sse #(stdio | sse)
|
||||||
|
|
@ -139,6 +139,11 @@ services:
|
||||||
- REDIS_CA=${REDIS_CA}
|
- REDIS_CA=${REDIS_CA}
|
||||||
- REDIS_KEEP_ALIVE=${REDIS_KEEP_ALIVE}
|
- REDIS_KEEP_ALIVE=${REDIS_KEEP_ALIVE}
|
||||||
- ENABLE_BULLMQ_DASHBOARD=${ENABLE_BULLMQ_DASHBOARD}
|
- ENABLE_BULLMQ_DASHBOARD=${ENABLE_BULLMQ_DASHBOARD}
|
||||||
|
|
||||||
|
# SECURITY
|
||||||
|
- CUSTOM_MCP_SECURITY_CHECK=${CUSTOM_MCP_SECURITY_CHECK}
|
||||||
|
- CUSTOM_MCP_PROTOCOL=${CUSTOM_MCP_PROTOCOL}
|
||||||
|
- HTTP_DENY_LIST=${HTTP_DENY_LIST}
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ['CMD', 'curl', '-f', 'http://localhost:${PORT:-3000}/api/v1/ping']
|
test: ['CMD', 'curl', '-f', 'http://localhost:${PORT:-3000}/api/v1/ping']
|
||||||
interval: 10s
|
interval: 10s
|
||||||
|
|
@ -276,6 +281,11 @@ services:
|
||||||
- REDIS_CA=${REDIS_CA}
|
- REDIS_CA=${REDIS_CA}
|
||||||
- REDIS_KEEP_ALIVE=${REDIS_KEEP_ALIVE}
|
- REDIS_KEEP_ALIVE=${REDIS_KEEP_ALIVE}
|
||||||
- ENABLE_BULLMQ_DASHBOARD=${ENABLE_BULLMQ_DASHBOARD}
|
- ENABLE_BULLMQ_DASHBOARD=${ENABLE_BULLMQ_DASHBOARD}
|
||||||
|
|
||||||
|
# SECURITY
|
||||||
|
- CUSTOM_MCP_SECURITY_CHECK=${CUSTOM_MCP_SECURITY_CHECK}
|
||||||
|
- CUSTOM_MCP_PROTOCOL=${CUSTOM_MCP_PROTOCOL}
|
||||||
|
- HTTP_DENY_LIST=${HTTP_DENY_LIST}
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ['CMD', 'curl', '-f', 'http://localhost:${WORKER_PORT:-5566}/healthz']
|
test: ['CMD', 'curl', '-f', 'http://localhost:${WORKER_PORT:-5566}/healthz']
|
||||||
interval: 10s
|
interval: 10s
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,11 @@ services:
|
||||||
- REDIS_CA=${REDIS_CA}
|
- REDIS_CA=${REDIS_CA}
|
||||||
- REDIS_KEEP_ALIVE=${REDIS_KEEP_ALIVE}
|
- REDIS_KEEP_ALIVE=${REDIS_KEEP_ALIVE}
|
||||||
- ENABLE_BULLMQ_DASHBOARD=${ENABLE_BULLMQ_DASHBOARD}
|
- ENABLE_BULLMQ_DASHBOARD=${ENABLE_BULLMQ_DASHBOARD}
|
||||||
|
|
||||||
|
# SECURITY
|
||||||
|
- CUSTOM_MCP_SECURITY_CHECK=${CUSTOM_MCP_SECURITY_CHECK}
|
||||||
|
- CUSTOM_MCP_PROTOCOL=${CUSTOM_MCP_PROTOCOL}
|
||||||
|
- HTTP_DENY_LIST=${HTTP_DENY_LIST}
|
||||||
ports:
|
ports:
|
||||||
- '${PORT}:${PORT}'
|
- '${PORT}:${PORT}'
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
|
|
||||||
|
|
@ -163,4 +163,13 @@ JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200
|
||||||
# REDIS_KEY=
|
# REDIS_KEY=
|
||||||
# REDIS_CA=
|
# REDIS_CA=
|
||||||
# REDIS_KEEP_ALIVE=
|
# REDIS_KEEP_ALIVE=
|
||||||
# ENABLE_BULLMQ_DASHBOARD=
|
# ENABLE_BULLMQ_DASHBOARD=
|
||||||
|
|
||||||
|
|
||||||
|
############################################################################################################
|
||||||
|
############################################## SECURITY ####################################################
|
||||||
|
############################################################################################################
|
||||||
|
|
||||||
|
# HTTP_DENY_LIST=
|
||||||
|
# CUSTOM_MCP_SECURITY_CHECK=true
|
||||||
|
# CUSTOM_MCP_PROTOCOL=sse #(stdio | sse)
|
||||||
|
|
@ -124,6 +124,12 @@ services:
|
||||||
- REDIS_CA=${REDIS_CA}
|
- REDIS_CA=${REDIS_CA}
|
||||||
- REDIS_KEEP_ALIVE=${REDIS_KEEP_ALIVE}
|
- REDIS_KEEP_ALIVE=${REDIS_KEEP_ALIVE}
|
||||||
- ENABLE_BULLMQ_DASHBOARD=${ENABLE_BULLMQ_DASHBOARD}
|
- ENABLE_BULLMQ_DASHBOARD=${ENABLE_BULLMQ_DASHBOARD}
|
||||||
|
|
||||||
|
# SECURITY
|
||||||
|
- CUSTOM_MCP_SECURITY_CHECK=${CUSTOM_MCP_SECURITY_CHECK}
|
||||||
|
- CUSTOM_MCP_PROTOCOL=${CUSTOM_MCP_PROTOCOL}
|
||||||
|
- HTTP_DENY_LIST=${HTTP_DENY_LIST}
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
- '${WORKER_PORT}:${WORKER_PORT}'
|
- '${WORKER_PORT}:${WORKER_PORT}'
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Tool } from '@langchain/core/tools'
|
import { Tool } from '@langchain/core/tools'
|
||||||
import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeOptionsValue, INodeParams } from '../../../../src/Interface'
|
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 { getVars, prepareSandboxVars } from '../../../../src/utils'
|
||||||
import { DataSource } from 'typeorm'
|
import { DataSource } from 'typeorm'
|
||||||
import hash from 'object-hash'
|
import hash from 'object-hash'
|
||||||
|
|
@ -75,8 +75,8 @@ class Custom_MCP implements INode {
|
||||||
},
|
},
|
||||||
placeholder: mcpServerConfig,
|
placeholder: mcpServerConfig,
|
||||||
warning:
|
warning:
|
||||||
process.env.CUSTOM_MCP_SECURITY_CHECK === 'true'
|
process.env.CUSTOM_MCP_PROTOCOL === 'sse'
|
||||||
? '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>'
|
? '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
|
: undefined
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -174,6 +174,14 @@ class Custom_MCP implements INode {
|
||||||
serverParams = JSON.parse(serverParamsString)
|
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
|
// Compatible with stdio and SSE
|
||||||
let toolkit: MCPToolkit
|
let toolkit: MCPToolkit
|
||||||
if (process.env.CUSTOM_MCP_PROTOCOL === 'sse') {
|
if (process.env.CUSTOM_MCP_PROTOCOL === 'sse') {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Tool } from '@langchain/core/tools'
|
import { Tool } from '@langchain/core/tools'
|
||||||
import { ICommonObject, INode, INodeData, INodeOptionsValue, INodeParams } from '../../../../src/Interface'
|
import { ICommonObject, INode, INodeData, INodeOptionsValue, INodeParams } from '../../../../src/Interface'
|
||||||
import { getNodeModulesPackagePath } from '../../../../src/utils'
|
import { getNodeModulesPackagePath } from '../../../../src/utils'
|
||||||
import { MCPToolkit, validateArgsForLocalFileAccess } from '../core'
|
import { MCPToolkit, validateMCPServerConfig } from '../core'
|
||||||
|
|
||||||
class Supergateway_MCP implements INode {
|
class Supergateway_MCP implements INode {
|
||||||
label: string
|
label: string
|
||||||
|
|
@ -106,9 +106,9 @@ class Supergateway_MCP implements INode {
|
||||||
args: [packagePath, ...processedArgs]
|
args: [packagePath, ...processedArgs]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.CUSTOM_MCP_SECURITY_CHECK === 'true') {
|
if (process.env.CUSTOM_MCP_SECURITY_CHECK !== 'false') {
|
||||||
try {
|
try {
|
||||||
validateArgsForLocalFileAccess(processedArgs)
|
validateMCPServerConfig(serverParams)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Security validation failed: ${error.message}`)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -171,6 +171,8 @@ JWT_REFRESH_TOKEN_EXPIRY_IN_MINUTES=43200
|
||||||
############################################################################################################
|
############################################################################################################
|
||||||
|
|
||||||
# HTTP_DENY_LIST=
|
# HTTP_DENY_LIST=
|
||||||
|
# CUSTOM_MCP_SECURITY_CHECK=true
|
||||||
|
# CUSTOM_MCP_PROTOCOL=sse #(stdio | sse)
|
||||||
|
|
||||||
|
|
||||||
############################################################################################################
|
############################################################################################################
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,10 @@ export abstract class BaseCommand extends Command {
|
||||||
REDIS_KEY: Flags.string(),
|
REDIS_KEY: Flags.string(),
|
||||||
REDIS_CA: Flags.string(),
|
REDIS_CA: Flags.string(),
|
||||||
REDIS_KEEP_ALIVE: Flags.string(),
|
REDIS_KEEP_ALIVE: Flags.string(),
|
||||||
ENABLE_BULLMQ_DASHBOARD: Flags.string()
|
ENABLE_BULLMQ_DASHBOARD: Flags.string(),
|
||||||
|
CUSTOM_MCP_SECURITY_CHECK: Flags.string(),
|
||||||
|
CUSTOM_MCP_PROTOCOL: Flags.string(),
|
||||||
|
HTTP_DENY_LIST: Flags.string()
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async stopProcess() {
|
protected async stopProcess() {
|
||||||
|
|
@ -202,5 +205,10 @@ export abstract class BaseCommand extends Command {
|
||||||
if (flags.REMOVE_ON_COUNT) process.env.REMOVE_ON_COUNT = flags.REMOVE_ON_COUNT
|
if (flags.REMOVE_ON_COUNT) process.env.REMOVE_ON_COUNT = flags.REMOVE_ON_COUNT
|
||||||
if (flags.REDIS_KEEP_ALIVE) process.env.REDIS_KEEP_ALIVE = flags.REDIS_KEEP_ALIVE
|
if (flags.REDIS_KEEP_ALIVE) process.env.REDIS_KEEP_ALIVE = flags.REDIS_KEEP_ALIVE
|
||||||
if (flags.ENABLE_BULLMQ_DASHBOARD) process.env.ENABLE_BULLMQ_DASHBOARD = flags.ENABLE_BULLMQ_DASHBOARD
|
if (flags.ENABLE_BULLMQ_DASHBOARD) process.env.ENABLE_BULLMQ_DASHBOARD = flags.ENABLE_BULLMQ_DASHBOARD
|
||||||
|
|
||||||
|
// Security
|
||||||
|
if (flags.CUSTOM_MCP_SECURITY_CHECK) process.env.CUSTOM_MCP_SECURITY_CHECK = flags.CUSTOM_MCP_SECURITY_CHECK
|
||||||
|
if (flags.CUSTOM_MCP_PROTOCOL) process.env.CUSTOM_MCP_PROTOCOL = flags.CUSTOM_MCP_PROTOCOL
|
||||||
|
if (flags.HTTP_DENY_LIST) process.env.HTTP_DENY_LIST = flags.HTTP_DENY_LIST
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue