CVE-2026-59726: Verified Reproduction
CVE-2026-59726: Unauthenticated RCE in ruflo MCP bridge default docker-compose deployment
CVE-2026-59726 is verified against ruflo · npm. Affected versions: < 3.16.3. Fixed in 3.16.3. Vulnerability class: RCE. This critical reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00315.
What Is CVE-2026-59726?
CVE-2026-59726 is a critical-severity RCE vulnerability affecting ruflo < 3.16.3. Pruva has independently reproduced it and publishes a verified, runnable proof-of-concept (reproduction REPRO-2026-00315).
CVE-2026-59726 Severity
CVE-2026-59726 is rated critical severity.
Critical — the most severe class — typically remotely exploitable with severe impact. Treat as an emergency.
Affected ruflo Versions
ruflo · npm versions < 3.16.3 are affected.
How to Reproduce CVE-2026-59726
pruva-verify REPRO-2026-00315 curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00315/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for CVE-2026-59726
- reached the target end-to-end
- full exploit chain demonstrated
- on the real production code path
- high confidence
- the upstream fix blocks the same trigger
Unauthenticated JSON-RPC POST /mcp body: method=tools/call, params.name=ruflo__terminal_execute, params.arguments.command=<arbitrary shell command>
- POST /mcp
- createMcpHandler/catch-all tools/call
- executeTool(name)
- backend.callTool(ruflo, terminal_execute)
- execSync(command) as node (uid 1000)
reproduction_steps.sh How the agent worked
Root Cause and Exploit Chain for CVE-2026-59726
The ruflo MCP bridge (ruflo/src/ruvocal/mcp-bridge/index.js, the service built by
ruflo/docker-compose.yml) exposed POST /mcp and POST /mcp/:group with no
authentication and bound to 0.0.0.0 by default. The only blocklist that referenced
terminal_execute (AUTOPILOT_BLOCKED_PATTERNS + isBlockedTool()) was enforced solely in
the autopilot SSE handler. The shared executeTool() function — invoked by POST /mcp and
POST /mcp/:group for every tools/call — performed no gate, so an unauthenticated
network attacker could call tools/call → ruflo__terminal_execute. The bridge routed that
call to the ruflo MCP backend (@claude-flow/cli), whose terminal_execute handler runs
execSync(command) on attacker-supplied input, yielding arbitrary command execution as the
node user (uid 1000) inside the bridge container.
- Package/component affected:
rufloMCP bridge —ruflo/src/ruvocal/mcp-bridge/index.js(the bridge built byruflo/docker-compose.yml, servicemcp-bridge, port 3001). The dangerous tool implementation lives in theruflobackend (@claude-flow/cli,src/mcp-tools/terminal-tools.ts,execSync(command)). - Affected versions: ruflo
< 3.16.3(vulnerable at main commit4e18ad84, the parent of the fix). The defaultdocker-compose.ymlenabled thedevtoolstool group (MCP_GROUP_DEVTOOLS=true), which exposesterminal_*tools, and ran the bridge with noMCP_AUTH_TOKENand no bind-host restriction. - Risk level: Critical. Unauthenticated remote code execution. From the shell as
node, an attacker can read every provider API key from the container environment (OPENAI_API_KEY,GOOGLE_API_KEY,OPENROUTER_API_KEY,ANTHROPIC_API_KEY), spawn attacker-controlled swarms on the victim's keys, and persist poisoned patterns into the AgentDB learning store.
Impact Parity
- Disclosed/claimed maximum impact: Unauthenticated remote code execution (shell as
node/ uid 1000) viaPOST /mcp→tools/call→terminal_execute, with provider API key disclosure and AgentDB poisoning. - Reproduced impact from this run: Unauthenticated RCE confirmed end-to-end through the
real running bridge container. A single
POST /mcptools/call/ruflo__terminal_executerequest with no authentication header returned command outputuid=1000(node) gid=1000(node) groups=1000(node)/whoami=node, wrote an attacker marker file to/tmpinside the container and read it back, and executedprintenvfor the provider keys (exitCode: 0). No API keys were present in the reproduction environment, so the env-leak primitive was exercised but produced empty values; the command-execution primitive is fully demonstrated. - Parity:
fullfor the core claimed impact (unauthenticated RCE asnodeviaPOST /mcp→terminal_execute). The downstream consequences (key theft, swarm abuse, AgentDB poisoning) are direct implications of the demonstrated shell and were not separately exercised.
Root Cause
createMcpHandler() (per-group) and the catch-all POST /mcp handler both call
executeTool(name, toolArgs) for tools/call. In the vulnerable code executeTool() only
validated search-query shape and then routed unknown tool names to the matching external MCP
backend via backend.callTool() — there was no server-side deny list. The
AUTOPILOT_BLOCKED_PATTERNS array (containing /terminal_execute/) and isBlockedTool()
were referenced only inside the autopilot SSE loop, never in executeTool():
// vulnerable (4e18ad84) — executeTool() has NO gate:
async function executeTool(name, args) {
if (!args || typeof args !== "object") args = {};
// ... only search-query validation ...
switch (name) { /* search, web_research, guidance */ default: {
const activeTools = getActiveTools();
const extTool = activeTools.find(t => t.name === name);
if (extTool) {
const backend = mcpBackends.get(extTool._backend);
if (backend) return backend.callTool(extTool._originalName, args); // -> execSync
}
}}
}
The ruflo backend's terminal_execute runs the command verbatim:
// @claude-flow/cli src/mcp-tools/terminal-tools.ts
output = execSync(command, { cwd, encoding: "utf-8", timeout, ... });
Compounding factors in the default deployment: app.listen(PORT) binds all interfaces; no
auth middleware; CORS Access-Control-Allow-Origin: *; the devtools group (prefix
terminal_) is enabled by default; MongoDB bound to 0.0.0.0:27017 without --auth.
Fix commit: d00a0a40cd8bdbca877ac7f675f416bdc69accd1 (PR #2521, ADR-166 Phase 1–3). It
adds a server-side DANGEROUS_TOOLS gate at the top of executeTool() (denies
terminal_execute unless MCP_ENABLE_TERMINAL=true), a requireAuth bearer middleware
(timingSafeEqual), BIND_HOST=127.0.0.1 by default with fail-closed on public bind without
MCP_AUTH_TOKEN, a CORS allowlist, and MongoDB --auth defaults.
Reproduction Steps
- See
bundle/repro/reproduction_steps.sh(self-contained, executable). - The script resolves the ruflo repo (prepared project cache or fresh clone), checks out the
vulnerable commit
4e18ad84(=d00a0a40^) and the fixed commitd00a0a40into separate worktrees, sanity-checks that the vulnerableindex.jslacksDANGEROUS_TOOLSand the fixed one has it, builds a realnode:20-slimcontainer for each commit (the ruflo MCP backend is the real publishedruflonpm package;terminal_executeis verified present before baking), starts each container, and sends the actual unauthenticatedPOST /mcptools/call→ruflo__terminal_executerequest through the running HTTP service. It runs two clean vulnerable attempts and two clean fixed (negative-control) attempts, then writesbundle/repro/runtime_manifest.json. - Expected evidence (all under
bundle/):artifacts/http/vuln_attempt1_response.json— MCP result whosetextcontainsoutput: "uid=1000(node) ... <marker> ... ENV_LEAK:",exitCode: 0→ RCE asnode.artifacts/http/fixed_noauth_response.txt—{"error":"unauthorized"}(HTTP 401).artifacts/http/fixed_attempt1_response.json—{"error":"Tool ... is disabled by default ...","code":"TOOL_DISABLED"}; the marker is absent (command not executed).logs/reproduction_steps.log,logs/vuln_container.log,logs/fixed_container.log.
Evidence
Key excerpts (from bundle/artifacts/http/vuln_attempt1_response.json, vulnerable bridge,
no Authorization header):
"command": "id; whoami; echo PRUVA_RCE_<marker> > /tmp/PRUVA_RCE_<marker>.txt; cat /tmp/PRUVA_RCE_<marker>.txt; echo ENV_LEAK:; printenv OPENAI_API_KEY GOOGLE_API_KEY OPENROUTER_API_KEY ANTHROPIC_API_KEY 2>/dev/null || true"
"output": "uid=1000(node) gid=1000(node) groups=1000(node)\nnode\nPRUVA_RCE_<marker>\nENV_LEAK:\n"
"exitCode": 0
Fixed bridge negative control (bundle/artifacts/http/fixed_attempt1_response.json, with
Authorization: Bearer ...):
{ "error": "Tool \"ruflo__terminal_execute\" is disabled by default. Set MCP_ENABLE_TERMINAL=true to allow.", "code": "TOOL_DISABLED" }
Fixed bridge, no auth (bundle/artifacts/http/fixed_noauth_response.txt, HTTP 401):
{"error":"unauthorized"}
Environment: ruflo repo ruvnet/ruflo; vulnerable commit 4e18ad84c6c61be7ef43f62e371f8303a0f7517d;
fixed commit d00a0a40cd8bdbca877ac7f675f416bdc69accd1; bridge built from
ruflo/src/ruvocal/mcp-bridge on node:20-slim; ruflo backend = published ruflo npm package
(via @claude-flow/cli), terminal_execute confirmed in tools/list (331 backend tools,
185 exposed after group filtering). Container runs as node (uid 1000).
Recommendations / Next Steps
- Apply ADR-166 (PR #2521) fully: keep the
executeTool()server-side gate as the single denial point for every path (not just autopilot); keep bearer auth + loopback bind by default; fail-closed on public bind withoutMCP_AUTH_TOKEN; keepMCP_ENABLE_TERMINALopt-in; enforce MongoDB--auth. - Operators of any pre-fix exposed instance: firewall
:3001and:27017immediately, rotate all provider API keys, and audit/purge the AgentDB pattern store for injectedagentdb_pattern-storeentries (a patched redeploy does not undo poisoning). - Add regression locks (the fix already ships
test-runtime-security.mjsandtest-security-lock.js) covering: unauthenticatedPOST /mcpterminal_execute→TOOL_DISABLED; authenticated call still gated unlessMCP_ENABLE_TERMINAL=true; public bind without token → non-zero exit.
Additional Notes
- Idempotency:
reproduction_steps.shwas executed three consecutive times; every run exited0withconfirmed=true. It reuses a cached base tar / ruflo prefix / worktrees on a large scratch disk and re-imports fresh images each run. - Build method note: The default ruflo Dockerfile runs
npm install -g ruflo, which resolves an >800 MB dependency tree that exceeds the 1 GB rootless-docker storage here, so a plaindocker buildruns out of space. The script instead assembles the container filesystem on the host's large workspace disk and imports it withdocker import(single flat layer). The bridgeindex.jsis the unmodified repo file at each commit, and the ruflo backend is the real published package (installed with--omit=optional, which still exposesterminal_executebecause that tool only requiresnode:child_processexecSync). The opt-in backends (agentic-flow,gemini-mcp-server,@openai/codex) and theintelligence(ruvector) backend are not part of the vulnerability path (terminal_executeis provided by thedevtools/ruflobackend, default-on) and were omitted to fit storage; this does not affect the reproduction. - Limitation: No live provider API keys were set in the reproduction environment, so the
env-leak output is empty; the
printenvcommand executed successfully (demonstrating env access), and key disclosure is a direct implication of the demonstrated shell.
CVE-2026-59726 Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.
Full session Replay every step — scrub the timeline or play it back.
Artifacts and Evidence for CVE-2026-59726
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-59726
Upgrade ruflo · npm to 3.16.3 or later.
FAQ: CVE-2026-59726
Is CVE-2026-59726 exploitable?
How severe is CVE-2026-59726?
What type of vulnerability is CVE-2026-59726?
Which versions of ruflo are affected by CVE-2026-59726?
Is there a fix for CVE-2026-59726?
How can I reproduce CVE-2026-59726?
Is the CVE-2026-59726 reproduction verified?
References for CVE-2026-59726
Authoritative sources for CVE-2026-59726 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.