Independent AI intelligence Two editions daily · ET
FervorAI

Analysis · July 31, 2026 · repo

RufloCVE-2026-59726Noma SecurityAgentDBmcp-securityagent-memoryagent-securitymulti-agentagent-infrastructure

Ruflo's CVSS 10 Bug Got Patched in a Day. The Poisoned Agent Memory Did Not

An unauthenticated HTTP request gave anyone a shell inside the leading open-source agent meta-harness. The shell is the easy part to fix. Step five of the attack chain outlives the fix, and the project's own advisory says so in bold.

Read Ruflo's security advisory closely and you find a line that does not belong in a normal CVE writeup. Buried in the operator instructions, after "firewall these ports" and "rotate these keys," is this:

Audit the AgentDB pattern store for injected agentdb_pattern-store entries; purge poisoned patterns. A patched redeploy alone does NOT undo poisoning.

That is the maintainer telling you that upgrading does not make you clean. It is the most interesting sentence published about agent security this month, and almost nobody quoted it.

Ruflo is the MIT-licensed agent meta-harness formerly called Claude Flow, the thing a lot of people reach for when they want agent swarms on top of Claude Code, Codex, or Hermes. Its GitHub page served me between 62.7k and 66.6k stars across three loads inside one session this morning, and Noma Security's writeup puts it above 67,000, so treat any single number you see as a snapshot of GitHub's cache rather than a fact. Whatever the count, this is not a weekend project. It is 7,298 commits and a very large number of people running docker compose up -d.

And until version 3.16.3, that compose file published a shell to the network.

The bug, and why one line of YAML was the whole thing

CVE-2026-59726, codenamed RufRoot by Noma Labs, is not clever. That is what makes it worth reading.

Ruflo's MCP Bridge is an Express server that every tool invocation passes through. It exposes 233 tools covering shell access, database operations, agent management, and memory writes. The shipped docker-compose.yml bound that bridge, and MongoDB alongside it, to 0.0.0.0. The POST /mcp endpoint accepted MCP JSON-RPC and handed it to executeTool() with no authentication layer at all. No token, no header check, no allowlist.

There was a command blocklist. It ran only inside the autopilot flow. POST /mcp and POST /mcp/:group walked straight past it. The maintainer's own release note is unusually direct about this, calling the missing server-side gate on terminal_execute "the missing link that made the disclosed RCE chain reach shell."

So one unauthenticated POST to port 3001, calling ruflo__terminal_execute, ran arbitrary commands as the container's node user. That is it. GitHub's advisory scores it 10.0, with scope changed and no privileges or user interaction required.

Noma Labs then built an eight-step proof of concept and ran it end to end against a default deployment on EC2. Enumerate all 233 tools with tools/list. Get code execution and confirm it with an out-of-band callback. Dump every provider key with printenv, because the compose file passes OpenAI, Anthropic, Google, and OpenRouter credentials in as environment variables and backend processes inherit the whole environment. Spawn attacker-controlled swarms on the victim's keys and compute. Poison the AgentDB learning store. Dump every stored conversation out of the unauthenticated MongoDB on the internal Docker network. Drop a beacon at /app/beacon.js, inject a require() for it into index.js, kill PID 1, and let Docker's restart: unless-stopped bring the backdoor up for you. Clear shell history.

Now count which of those steps the patch kills.

Seven die. One doesn't.

Upgrade to 3.16.3 and the bridge binds loopback by default. Public bind now fails closed unless you set MCP_AUTH_TOKEN, and the process exits at boot with a FATAL message if you try. Bearer auth uses a constant-time compare. terminal_execute is gated behind MCP_ENABLE_TERMINAL=true, off by default, enforced identically on /mcp, /mcp/:group, and autopilot. MongoDB runs with --auth and refuses to start without a root password. The bridge rootfs is read-only with tmpfs. CORS gets an allowlist instead of a wildcard. There are static and runtime regression locks in CI so the defaults cannot drift back.

That closes recon, execution, key theft, swarm hijacking, conversation theft, the backdoor, and the cleanup. Rotate your keys and the stolen credentials are dead too.

