Leaving your AI agent's brain surgery tools on an unlocked table in the hospital lobby
CVE-2026-59726, codenamed RufRoot by Noma Labs, is an unauthenticated remote code execution flaw in Ruflo, an open-source AI agent orchestration platform (~66,500 GitHub stars, ~700K lifetime npm downloads) that coordinates Claude Code and OpenAI Codex agent swarms. All versions prior to 3.16.3 ship a docker-compose.yml that binds an Express.js MCP bridge on port 3001 to 0.0.0.0 — every network interface — with zero authentication on the POST /mcp and POST /mcp/:group endpoints. This exposes 233 tools including ruflo__terminal_execute, which runs arbitrary shell commands inside the bridge container. A single curl POST is the entire exploit. The attacker gains shell access as node (UID 1000), can steal LLM provider API keys from environment variables, dump all conversations from the unauthenticated MongoDB sidecar, poison the AgentDB learning store to corrupt future AI outputs for all users, and establish persistence via index.js injection that survives docker restart.
The vendor CVSS of 10.0 is technically correct for the vulnerability mechanics — unauthenticated, network-accessible, zero-complexity, scope-changing RCE. But it overstates real-world risk for most deployments. Ruflo is a developer-oriented tool, not core enterprise infrastructure. The vast majority of instances run on developer laptops behind NAT, in ephemeral test environments, or in cloud VPCs protected by security groups. No Shodan or Censys data was found showing mass internet exposure. The EPSS score of 3.019% (~95th percentile, but still low absolute probability) and the absence of KEV listing or confirmed in-the-wild exploitation campaigns corroborate that real-world exploitation pressure is moderate, not maximum. However, for the subset of deployments in CI/CD pipelines, shared team environments, or misconfigured cloud VMs, the vulnerability is devastating — the API key theft alone extends blast radius well beyond the container, and the memory poisoning creates a supply-chain-grade integrity compromise. The vendor's CRITICAL is the right call for internet-exposed instances; for the typical deployment behind a firewall, HIGH is the honest assessment.
6 steps from start to impact.
Network reachability to MCP bridge
docker-compose.yml binds to 0.0.0.0:3001, so any host on the same network segment — or the entire internet if firewall rules are absent — can connect. No authentication header, API key, or IP allowlist is enforced.- Network path to target host on TCP/3001
- Ruflo version < 3.16.3 running default docker-compose config
- Most developer workstations sit behind NAT/home routers that block inbound connections
- Enterprise firewalls typically do not permit inbound traffic to ephemeral developer ports
- Cloud security groups default-deny inbound on non-standard ports in AWS, GCP, Azure
CVE-2026-59726 (PR #17249 in projectdiscovery/nuclei-templates) can scan for exposed /mcp endpoints. Port scan for TCP/3001 across your estate.Tool enumeration via tools/list
{"method":"tools/list"} to POST /mcp. The bridge returns all 233 registered tools with their schemas, confirming the instance is vulnerable and revealing the full attack surface. No credentials are required. This step doubles as a fingerprint for the target.- Step 1 completed — network reachability confirmed
- None once Step 1 is achieved — the endpoint is completely open
/mcp path. Sigma rule 550d9a23-0f72-4e3a-a8c5-1d2f3b4c5d6e detects suspicious child processes.Remote code execution via terminal_execute
{"method":"tools/call","params":{"name":"ruflo__terminal_execute","arguments":{"command":"id && hostname"}}} to POST /mcp. The bridge executes the command as the node user (UID 1000) inside the Docker container and returns stdout. The AUTOPILOT_BLOCKED_PATTERNS blocklist only applies within the autopilot flow and does NOT gate the /mcp endpoint. Exploitation is 100% reliable — no race conditions, memory corruption, or payload tuning required.- Step 2 completed — target confirmed as vulnerable Ruflo instance
- Execution is container-scoped (node user, not host root) — limits immediate host-level damage unless container escape is chained
node process spawning bash, sh, nc, python, or perl child processes. Sigma rule 7a3f1c82-9e4b-4d67-bc12-3e5a8f901234.LLM API key theft from environment variables
env or printenv via terminal_execute to harvest LLM provider API keys (Anthropic, OpenAI, etc.) passed as environment variables to the container. These keys typically have broad permissions and no IP restrictions. With stolen keys the attacker can run arbitrary LLM workloads at the victim's expense, exfiltrate training data via API, or pivot to other services authenticated by the same credentials.- Step 3 completed — shell access achieved
- Some organizations use secret managers that inject short-lived tokens rather than static API keys, limiting the window of abuse
AgentDB memory poisoning
ruflo__agentdb_pattern-store, the attacker injects malicious patterns into the learning store. These patterns alter future AI agent outputs for all users of the instance — a persistent integrity compromise. Poisoned patterns survive container restarts because AgentDB is backed by MongoDB volumes. This is the supply-chain dimension of the vulnerability: downstream consumers of the AI agent's outputs receive attacker-influenced results.- Step 3 completed — can invoke any of the 233 tools
- Impact is limited to users of this specific Ruflo instance, not a global supply-chain compromise
- Requires the instance to be actively used by multiple consumers for the poisoning to have meaningful blast radius
pattern-store entries for unexpected or recently-modified patterns. Compare against known-good baseline.Persistence via index.js backdoor
/app/beacon.js and injects a require('./beacon') line into /app/index.js. Because Docker restart policies re-execute the entrypoint, the backdoor persists across container restarts. The beacon can establish a reverse shell, exfiltrate data periodically, or serve as a staging point for lateral movement within the Docker network.- Step 3 completed — write access to /app directory inside the container
- The fix in 3.16.3 makes the container filesystem read-only with tmpfs, eliminating this persistence vector
- Container rebuilds from clean images destroy the backdoor
/app/ directory inside the container. Monitor for unexpected outbound connections from the container.iptables/ufw/Windows Firewall) and any upstream network firewall eliminates the attack surface completely. Deploy within the noisgate mitigation SLA of 30 days for HIGH severity. For cloud deployments, audit security groups immediately.terminal_execute unless explicitly opted in, enables MongoDB authentication, and sets the container filesystem read-only. This is the definitive fix. Deploy within the noisgate remediation SLA of 180 days for HIGH severity.agentdb_pattern-store collection in MongoDB for unexpected or recently-modified entries. Compare against a known-good baseline. If any suspicious patterns are found, wipe the learning store and rebuild all containers from a clean image rather than restarting existing ones. Check /app/index.js for injected require() statements.- Container network isolation alone — the default docker-compose creates a bridge network, but the
0.0.0.0binding on the host bypasses container network isolation entirely. The host port mapping is the problem, not the container networking. - AUTOPILOT_BLOCKED_PATTERNS blocklist — this only applies within Ruflo's autopilot flow and does NOT gate the
/mcpendpoint. Do not rely on it as a security control. - WAF rules on the MCP endpoint — generic WAFs will not inspect JSON-RPC payloads for tool invocation patterns. A custom rule matching
ruflo__terminal_executein the request body could help but is brittle and bypass-prone.
The supporting signals.
| In-the-wild exploitation | No confirmed in-the-wild exploitation. Not listed in CISA KEV. No GreyNoise tags observed. Noma Labs verified an 8-stage PoC against a default AWS EC2 deployment, but no campaigns targeting production instances have been reported as of 2026-10-07. |
|---|---|
| Proof-of-concept availability | Public and trivial. The exploit is a single curl one-liner published in the Noma Labs advisory and The Hacker News. A Nuclei template (PR #17249 by DhiyaneshGeek) enables mass scanning. GitHub repo HORKimhab/CVE-2026-59726 contains additional analysis. |
| EPSS score | 0.03019 (3.019%) — approximately 95th percentile. Indicates moderate but not extreme predicted exploitation probability over the next 30 days. |
| CISA KEV status | Not listed as of 2026-10-07. |
| CVSS vector interpretation | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H = 10.0. Network-accessible, zero complexity, no privileges, no user interaction, scope change (container → host environment via credential theft), full CIA impact. The vector is technically accurate for the worst-case deployment. |
| Affected versions | All Ruflo versions < 3.16.3 (npm packages: ruflo, claude-flow, @claude-flow/cli). Includes v3.5.x through v3.16.2 and all pre-release tags. |
| Fixed version | 3.16.3 — released 2026-07-01, within 24 hours of responsible disclosure. Fix commits: d00a0a40cd8b and 4eb807aa7cfc (PR #2521). Binds bridge to loopback, adds bearer auth middleware, disables terminal_execute by default, enables MongoDB auth, sets container filesystem read-only. |
| Scanning / exposure data | No Shodan or Censys data found documenting internet-exposed Ruflo instances. The default 0.0.0.0 binding creates exposure on local networks and misconfigured cloud VMs, but no mass internet scanning results have been published. |
| Disclosure timeline | 2026-06-30: Noma Labs responsible disclosure to maintainer Reuven Cohen. 2026-07-01: Patch released (v3.16.3). 2026-07-09: CVE published. 2026-07-29: Full public disclosure by Noma Labs. |
| Reporting researcher | Eli Ainhorn, Noma Labs (Noma Security). Codenamed the vulnerability RufRoot. |
Sources.
Why this verdict
- Network reachability is the gating friction: The default
0.0.0.0binding is dangerous, but most Ruflo instances run on developer laptops behind NAT, in ephemeral containers, or in cloud VPCs with security groups that default-deny inbound on port 3001. Without Shodan/Censys evidence of mass internet exposure, the effective attack surface is dramatically smaller than the CVSS vector implies. - Container-scoped compromise limits host-level blast radius: Initial execution is as
node(UID 1000) inside a Docker container — not root on the host. Host compromise requires a container escape chain not included in this CVE. The real damage extends beyond the container via stolen API keys and memory poisoning, but those are secondary impacts, not direct host takeover. - EPSS and KEV corroborate moderate real-world risk: EPSS of 3.019% is elevated but not extreme. No KEV listing. No confirmed in-the-wild exploitation campaigns. The vulnerability has been public for 3 months without mass exploitation materializing, suggesting the reachable population is small.
- Niche developer tool, not enterprise infrastructure: Ruflo has ~66,500 GitHub stars but only ~23,000 weekly npm downloads. It is not a domain controller, hypervisor, identity provider, or network edge appliance. The installed base is primarily individual developers and small teams experimenting with AI agents.
- Role multiplier: (a) *Low-value role (developer workstation/sandbox)* — estimated ≥80% of installs. Chain succeeds if port 3001 is reachable on LAN; blast radius is single-developer credential theft and local AI poisoning. (b) *Typical role (team-shared AI agent server)* — estimated ~15% of installs. Chain succeeds if accessible on internal network; blast radius is team-wide API key compromise and shared AI memory corruption. (c) *High-value role (CI/CD-adjacent deployment orchestrating autonomous coding agents)* — estimated ~5% of installs. Chain succeeds and blast radius includes supply-chain integrity compromise (poisoned code generation patterns, stolen CI credentials). This ≥1% high-value-role threshold floors the verdict at HIGH. However, Ruflo is NOT canonically a CI/CD server (it's an orchestration layer on top), so the floor does not rise to CRITICAL.
Why not higher?
The vendor's CRITICAL 10.0 assumes direct internet exposure with full blast radius. In practice, the vast majority of Ruflo deployments are behind NAT or cloud security groups, making the network reachability prerequisite the dominant friction point. Without confirmed mass internet exposure or in-the-wild exploitation, elevating to CRITICAL would overstate the urgency for most enterprises and misallocate patching resources away from genuinely internet-facing CRITICAL vulnerabilities.
Why not lower?
Despite limited exposure, the exploit is a trivial one-liner curl command with a public Nuclei template for mass scanning. If an instance IS reachable — on a misconfigured cloud VM, a shared development server, or an internal network after initial compromise — exploitation is 100% reliable with zero authentication required. The API key theft and AI memory poisoning extend blast radius beyond the container, and the CI/CD deployment scenario creates a legitimate supply-chain risk that floors this at HIGH.
Crowdsourced verification payload.
Run this script on each host where Ruflo may be deployed (developer workstations, CI/CD runners, Docker hosts). Execute as any user with access to docker commands. Example: chmod +x check_rufroot.sh && ./check_rufroot.sh
#!/usr/bin/env bash
# CVE-2026-59726 (RufRoot) Verification Script
# Checks for vulnerable Ruflo MCP bridge instances
# Exit codes: 0 = PATCHED/not present, 1 = VULNERABLE, 2 = UNKNOWN
set -euo pipefail
VULNERABLE=0
UNKNOWN=0
# Check 1: Is port 3001 listening on 0.0.0.0?
echo "[*] Checking for MCP bridge listening on 0.0.0.0:3001..."
if ss -tlnp 2>/dev/null | grep -q '0.0.0.0:3001'; then
echo "[!] FOUND: Port 3001 bound to 0.0.0.0 (all interfaces)"
VULNERABLE=1
elif ss -tlnp 2>/dev/null | grep -q ':3001'; then
echo "[i] Port 3001 is listening but bound to loopback only (likely patched)"
else
echo "[i] Port 3001 is not listening"
fi
# Check 2: Probe the /mcp endpoint for unauthenticated access
if command -v curl &>/dev/null; then
echo "[*] Probing localhost:3001/mcp for unauthenticated tool listing..."
RESP=$(curl -s -m 5 -X POST http://127.0.0.1:3001/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' 2>/dev/null || true)
if echo "$RESP" | grep -q 'ruflo__terminal_execute'; then
echo "[!] VULNERABLE: MCP bridge returned terminal_execute tool without auth"
VULNERABLE=1
elif echo "$RESP" | grep -q '"error"'; then
echo "[i] MCP bridge returned error (likely patched or auth-gated)"
elif [ -z "$RESP" ]; then
echo "[i] No response from MCP bridge (not running or filtered)"
fi
fi
# Check 3: Check installed ruflo/claude-flow version via npm
for PKG in ruflo claude-flow @claude-flow/cli; do
if command -v npm &>/dev/null; then
VER=$(npm list -g "$PKG" 2>/dev/null | grep "$PKG@" | head -1 | sed 's/.*@//' || true)
if [ -n "$VER" ]; then
echo "[*] Found $PKG@$VER installed globally"
# Compare version - vulnerable if < 3.16.3
MAJOR=$(echo "$VER" | cut -d. -f1)
MINOR=$(echo "$VER" | cut -d. -f2)
PATCH=$(echo "$VER" | cut -d. -f3 | cut -d- -f1)
if [ "$MAJOR" -lt 3 ] 2>/dev/null || \
([ "$MAJOR" -eq 3 ] && [ "$MINOR" -lt 16 ] 2>/dev/null) || \
([ "$MAJOR" -eq 3 ] && [ "$MINOR" -eq 16 ] && [ "$PATCH" -lt 3 ] 2>/dev/null); then
echo "[!] VULNERABLE: $PKG@$VER is below 3.16.3"
VULNERABLE=1
else
echo "[i] PATCHED: $PKG@$VER is >= 3.16.3"
fi
fi
fi
done
# Check 4: Check running Docker containers
if command -v docker &>/dev/null; then
echo "[*] Checking running Docker containers for ruflo..."
CONTAINERS=$(docker ps --format '{{.Names}} {{.Image}}' 2>/dev/null | grep -i ruflo || true)
if [ -n "$CONTAINERS" ]; then
echo "[!] Found ruflo containers running:"
echo "$CONTAINERS"
# Check if port 3001 is mapped to 0.0.0.0
if docker ps --format '{{.Ports}}' 2>/dev/null | grep -q '0.0.0.0:3001'; then
echo "[!] VULNERABLE: Container has 0.0.0.0:3001 port mapping"
VULNERABLE=1
fi
else
echo "[i] No ruflo containers found running"
fi
fi
# Final verdict
echo ""
if [ "$VULNERABLE" -eq 1 ]; then
echo "VULNERABLE — CVE-2026-59726 (RufRoot) exposure detected"
exit 1
else
echo "PATCHED — No CVE-2026-59726 exposure detected"
exit 0
fi