CVSS 10.0, Shipped by Default

CVSS 10.0, Shipped by Default

The scariest number in agent security this week is not a model capability. It is a docker-compose file. CVE-2026-59726 carries a CVSS 3.1 base score of 10.0 — unauthenticated remote code execution in Ruflo, an open-source “agent meta-harness” for Claude Code and Codex with 67,000 stars on GitHub. No credentials, no user interaction, network attack vector, and a scope change that drags confidentiality, integrity, and availability all to High.

The interesting part is not that an agent tool had an RCE. It is where the RCE lived: in the default deployment, not in a weird configuration. The exploit chain ended in something even harder to clean up than a shell — the harness’s learning store, where a poisoned entry steers every future agent output for every user of the instance, and survives a patched redeploy.

The harness and the bridge

Ruflo describes itself as an agent meta-harness: deploy multi-player agent swarms, coordinate autonomous workflows, with “adaptive memory, self-learning intelligence,” RAG integration, and native Claude Code and Codex integration, per the repository. The component that failed is the MCP bridge — an HTTP server exposing Model Context Protocol tools to the rest of the stack. The bridge’s job is to make tools callable over HTTP; it is the network face of the harness.

The GitHub security advisory (GHSA-c4hm-4h84-2cf3), published July 1, 2026, and credited to Eli Ainhorn of Noma Security, is precise about what was wrong:

The MCP bridge shipping in ruflo/docker-compose.yml exposed POST /mcp with no authentication. The docker-compose defaults bound the bridge and MongoDB to all interfaces.

Two separate trust failures compounded into a 10.0:

  1. The tool gate only existed in one code path. Ruflo had a blocklist covering terminal_execute — but it was “enforced only in the autopilot flow.” The primary POST /mcp and POST /mcp/:group paths bypassed it entirely.
  2. The compose defaults assumed loopback. docker-compose.yml published the bridge (:3001) and MongoDB (:27017) to all interfaces. An “unauthorized” network attacker was never supposed to exist — except the default config invited one.

The chain

From the advisory, the unauth’d path is a straight line:

  • tools/call → terminal_execute on the unauthenticated /mcp endpoint → shell as node (uid 1000) inside the bridge container.
  • Read every provider API keyOPENAI, GOOGLE, OPENROUTER, ANTHROPIC — from the container environment.
  • Spawn attacker-controlled agent swarms on the victim’s keys, spending their credits and operating under their identity.
  • Poison the AgentDB learning store by persisting an agentdb_pattern-store entry that “steers future AI outputs for all users of the instance.”

The last step is the one that should keep you up at night. Most RCE write-ups end at “rotate the keys and rebuild the box.” This one ends at a persistent, stateful artifact that shapes behavior. The NVD description confirms the full chain: “obtain a shell in the bridge container, read provider API keys, and poison AgentDB learning-store patterns.” The CNA-assigned vector is CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H, with CWE-78 (OS command injection), CWE-306 (missing authentication for critical function), and CWE-942 (permissive cross-domain policy). CISA’s SSVC assessment on July 9 marked it automatable: yes, technical impact: total — no public exploitation observed yet, which is the only relief in the record.

The fix: enforcement moved from prompts to the substrate

The remediation, ADR-166, merged as commit d00a0a4 with 112 checks passing, is a small masterclass in what agent-harness security should look like. The design contract is stated up front:

The MCP bridge is local-only by default. Public exposure is an explicit opt-in (MCP_BIND_HOST + MCP_AUTH_TOKEN), and the bridge exits ≠0 at boot if the operator opts into public without a token.

The changes, per PR #2521:

  • Loopback by default. docker-compose.yml publishes 127.0.0.1:3001:3001 and 127.0.0.1:27017:27017; the bridge binds BIND_HOST = MCP_BIND_HOST || '127.0.0.1'.
  • Fail-closed public exposure. Public bind without a token → non-zero exit with a FATAL message. ruflo/docker-compose.public.yml is now the explicit override composition, requiring both MCP_BIND_HOST=0.0.0.0 and MCP_AUTH_TOKEN generated via openssl rand -base64 32.
  • Bearer auth where it matters. A requireAuth middleware (constant-time timingSafeEqual) guards /mcp*, /chat/completions, /autopilot*, and /mcp-servers when a token is set.
  • A server-side tool gate. executeTool() in both bridges denies terminal_execute (and any *terminal_execute* name) unless MCP_ENABLE_TERMINAL=true — enforced at the same site every request path reaches, returning {error, code: "TOOL_DISABLED"}.
  • MongoDB auth by default. --auth plus a hard-required MONGO_INITDB_ROOT_PASSWORD to boot.
  • A read-only bridge rootfs (read_only: true + tmpfs: /tmp) — which, per the PR, “breaks step 7 of the disclosed PoC” where a beacon file write fails with EROFS.
  • CI regression locks: static-source checks, runtime tests that spawn each bridge and assert 401/200/TOOL_DISABLED/exit-1 over the wire, and a compose-defaults gate.

Notice what this list is: no prompt hardening, no model alignment. Every control is architectural — where the process binds, whether the filesystem is writable, whether the database requires a password, whether the dangerous tool exists behind an explicit opt-in flag. That is the pattern the rest of the agent ecosystem needs to copy.

What common intuition gets wrong

The most instructive artifact of this whole episode is the pushback the maintainers cite from their own review thread:

“If someone has unauthorized access to your environment, you have bigger problems. The MCP is local so unauthorized is the only option.”

That argument — “it’s local, so auth is pointless” — is exactly backwards for a harness with a shipped compose file. The default deployment was the trust boundary, and the compose file bound the bridge to 0.0.0.0 by default. “Local-only” was an assumption, not an enforcement. The maintainers’ resolution is the right one: keep the zero-friction loopback path for the local CLI case (where trust flows from the spawning process over stdio), and concentrate the auth cost entirely on operators who explicitly choose public exposure. Security cost should be paid by the party assuming the risk — not by everyone, and not by no one.

The second intuition worth killing: a patched redeploy is not remediation. The advisory’s operator checklist makes this explicit — after upgrading:

  1. Firewall :3001 and :27017 immediately.
  2. Rotate OPENAI / GOOGLE / OPENROUTER / ANTHROPIC keys.
  3. Audit the AgentDB pattern store for injected agentdb_pattern-store entries and purge poisoned patterns — “A patched redeploy alone does NOT undo poisoning.”
  4. Audit MongoDB for tampering.

When a security boundary fails in an agent system, the state inside it is part of the blast radius. Agent memory is not a cache you can clear by rebuilding; it is a control input to future behavior. Treat it as such.

The uncomfortable truth

A CVSS 10.0 does not need an exotic exploit chain. It needed a default port binding, an unauthenticated route, and a tool gate that existed in only one code path. CISA’s SSVC flags it as automatable — a script can do what the researcher did.

The uncomfortable part for anyone building on this stack: Ruflo is not a toy. It is a 67k-star harness with agent-swarm coordination, adaptive memory, and MCP integration — the exact category of infrastructure that teams are standing up weekly with docker compose up. The learning store that makes it “self-learning” is also the part that turns a one-shot RCE into persistent behavioral poisoning that outlives the patch. A model you can’t trust is a problem; a memory store you can’t trust is a compromise that keeps working after you’ve fixed the door.

The verifier in this loop was the deployment default, and the default failed. Patch, rotate, audit the pattern store — and then treat every other harness in your stack as if it has the same skeleton in its compose file, because until the tool gate lives in the substrate, the next 10.0 is one default away.

Sources

Keep reading