Step five is the one that walks through the patch untouched. Poisoning the AgentDB pattern store is a legitimate write to a legitimate datastore, performed through a legitimate tool, by an attacker who at the time held legitimate access. Noma's example is a fake SOC 2 compliance policy stored as a learned pattern, instructing the platform to include an attacker-controlled URL in every deployment script it generates from then on. The upgrade does not touch that row. The key rotation does not touch it. It sits in the learning store and steers output for every user of that instance, indefinitely, and the users have no signal that anything is off, because a helpful agent citing an internal compliance policy is precisely what a working agent looks like.

That is the actual lesson of RufRoot, and it generalizes past Ruflo. Any agent stack with persistent learned memory has a datastore where a write today changes behavior in six months. Most teams treat that store as a cache. It is closer to a config file with no code review.

Put this into practice

Start with the ten-minute version if you run Ruflo:

Check exposure from another machine. curl -s -X POST http://<your-host>:3001/mcp -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'. If you get a tool list back from off-box, you were exploitable and should assume you were exploited.

Upgrade and confirm. npm view ruflo@latest version should return 3.16.3 or higher. All three dist-tags were repointed. The compose file now needs a Mongo password before it will boot: echo "MONGO_INITDB_ROOT_PASSWORD=$(openssl rand -base64 32)" >> .env.

Firewall 3001 and 27017, then rotate every provider key that ever sat in that container's environment.

Then do the slow part. Read the AgentDB pattern store by hand. Sort by write time, look for anything stored during a window you cannot account for, and look specifically for patterns that reference external URLs, install commands, or policies nobody on your team wrote. There is no tool for this and no published baseline for what a clean store looks like, which is exactly the gap worth complaining loudly about.

The transferable practice, if you never touch Ruflo: put provenance on every memory write. Who or what wrote this entry, from which interface, at what time, under whose authentication. If your agent memory cannot answer those four questions per row, then a compromise anywhere in your stack becomes a permanent behavioral change you cannot detect or reverse. Add the fields now, while the store is small and you can still believe every row in it.

And treat any MCP server the way you would treat an admin API, because that is what it is. Authenticate every request. Ship the restrictive default and make the permissive one opt-in with an explicit token. Ruflo's fix is a decent template for exactly that.

Where this story is less tidy than the headlines

The response here was genuinely good, and I want to give the maintainer credit before I complain. Coordinated disclosure landed June 30, the fix merged and shipped as v3.16.3 on July 1 at 14:34 UTC, and the advisory published the same day with full operator instructions. That is roughly a day from report to patched release on a CVSS 10, which is faster than most funded vendors manage.

The severity numbers do not agree with each other, though, and the disagreement is inside one project. GitHub's advisory page shows 10.0 overall. The v3.16.3 release note, written by the same maintainer, says "CVSS 9.8 critical." NVD and Noma both carry 10.0. The GitHub advisory also still reads "No known CVE" while NVD has assigned CVE-2026-59726. None of that changes what you should do, and all of it is a reminder that severity metadata is human-entered and frequently stale.

The gap that bothers me more is calendrical. The patch shipped July 1. Noma's public writeup landed July 29, and the press picked it up on the 29th and 30th. That is four weeks in which the fix existed and an operator running a public instance had no particular reason to know they urgently needed it, since a security release on a project that ships constantly reads like any other release. Coordinated disclosure windows exist for good reasons. They also mean the people most exposed learn last.

A few other things to hold loosely. Noma Security sells MCP server security and the writeup ends in a demo request, so read the framing accordingly. I trust the technical chain because the maintainer's own advisory independently describes the same eight steps, not because a vendor said so. Ruflo's reach numbers, roughly 10 million downloads and a million active users, come from the project and its coverage rather than any independent count. And "bound to 0.0.0.0" is not the same as "reachable from the internet." Actual exposure depended on each operator's firewall rules, security groups, and network segmentation, and nobody has published a scan of how many instances were genuinely reachable. The honest answer is that we do not know how big this was.

The part worth remembering

The shell got fixed in a day. The poisoned pattern store is still, in principle, sitting inside any instance that was reachable in June, doing its job perfectly.

Go look at whatever your agents write to and read back later. Not the conversation log, the thing they learn from. Ask when each entry got written, and by what. If you cannot answer, that store is a boundary you have not defended, and it is the one that keeps its contents after everything else is rotated.

Sources: ruvnet/ruflo GHSA-c4hm-4h84-2cf3 advisory; ruflo v3.16.3 release notes; Noma Security, "RufRoot" (Jul 29, 2026); The Hacker News; NVD CVE-2026-59726.