All posts
Unproven Execution

Flowise MCP Adapter: Filters Aren't Authorization

CVE-2026-40933 turns a Flowise chatflow import into 1-click RCE; the Feb 2026 flag-denylist patch is bypassable, so stdio MCP filters remain a treadmill

Securityv0 Intelligence Team OWASP: ASI05 sv0 finding: unproven_execution
flowise mcp stdio unproven-execution asi05 cve-2026-40933

The Incident

On April 15, 2026, FlowiseAI published GHSA-c9gw-hvqq-f33r, assigned CVE-2026-40933 (CVSS v3.1 9.9, CWE-78), disclosing a one-click remote code execution flaw in the Custom MCP tool adapter shipped by flowise and flowise-components ≤ 3.0.13 on npm, fixed in 3.1.0. The bug lives in packages/components/nodes/tools/MCP/core.ts: when an MCP server entry uses the stdio transport, Flowise spawns the configured command as a child process of the Flowise server. An authenticated user who imports a chatflow containing a malicious MCP entry has that command spawned at canvas-load time, because the editor auto-enumerates available tools as the workflow loads. No save, run, or approval is needed before code executes. The advisory credits researchers MosesOX and 13ph03nix.

On June 1, 2026, Obsidian Security published a working PoC titled “When Is stdio MCP Actually a Vulnerability?” demonstrating that the official patch is bypassable. The Feb 24, 2026 fix (PR #5741, with follow-up PR #5943) added validateCommandFlags() in core.ts — an allowlist of binaries (node | npx | python | python3 | docker) plus a denylist of flags. The Obsidian payload {"command":"npx","args":["-c","<attacker shell>"]} survives the filter because -c is exactly the kind of code-execution flag an allowlisted binary still accepts. Self-hosted deployments are vulnerable by default; managed Flowise Cloud is not affected. Exploit code is public. MITRE ATT&CK coverage: T1059 Command and Scripting Interpreter, T1190 Exploit Public-Facing Application.

The Authority Path That Failed

The identity carrying execution authority is the Flowise server process itself — a non-human identity owning the Node.js runtime. Its held scope is whatever the operator granted it: filesystem, the secret store containing every credential Flowise has for connected datasources, LLM APIs and cloud accounts, and outbound network. Its exercised scope at the moment of failure is child_process.spawn("npx", ["-c", "<attacker shell>"]) — an arbitrary OS command triggered by chatflow JSON deserialized from an attacker-controlled file.

Two trust anchors failed in sequence. First, the stdio transport treats “an authenticated workflow author may configure a local command” as equivalent to “the operator authorized that command to run as the server” — there is no per-command operator approval gate, and chatflow import implicitly consents on the operator’s behalf. Second, the input-validation defense in validateCommandFlags() tries to constrain authority by filtering arguments, but the allowlisted binaries themselves accept code-execution flags (npx -c, node --eval, python -c). The February flag denylist chased symptoms; the June bypass proves argument filtering cannot close a path whose root cause is that the operator never authorized arbitrary command execution in the first place. The gap between held authority (full OS) and intended authority (run a known MCP server binary) was discoverable pre-incident — any inventory of stdio-transport MCP configs without an operator-signed allowlist of (command, args) tuples would have flagged it.

SecurityV0 Perspective

This is unproven_execution, OWASP ASI05 — the third instance SecurityV0 has documented in the LLM-workflow ecosystem in eight weeks. Langflow’s CSV Agent + Python REPL attached an interpreter to LLM output; Anthropic’s MCP stdio defaults attached an unsandboxed child process to any project that names one; Flowise’s Custom MCP adapter attaches the same child_process.spawn surface to any imported chatflow. The shape repeats because the patch loop targets the wrong layer: a denylist of dangerous flags is a downstream control on a path the operator never authorized in the first place.

The evidence pack SecurityV0 would produce against a Flowise deployment ties together (1) every chatflow that contains a stdio-transport MCP entry, with the exact (command, args) tuple each entry would spawn; (2) whether the operator has explicitly approved that tuple against a signed allowlist; (3) the host NHIs reachable from the Flowise process — every cloud SDK credential, registry token, vector-store key and secrets-manager scope the spawn would inherit; and (4) the installed version of flowise and flowise-components against the ≤ 3.0.13 affected range. Pre-incident, the pack answers a specific question: which chatflow imports would spawn a command the operator never authorized? Post-incident, it answers the forensic question: which Flowise host processes spawned which MCP commands, sourced from which chatflow file, during the exposure window?

What To Do

  • Upgrade Flowise to 3.1.0 and set CUSTOM_MCP_PROTOCOL=sse. Per Endor Labs and CSO Online, switching the Custom MCP transport from stdio to Server-Sent Events removes the child_process.spawn path entirely. Staying on stdio with only the v3.1.0 flag denylist remains exploitable via npx -c "<cmd>" per the June 2026 Obsidian PoC; SSE is the only complete mitigation as of disclosure.
  • Inventory every chatflow with a stdio-transport MCP entry, with the exact (command, args) tuple. Treat the union as your attack surface. The control you need is an operator-signed allowlist of approved tuples, enforced at chatflow-import time — not at spawn time. Argument sanitization in validateCommandFlags() is downstream of the real authority boundary; SV0 published this same pattern against Anthropic’s MCP stdio defaults in April.
  • Restrict who can import chatflows, and review the JSON. Block the import endpoint at your reverse proxy for non-vetted user roles; require manual review of any chatflow JSON that references the Custom MCP node. The PoC {"command":"npx","args":["-c","touch /tmp/pwn"]} is one JSON field away from any imported file, and import alone is the trigger.
  • Drop the Flowise container’s privileges and segment its credential store. Default Docker images run as root in many deployments; bind a non-root UID and split the secret store so a compromised Flowise process cannot read every connected LLM, cloud and database credential at once. Every credential the Flowise process holds is one the PoC would dump on exploitation.
  • Log Flowise process-spawn telemetry and alert on npx -c, node --eval, python -c in the Flowise PID tree. Even with stdio disabled, these argument shapes appearing under your AI platform’s process tree are evidence that an MCP entry resurrected the path. The forensic question after an incident is which Flowise host spawned which MCP binary during the exposure window — make sure your logs answer it.

Sources