Ruflo CVE-2026-59726: la RCE sul bridge MCP che sopravvive alla patch
Ruflo CVE-2026-59726 (CVSS 10.0) esponeva 233 tool MCP senza autenticazione. La RCE è chiusa in 3.16.3 — la memoria degli agenti avvelenata che ha lasciato dietro no.
Quasi tutte le vulnerabilità critiche finiscono quando installi la patch. Questa no, ed è il motivo per cui merita la vostra mattinata. Il 29 luglio 2026 Noma Labs ha pubblicato RufRoot, una falla di severità massima in Ruflo — l'harness di orchestrazione multi-agente nato come Claude Flow, oltre 67.000 stelle su GitHub e, secondo i numeri dichiarati da Noma, circa 10 milioni di download. Registrata come CVE-2026-59726 con punteggio CVSS 10.0 (AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H), permetteva a chiunque riuscisse a raggiungere la porta TCP 3001 di eseguire comandi shell dentro il container del bridge con una singola richiesta HTTP POST non autenticata. Il maintainer ha rilasciato la correzione in meno di 24 ore. Quella correzione chiude la porta. Non rimuove chi è già entrato — perché la cosa più duratura che un attaccante poteva fare qui era scrivere un record nella memoria persistente della piattaforma di agenti, e un redeploy patchato lascia quel record esattamente dov'era.
Cosa esponeva davvero CVE-2026-59726
L'MCP Bridge di Ruflo è un server Express.js che gestisce le invocazioni di tool via Model Context Protocol. Ogni azione di un agente, ogni scrittura in memoria, ogni chiamata a un tool passa da lì. Nelle parole di Noma, il bridge "non è una interfaccia di debug ausiliaria qualsiasi: è il sistema nervoso centrale di Ruflo".
Il docker-compose.yml distribuito legava quel bridge a 0.0.0.0:3001 e non applicava alcuna autenticazione su POST /mcp né su POST /mcp/:group. Dietro quei due endpoint stavano 233 tool: esecuzione di comandi shell, operazioni sul database, gestione del ciclo di vita degli agenti, scrittura sulla memoria persistente. Nessun token, nessuna API key, nessun controllo di header, nessuna allowlist di IP. Anche MongoDB era esposto su tutte le interfacce, senza autenticazione, sulla 27017.
La proof of concept è una sola richiesta:
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"}}}'
Restituisce l'output del comando eseguito come utente node del container. L'advisory GitHub GHSA-c4hm-4h84-2cf3 classifica il problema come CWE-78 (OS command injection) insieme a CWE-306 (autenticazione assente su una funzione critica): è il secondo a portare il punteggio a 10.0. Noma ha validato la catena su deployment di default su AWS EC2 — cioè su quello che ottenete seguendo la quickstart.
Gli otto passi della catena, e perché conta il quinto
Il report di Noma percorre l'intero percorso. Mappato su MITRE ATT&CK, con gli ID tecnica presenti nel corpus ATT&CK sincronizzato di Zero Hunt:
| # | Passo | Tool / meccanismo | ATT&CK |
|---|---|---|---|
| 1 | Enumerazione | tools/list elenca tutti i 233 tool, senza autenticazione |
T1190 |
| 2 | Esecuzione | ruflo__terminal_execute → shell come node |
T1059.013 |
| 3 | Furto chiavi | chiavi OpenAI / Anthropic / Google / OpenRouter dall'env del container | T1552.007 |
| 4 | Agenti ostili | ruflo__swarm_init, ruflo__agent_spawn → swarm dell'attaccante sulle chiavi della vittima |
T1078.004 |
| 5 | Memoria avvelenata | ruflo__agentdb_pattern-store → pattern malevoli nel learning store |
T1565.001 |
| 6 | Lettura conversazioni | MongoDB non autenticato sulla rete Docker | T1213 |
| 7 | Persistenza | beacon scritto nella directory scrivibile /app, riavviato in automatico |
T1505 |
| 8 | Pulizia | cronologia shell azzerata | T1070.003 |
I passi da 1 a 4 e da 6 a 8 sono una brutta giornata ordinaria: ricostruisci il container da un'immagine pulita, ruoti le credenziali e li hai annullati. Il passo 5 è diverso per natura. agentdb_pattern-store è l'interfaccia attraverso cui gli agenti di Ruflo imparano. Un attaccante che ci scrive dentro non sta lasciando un file: sta inserendo un'istruzione nella conoscenza istituzionale della piattaforma. La dimostrazione di Noma era una finta policy di conformità SOC 2 che ordinava agli agenti di includere una URL controllata dall'attaccante in ogni script di deploy generato. La loro valutazione dell'effetto: "le conversazioni future producono output influenzato dall'attaccante, e l'utente non ha modo di accorgersene".
Perché la MCP security fallisce sempre allo stesso modo
Non è una svista specifica di Ruflo, e trattarla come tale è il modo migliore per ritrovarsi le prossime quattro. La specifica di autorizzazione MCP definisce un framework OAuth 2.1 e poi marca l'autorizzazione come opzionale per le implementazioni. Una scansione di internet del luglio 2025 citata nella research note della Cloud Security Alliance sulla MCP security (4 maggio 2026) ha individuato 1.862 server MCP pubblicamente raggiungibili che rispondevano a richieste di tool-listing non autenticate. Autenticazione opzionale in una specifica diventa autenticazione assente in un docker-compose.yml di default, e le configurazioni di default sono quelle che gira la stragrande maggioranza dei deployment.
Su CSO Online, Amit Jena di Kanerika ha centrato il punto generalizzabile: "qualsiasi piattaforma che dia agli agenti uno store di memoria persistente e scrivibile deve trattare quello store come un confine di sicurezza" — e oggi lo fanno in pochissimi.
Lo schema ha anche nove anni. CVE-2017-20198 è la Marathon UI in DC/OS prima della 1.9.0, che permetteva a utenti non autenticati di deployare container Docker arbitrari perché il control plane di orchestrazione era stato rilasciato senza uno strato di autenticazione. Stessa forma, decennio diverso: un control plane pensato per una rete locale fidata, poi containerizzato, poi legato a 0.0.0.0 da un file compose che nessuno ha più riletto. A giugno avevamo raccontato il caso adiacente, quando gli endpoint di test MCP di LiteLLM hanno trasformato l'AI gateway in una superficie di RCE. L'harness di agenti è ormai la stessa categoria di asset che il gateway è diventato: privilegi alti, pieno di credenziali e assente dall'inventario.
La memoria avvelenata è un dato, non un malware
Ogni runbook di bonifica in mano ai vostri team presuppone che l'artefatto sia un file. Hash, regole YARA, quarantena EDR, ricostruzione dell'immagine. Un pattern AgentDB avvelenato le batte tutte, perché è una riga legittima in un datastore legittimo, scritta attraverso l'API legittima, nello stesso formato di ogni voce onesta creata dai vostri ingegneri. Non ha un hash che valga la pena bloccare. Ricostruire il container lo preserva, perché il senso stesso dello store è sopravvivere al ciclo di vita del container. Noma è esplicita: il solo redeploy della versione patchata non annulla l'avvelenamento.
"Abbiamo aggiornato alla 3.16.3 in giornata. Siamo puliti."
Davvero? Siete stati in 3.16.2 con la 3001 su
0.0.0.0per undici settimane. Il bridge accettavatools/callda chiunque lo trovasse e non ha loggato niente che abbiate conservato. Le chiavi dei provider sono rimaste nell'environment di quel container per tutto il tempo. Se qualcuno ha scritto un pattern in AgentDB alla terza settimana, i vostri agenti generano da allora output modellato da quell'istruzione — e la patch installata mercoledì non ha cambiato nulla di tutto questo. Ruotare le chiavi era necessario. Leggere lo store di memoria è la parte che nessuno ha messo a calendario.
È questo a rendere la classe degna di una policy, non solo di una patch. Qualsiasi piattaforma di agenti con un learning store scrivibile — memoria, indice RAG, feedback loop, coda di fine-tuning — ha una superficie di integrità che nessun controllo attualmente in esercizio nella vostra organizzazione copre. L'accesso in scrittura a quello store equivale all'accesso in scrittura al comportamento del modello, per ogni utente futuro, senza crash, senza alert e senza artefatto.
Remediation
1. Sono impattato?
Tutto ciò che è sotto la 3.16.3 è vulnerabile; se siate stati sfruttabili dipende da quanto il bridge fosse raggiungibile.
# Versione, lato host e dentro il container
npm ls -g ruflo claude-flow @claude-flow/cli 2>/dev/null
docker exec <bridge-container> npx ruflo --version
# A cosa è legato davvero il bridge?
ss -lntp | grep -E ':3001|:27017'
docker compose ps --format '{{.Service}}\t{{.Ports}}'
# "0.0.0.0:3001->3001/tcp" è il finding. "127.0.0.1:3001->3001/tcp" no.
# La prova che chiude la questione — da un ALTRO host della rete
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
Se l'ultimo comando restituisce l'elenco dei tool senza token, l'istanza era aperta a chiunque avesse una rotta verso di essa. Consideratela compromessa e andate al passo 4 — non fermatevi al passo 2.
2. Patch — versioni corrette esatte
Aggiornate ruflo, claude-flow e @claude-flow/cli alla 3.16.3 o successiva. Secondo le release note, la 3.16.3 cambia i default in modo da bloccare deliberatamente un aggiornamento distratto:
- il bridge MCP si lega a
127.0.0.1per default; un bind pubblico richiedeMCP_AUTH_TOKENo il processo esce con FATAL all'avvio; - autenticazione a bearer token con confronto a tempo costante;
terminal_executeprotetto daMCP_ENABLE_TERMINAL=true, disattivato per default;- autenticazione MongoDB obbligatoria —
MONGO_INITDB_ROOT_PASSWORDva valorizzata o il container non parte (openssl rand -base64 32); - rootfs del container in sola lettura con tmpfs; allowlist CORS via
MCP_CORS_ORIGINal posto del wildcard.
Se l'aggiornamento "ha funzionato subito" senza toccare la configurazione, verificate di aver riavviato davvero dal nuovo file compose e non da un override locale.
3. Non potete patchare subito? Controlli compensativi
- Filtrate la 3001 e la 27017 su loopback o su una allowlist esplicita. Solo questo elimina il percorso di rete non autenticato.
- Riscrivete il mapping di porta come
127.0.0.1:3001:3001nel file compose e ricreate il container. Pubblicare su0.0.0.0scavalca le regoleiptablesdell'host con il networking di default di Docker: il controllo che tiene è l'indirizzo di bind. - Se l'accesso remoto serve davvero, mettete davanti un reverse proxy autenticante e non esponete mai il bridge in diretta.
- Fate girare il container come utente non privilegiato con rootfs in sola lettura, e togliete il permesso di scrittura su
/appcosì che il passo 7 della catena fallisca. - Limitate ambito e rate di ogni API key di provider presente nel container, con tetti di spesa. Una chiave rubata con un limite di 50 dollari al giorno è un incidente circoscritto.
4. Cercare tracce di compromissione
Partite dal presupposto che non sia stato loggato nulla e cercate sullo stato, non sugli alert.
# Chiamate al bridge da qualcosa che non sia loopback
docker logs <bridge-container> 2>&1 | grep -E 'tools/call|terminal_execute' | grep -v '127.0.0.1'
# File aggiunti o modificati rispetto all'immagine
docker diff <bridge-container>
docker exec <bridge-container> ls -la /app --time-style=full-iso
# Una history troncata è già un segnale (T1070.003)
docker exec <bridge-container> sh -c 'wc -l ~/.bash_history 2>/dev/null'
Segnali che meritano un ticket, mappati a tecnica:
POST /mcpo/mcp/:groupda un indirizzo sorgente non loopback — T1190.- Qualsiasi
tools/callcon nomeruflo__terminal_executenon attribuibile a un operatore — T1059.013. - Dashboard dei provider: picchi di spesa in token, chiamate da ASN o regioni in cui non operate, modelli mai abilitati — T1078.004.
- Connessioni non autenticate sulla 27017; collection o documenti fuori dal vostro schema — T1213.
- Voci nel pattern store di AgentDB che non avete scritto voi. Esportate lo store, ordinate per data di scrittura e riconciliate ogni voce con un deploy o con un ingegnere identificabile — T1565.001.
- Sessioni in uscita dal container del bridge verso destinazioni mai contattate prima, in particolare connessioni a intervalli regolari — T1071.001.
5. Bonificare e verificare
- Ricostruite da un'immagine 3.16.3 pulita. Non "ripulite" un container in esecuzione: il passo 8 della catena esiste proprio per rendere inaffidabile quel giudizio.
- Revocate — non semplicemente ruotate — ogni chiave di provider presente nell'environment del container (OpenAI, Anthropic, Google, OpenRouter) più le credenziali MongoDB. Emettere una chiave nuova lasciando valida la vecchia non cambia niente.
- Verificate il pattern store di AgentDB voce per voce ed eliminate tutto ciò che non è attribuibile. È il passo che viene saltato, ed è l'unico che affronta la persistenza che sopravvive a tutto il resto.
- Ispezionate MongoDB alla ricerca di record di conversazione manomessi prima di fidarvi di qualunque cosa ne esportiate.
- Rieseguite il
curlesterno del passo 1. Dopo la correzione deve restituire 401 o rifiutare la connessione. Confermate cheMCP_ENABLE_TERMINALnon sia valorizzata, che il bridge sia su127.0.0.1e che Mongo pretenda autenticazione.
Cosa significa validazione continua per l'infrastruttura ad agenti
Rileggete il passo 4 e notate cosa richiede. Non c'è un hash da bloccare né un alert che sia scattato. Le due cose che tradiscono davvero una piattaforma di agenti compromessa sono entrambe comportamentali: un endpoint che risponde agli sconosciuti, e un container il cui comportamento di rete smette di somigliare alla propria storia.
La seconda è ciò per cui è costruita l'AI Traffic Analysis di Zero Hunt. Un modello di deep learning addestrato su miliardi di sequenze PCAP, con quattro teste di inferenza in parallelo — traffico sospetto, classificazione malware, identificazione del tipo di attacco, fingerprinting applicativo — a 2,7+ Gbit/s sulla GPU dell'appliance, interamente on-premise. Applicato a questo scenario significa una cosa concreta: un container di agenti è tra gli host più profilabili della vostra rete, perché la sua baseline è stretta e ripetitiva. Quando inizia ad aprire sessioni verso un ASN mai visto, o uno swarm che nessuno ha pianificato comincia a bruciare token su TLS in uscita sostenuto, quello è uno scostamento da una baseline appresa e non un match di firma — che è l'unica rilevazione che funziona quando il payload è una riga di database e il C2 è HTTPS verso un'API legittima. E avviene mentre l'attività è in corso, non nel digest SIEM del mattino dopo.
La prima — un endpoint che risponde agli sconosciuti — è una domanda di perimetro, ed è quella che ha reso questo un 10.0 invece che un 8.8. Le campagne change-triggered di Zero Hunt avviano un run completo entro un'ora da quando un nuovo asset compare sul perimetro, e lo swarm di 10 agenti scrive un exploit per-target con un LLM locale invece di rigiocare una PoC pubblica: quello che viene validato è raggiungibilità e impatto nel vostro ambiente, cioè se la porta 3001 risponde a tools/list dall'esterno e cosa restituisce. Ogni finding è backtestato nell'AI Gym prima di arrivare in produzione e firmato ECDSA al momento della scrittura, così "il bridge è rimasto aperto dal 12 maggio al 30 luglio" diventa un elemento di prova da consegnare a un auditor o a un assicuratore, non un ricordo.
La parte scomoda di RufRoot è che nessuno ha ancora costruito il controllo che intercetta il passo 5. Osservare come si comporta l'agente dopo è, al momento, la cosa che ci va più vicino.
Se gestite infrastruttura ad agenti e volete una risposta sulla superficie esposta basata su evidenze e non su deduzioni, scriveteci.