FlawOpen

flawopen.com/Teardowns/cve-2026-105699-langflow-mcp-cross-project-file-disclosure

● CVE-2026-105699 · CVSS v4 7.1 · Alta
Investigación · FlawOpen

CVE-2026-105699: Archivos MCP en Langflow

Langflow autenticaba la conexión MCP, pero no autorizaba cada archivo solicitado. Un usuario podía leer archivos de flujos ajenos; la versión 1.9.1 corrige la comprobación de titularidad.[1], [2], [5]

Vector CVSSCVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N
💡 Explicación en Lenguaje Sencillo (ELI5)

Imagina un hotel que comprueba que tienes una llave y después te entrega el equipaje de cualquier habitación si dices su etiqueta. La llave demuestra que puedes entrar al hotel, no que la maleta de otro huésped sea tuya. La lectura MCP de Langflow debe comprobar el propietario y el proyecto antes de abrir el archivo.[1]

Conceptos Clave y Términos

Model Context Protocol (MCP)
Protocolo por el que una aplicación de IA solicita herramientas y recursos a un servidor. El servidor debe autorizar cada objeto solicitado.[1]
Endpoint MCP ligado a un proyecto
Conexión MCP de Langflow asociada a un proyecto. Acceder al endpoint no concede acceso a archivos de todos los proyectos.[1]
resources/read
Operación MCP que solicita al servidor un recurso identificado mediante una URI.[1]
Autorización a nivel de objeto
Comprobación de que el usuario autenticado puede acceder al registro o archivo concreto, no solo al endpoint.[1], [6]
CWE-639
Elusión de autorización mediante una clave controlada por el usuario: cambiar un identificador permite alcanzar el registro de otra persona.[6], [2]

Mapeos de marcos

  • Referencia CWE para la debilidad descrita en esta guía: CWE-639

Análisis de Causa Raíz

En las versiones afectadas de Langflow, de la 1.6.8 a la 1.9.0, el transporte MCP ligado a un proyecto autenticaba el acceso al ID del proyecto incluido en la URL de conexión. Después, el manejador resources/read recibía una URI, extraía un ID de flujo y un nombre de archivo y los enviaba a storage_service.get_file() sin comprobar que el flujo perteneciera al usuario autenticado o al proyecto actual. El almacenamiento lee por espacio de nombres y nombre de archivo; no conoce los permisos de usuario ni de proyecto de Langflow. Por ello, un usuario con cuenta y acceso al endpoint MCP de su propio proyecto podía solicitar un archivo del espacio de nombres de flujo de otra persona. El comportamiento global de resources/list y tools/list, documentado por separado, podía revelar identificadores y facilitar la selección del objetivo, pero no era necesario para la lectura. Es una exposición de confidencialidad autenticada entre usuarios, no acceso anónimo ni ejecución de código. El registro CVE asigna CVSS 4.0 7.1 High y CWE-639. Langflow 1.9.1 corrige el flujo: handle_read_resource() obtiene al usuario autenticado y busca el flujo exigiendo Flow.user_id == current_user.id y, en transportes de proyecto, Flow.folder_id == project_id; rechaza los espacios no autorizados antes de acceder al almacenamiento. El parche también rechaza separadores y secuencias de traversal, aplica contención en rutas de lectura/escritura/borrado y limita los listados globales al usuario actual. La prueba de regresión del mantenedor deniega la lectura del flujo de otro usuario.[1], [2], [3], [5]

Flujo de Ataque Paso a Paso

Paso 1

El usuario se autentica en su propio endpoint MCP de proyecto

El atacante necesita una cuenta Langflow y acceso a al menos una conexión MCP de proyecto. La vulnerabilidad no elude el inicio de sesión.[1]

Paso 2

El cliente solicita una URI de recurso

La solicitud resources/read incluye una URI cuyos últimos componentes identifican un espacio de nombres de flujo y un archivo.[1]

Paso 3

El manejador vulnerable confía en los identificadores

handle_read_resource() extraía el ID y el nombre, pero no vinculaba el flujo con el usuario ni con el proyecto del endpoint.[1], [3]

Paso 4

El almacenamiento lee por espacio de nombres

storage_service.get_file() devolvía el archivo del espacio indicado. El almacenamiento no tenía contexto de titularidad del usuario.[1]

Paso 5

La respuesta cruza la frontera entre usuarios

El solicitante podía recibir el archivo de flujo de otra persona. Los listados globales podían revelar identificadores, pero no eran necesarios para la falta de autorización.[1], [2]

Código Fuente: Vulnerable vs. Seguro

Son fragmentos enfocados en Python del flujo documentado, no una aplicación independiente. El camino vulnerable trataba el espacio de nombres enviado por el cliente como autoridad suficiente. La corrección de 1.9.1 resuelve primero el flujo dentro del usuario autenticado y, para transportes MCP de proyecto, del proyecto actual; solo entonces permite leerlo desde el almacenamiento. El parche también valida los nombres de archivo y aplica contención de rutas en la capa de almacenamiento para que otros puntos de lectura/escritura/borrado no eludan el control. Mantén la autorización en el servicio que conoce al usuario y al proyecto: un adaptador de almacenamiento no puede inferir la titularidad. Alcance del fragmento: el controlador real también permite un archivo del bucket del usuario únicamente cuando el espacio de nombres coincide con el ID del propio usuario autenticado y el servidor no tiene ámbito de proyecto. El fragmento omite esa rama independiente; no debe interpretarse como acceso a cualquier espacio de nombres. La suite de regresión upstream incluye test_handle_read_resource_denies_other_users_flow, que verifica que se deniegue la lectura entre usuarios.[1], [3], [4], [7]

Fragmento de origen: mcp_utils.py — fragmentos de código seleccionados

IMPLEMENTACIÓN VULNERABLE
# Fragmento de la ruta de lectura afectada: los ID analizados se enviaban directamente al almacenamiento.
content = await storage_service.get_file(
    flow_id=flow_id,
    file_name=filename,
)
PARCHE SEGURO Y ROBUSTO
# Reducción con estructura de la fuente del límite de autorización de la versión 1.9.1.
current_user = current_user_ctx.get()
query = select(Flow).where(
    Flow.id == namespace_id,
    Flow.user_id == current_user.id,
)
if project_id is not None:
    query = query.where(Flow.folder_id == project_id)
flow = (await session.exec(query)).first()
if flow is None:
    raise ValueError("Resource not found or access denied")
content = await storage_service.get_file(
    flow_id=namespace_id,
    file_name=filename,
)

Lista de Verificación de Seguridad para Ingeniería

Fuentes