Ruflo CVE-2026-59726: an MCP bridge RCE that outlives the patch
Ruflo CVE-2026-59726 (CVSS 10.0) exposed 233 MCP tools with no authentication. The RCE is fixed in 3.16.3 — the poisoned agent memory it left behind is not.
Most critical vulnerabilities end when you install the patch. This one does not, and that is the reason it is worth your morning. On 29 July 2026 Noma Labs published RufRoot, a maximum-severity flaw in Ruflo — the multi-agent orchestration harness formerly known as Claude Flow, with 67,000+ GitHub stars and, per Noma's own figures, roughly 10 million downloads. Tracked as CVE-2026-59726 and scored CVSS 10.0 (AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H), it let anyone who could reach TCP 3001 run shell commands inside the bridge container with a single unauthenticated HTTP POST. The maintainer shipped a fix within 24 hours. That fix closes the door. It does not remove whoever walked through it — because the most durable thing an attacker could do here was write a record into the agent platform's persistent memory, and a patched redeploy leaves that record exactly where it was.
What CVE-2026-59726 actually exposed
Ruflo's MCP Bridge is an Express.js server that handles Model Context Protocol tool invocations. Every agent action, every memory write, every tool call routes through it. In Noma's words, it "isn't a random auxiliary debug interface; rather, it is Ruflo's central nervous system."
The shipped docker-compose.yml bound that bridge to 0.0.0.0:3001 and applied no authentication to POST /mcp or POST /mcp/:group. Behind those two endpoints sat 233 tools — shell execution, database operations, agent lifecycle management, persistent memory storage. No token, no API key, no header check, no IP allowlist. MongoDB was likewise bound to all interfaces without authentication on 27017.
The proof of concept is one request:
curl -s -X POST https://<target>:3001/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"ruflo__terminal_execute",
"arguments":{"command":"id && hostname"}}}'
That returns command output as the container's node user. The GitHub advisory GHSA-c4hm-4h84-2cf3 records the classification as CWE-78 (OS command injection) plus CWE-306 (missing authentication for a critical function) — the second is the one that made it a 10.0. Noma validated the chain against default deployments on AWS EC2, which is to say: against what you get when you follow the quickstart.
The eight-step chain, and why step five is the one that matters
Noma's write-up walks the full path. Mapped to MITRE ATT&CK, using the technique IDs in Zero Hunt's synced ATT&CK corpus:
| # | Step | Tool / mechanism | ATT&CK |
|---|---|---|---|
| 1 | Enumerate | tools/list returns all 233 tools, unauthenticated |
T1190 |
| 2 | Execute | ruflo__terminal_execute → shell as node |
T1059.013 |
| 3 | Steal keys | OpenAI / Anthropic / Google / OpenRouter keys from container env | T1552.007 |
| 4 | Weaponise agents | ruflo__swarm_init, ruflo__agent_spawn → attacker swarms on victim keys |
T1078.004 |
| 5 | Poison memory | ruflo__agentdb_pattern-store → malicious patterns in the learning store |
T1565.001 |
| 6 | Read conversations | unauthenticated MongoDB on the Docker network | T1213 |
| 7 | Persist | beacon written to the writable /app directory, auto-restarted |
T1505 |
| 8 | Clean up | shell history cleared | T1070.003 |
Steps 1 through 4 and 6 through 8 are a normal bad day: rebuild the container from a clean image, rotate the credentials, and you have undone them. Step 5 is different in kind. agentdb_pattern-store is the interface through which Ruflo's agents learn. An attacker writing there is not dropping a file — they are inserting an instruction into the platform's institutional knowledge. Noma's demonstration was a fabricated SOC 2 compliance policy that told agents to include an attacker-controlled URL in every deployment script they generated. Their assessment of the result: "Future conversations produce attacker-influenced output, and the user has no way to tell."
Why MCP security keeps failing in exactly this way
This is not a Ruflo-specific lapse, and treating it as one is how you get the next four. The MCP authorization specification defines an OAuth 2.1 framework and then marks authorization optional for implementations. A July 2025 internet scan cited in the Cloud Security Alliance's MCP security research note (4 May 2026) found 1,862 publicly accessible MCP servers responding to unauthenticated tool-listing requests. Optional authentication in a specification becomes absent authentication in a default docker-compose.yml, and default configurations are what the overwhelming majority of deployments run.
Speaking to CSO Online, Kanerika's Amit Jena made the generalisable point: "any platform that gives agents a persistent, writable memory store needs to treat that store as a security boundary" — and very few do today.
The pattern is also nine years old. CVE-2017-20198 is the Marathon UI in DC/OS before 1.9.0, which let unauthenticated users deploy arbitrary Docker containers because the orchestration control plane shipped without an auth layer. Same shape, different decade: a control plane built for a trusted local network, then containerised, then bound to 0.0.0.0 by a compose file nobody re-read. We covered the adjacent case in June, when LiteLLM's MCP test endpoints turned the AI gateway into an RCE surface. The agent harness is now the same category of asset the gateway became: high-privilege, credential-rich, and absent from the asset register.
Poisoned memory is data, not malware
Every eradication runbook your team owns assumes the artefact is a file. Hashes, YARA rules, EDR quarantine, image rebuild. A poisoned AgentDB pattern defeats all of it, because it is a legitimate row in a legitimate datastore, written through the legitimate API, in the same format as every honest entry your own engineers created. It has no hash worth blocking. Rebuilding the container preserves it, because the whole point of the store is that it survives container lifecycle. Noma is explicit: patched redeployment alone does not undo the poisoning.
"We patched to 3.16.3 the same day. We're clean."
Are you? You were on 3.16.2 with 3001 on
0.0.0.0for eleven weeks. The bridge acceptedtools/callfrom anyone who found it and logged nothing you kept. Your provider keys sat in that container's environment the entire time. If someone wrote one pattern into AgentDB in week three, your agents have been generating output shaped by it ever since — and the patch you installed on Wednesday changed none of that. Rotating the keys was necessary. Reading the memory store is the part nobody scheduled.
This is what makes the class worth a policy, not just a patch. Any agent platform with a writable learning store — memory, RAG index, feedback loop, fine-tuning queue — has an integrity surface that is not covered by any control your organisation currently runs. Write access to it is equivalent to write access to the model's behaviour, for every future user, with no crash, no alert, and no artefact.
Remediation
1. Am I affected?
Anything below 3.16.3 is vulnerable; whether you were exploitable depends on whether the bridge was reachable.
# Version, host-side and in-container
npm ls -g ruflo claude-flow @claude-flow/cli 2>/dev/null
docker exec <bridge-container> npx ruflo --version
# What is the bridge actually bound to?
ss -lntp | grep -E ':3001|:27017'
docker compose ps --format '{{.Service}}\t{{.Ports}}'
# "0.0.0.0:3001->3001/tcp" is the finding. "127.0.0.1:3001->3001/tcp" is not.
# The test that settles it — run from a DIFFERENT host on the network
curl -s -m 5 -X POST http://<target>:3001/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | head -c 400
If that last command returns a tool list without a token, the instance was open to anyone who could route to it. Treat it as compromised and go to step 4 — do not stop at step 2.
2. Patch — exact fixed versions
Upgrade ruflo, claude-flow and @claude-flow/cli to 3.16.3 or later. Per the release notes, 3.16.3 changes defaults in ways that will stop a careless upgrade, deliberately:
- MCP bridge binds
127.0.0.1by default; a public bind requiresMCP_AUTH_TOKENor the process exits FATAL at boot. - Bearer-token authentication with constant-time comparison.
terminal_executegated behindMCP_ENABLE_TERMINAL=true, off by default.- MongoDB authentication required —
MONGO_INITDB_ROOT_PASSWORDmust be set or the container will not start (openssl rand -base64 32). - Read-only container rootfs with tmpfs; CORS allowlist via
MCP_CORS_ORIGINreplacing the wildcard.
If your upgrade "just worked" with no config changes, verify you actually restarted from the new compose file rather than an overriding local one.
3. Can't patch now? Compensating controls
- Firewall 3001 and 27017 to loopback or an explicit allowlist. This alone removes the unauthenticated network path.
- Rewrite the port mapping to
127.0.0.1:3001:3001in your compose file and recreate the container. Publishing to0.0.0.0bypasses hostiptablesrules on Docker's default networking — the bind address is the control that holds. - If remote access is genuinely required, put an authenticating reverse proxy in front and never expose the bridge directly.
- Run the container non-root with a read-only rootfs, and drop
/appwrite permission so step 7 of the chain fails. - Scope and rate-limit every provider API key the container holds, with spend caps. A stolen key with a $50 daily ceiling is a bounded incident.
4. Hunt for compromise
Assume nothing was logged and hunt on state, not on alerts.
# Bridge calls from anything that isn't loopback
docker logs <bridge-container> 2>&1 | grep -E 'tools/call|terminal_execute' | grep -v '127.0.0.1'
# Files added or changed since the image was built
docker diff <bridge-container>
docker exec <bridge-container> ls -la /app --time-style=full-iso
# Truncated history is itself a signal (T1070.003)
docker exec <bridge-container> sh -c 'wc -l ~/.bash_history 2>/dev/null'
Signals worth a ticket, mapped to technique:
POST /mcpor/mcp/:groupfrom a non-loopback source address — T1190.- Any
tools/callnamingruflo__terminal_executeyou cannot attribute to an operator — T1059.013. - Provider dashboards: token-spend spikes, calls from ASNs or regions you do not operate in, models you never enabled — T1078.004.
- Unauthenticated connections on 27017; collections or documents outside your schema — T1213.
- AgentDB pattern-store entries you did not author. Export the store, sort by write time, and reconcile every entry against a deploy or a named engineer — T1565.001.
- Outbound sessions from the bridge container to destinations it has never contacted, particularly steady-interval connections — T1071.001.
5. Eradicate and verify
- Rebuild from a clean 3.16.3 image. Do not clean a running container — step 8 of the chain exists specifically to make that judgement unreliable.
- Revoke — not merely rotate — every provider key that was present in the container environment (OpenAI, Anthropic, Google, OpenRouter) plus MongoDB credentials. Issuing a new key while the old one stays valid changes nothing.
- Audit the AgentDB pattern store entry by entry and delete anything unattributable. This is the step that is skipped, and the only one that addresses the persistence that survives everything else.
- Inspect MongoDB for tampered conversation records before you trust anything you export from it.
- Re-run the external
curlfrom step 1. Post-fix it must return 401 or refuse the connection. ConfirmMCP_ENABLE_TERMINALis unset, the bridge is on127.0.0.1, and Mongo demands authentication.
What continuous validation looks like for agent infrastructure
Read step 4 again and notice what it is asking for. There is no hash to block and no alert that fired. The two things that actually betray a compromised agent platform are both behavioural: an endpoint that answers strangers, and a container whose network behaviour stops matching its own history.
The second is what Zero Hunt's AI Traffic Analysis is built to catch. A deep-learning model trained on billions of PCAP sequences, running with four parallel inference heads — suspicious traffic, malware classification, attack type identification, application fingerprinting — at 2.7+ Gbit/s on the appliance GPU, entirely on-premises. Against this scenario that matters concretely: an agent container is one of the most profiled-able hosts on your network, because its baseline is narrow and repetitive. When it starts opening sessions to a never-seen ASN, or a swarm nobody scheduled begins burning provider tokens on sustained outbound TLS, that is a departure from a learned baseline rather than a signature match — which is the only detection that works when the payload is a database row and the C2 is HTTPS to a legitimate API. It runs while the activity is happening, not in the next morning's SIEM digest.
The first — an endpoint that answers strangers — is a perimeter question, and it is the one that made this a 10.0 rather than an 8.8. Zero Hunt's change-triggered campaigns start a full run within the hour when a new asset appears on the perimeter, and the 10-agent swarm writes a per-target exploit with a local LLM rather than replaying a public PoC, so what gets validated is reachability and impact in your environment: does port 3001 answer tools/list from outside, and what does it hand back. Every finding is backtested through the AI Gym before it reaches production and ECDSA-signed at write time, so "the bridge was open from 12 May to 30 July" is an evidence item you can hand an auditor or an insurer, not a recollection.
The uncomfortable part of RufRoot is that no one has yet built the control that catches step 5. Watching how the agent behaves afterwards is currently the closest thing we have.
If you are running agent infrastructure and want the exposure question answered with evidence rather than inference, get in touch.