FlawOpen

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

● CVE-2026-105699 · CVSS v4 7.1 · Haute
Recherche · FlawOpen

CVE-2026-105699 : fichiers MCP Langflow

Langflow authentifiait la connexion MCP, mais n’autorisait pas chaque fichier demandé. Un utilisateur pouvait lire les fichiers de flux d’un autre ; la version 1.9.1 corrige le contrôle de propriété.[1], [2], [5]

Vecteur 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
💡 Explication en Langage Simple (ELI5)

Imaginez un hôtel qui vérifie votre carte, puis vous remet les bagages de n’importe quelle chambre si vous indiquez leur étiquette. La carte prouve que vous pouvez entrer dans l’hôtel, pas que le sac d’un autre client vous appartient. La lecture MCP de Langflow doit vérifier le propriétaire et le projet avant d’ouvrir le fichier.[1]

Concepts Clés et Termes

Model Context Protocol (MCP)
Protocole permettant à une application d’IA de demander des outils et des ressources à un serveur. Celui-ci doit encore autoriser chaque objet demandé.[1]
Point de terminaison MCP lié à un projet
Connexion MCP de Langflow associée à un projet. L’accès à ce point ne donne pas accès aux fichiers de tous les projets.[1]
resources/read
Opération MCP qui demande au serveur une ressource identifiée par une URI.[1]
Autorisation au niveau de l’objet
Vérification que l’utilisateur authentifié peut accéder à l’enregistrement ou au fichier précis, et pas seulement au point de terminaison.[1], [6]
CWE-639
Contournement d’autorisation par une clé contrôlée par l’utilisateur : modifier un identifiant permet d’atteindre l’enregistrement d’un autre utilisateur.[6], [2]

Correspondances de référentiels

  • Référence CWE pour la faiblesse décrite dans ce guide : CWE-639

Analyse de Cause Racine

Dans les versions de Langflow affectées, de 1.6.8 à 1.9.0, le transport MCP lié à un projet authentifiait l’accès à l’identifiant du projet présent dans l’URL de connexion. Ensuite, le gestionnaire resources/read recevait une URI, en extrayait un identifiant de flux et un nom de fichier, puis les transmettait à storage_service.get_file() sans vérifier que le flux appartenait à l’utilisateur authentifié ou au projet courant. Le stockage lit par espace de noms et nom de fichier ; il ne connaît pas les autorisations Langflow des utilisateurs et des projets. Un utilisateur disposant d’un compte et de l’accès à son propre point MCP de projet pouvait donc demander un fichier dans l’espace de noms de flux d’un autre utilisateur. Le comportement global distinct de resources/list et tools/list pouvait révéler des identifiants et faciliter le ciblage, mais n’était pas nécessaire à la lecture. Il s’agit d’une atteinte authentifiée à la confidentialité entre utilisateurs, pas d’un accès anonyme ni d’une exécution de code. La fiche CVE attribue CVSS 4.0 7,1 High et CWE-639. Langflow 1.9.1 corrige le parcours : handle_read_resource() récupère l’utilisateur authentifié et recherche le flux en imposant Flow.user_id == current_user.id et, pour un transport de projet, Flow.folder_id == project_id; tout espace non autorisé est rejeté avant l’accès au stockage. Le correctif rejette aussi les séparateurs et séquences de traversal, vérifie le confinement des chemins de lecture/écriture/suppression et limite les listes globales à l’utilisateur courant. Le test de régression du mainteneur refuse la lecture du flux d’un autre utilisateur.[1], [2], [3], [5]

Déroulement de l'Attaque Étape par Étape

Étape 1

Un utilisateur s’authentifie sur son propre point MCP de projet

L’attaquant doit posséder un compte Langflow et accéder à au moins une connexion MCP de projet. La faille ne contourne pas la connexion.[1]

Étape 2

Le client demande une URI de ressource

La requête MCP resources/read contient une URI dont les derniers composants désignent un espace de noms de flux et un fichier.[1]

Étape 3

Le gestionnaire vulnérable fait confiance aux identifiants

handle_read_resource() extrayait l’identifiant et le nom, sans rattacher le flux à l’utilisateur ni au projet du point de terminaison.[1], [3]

Étape 4

Le stockage lit selon l’espace de noms

storage_service.get_file() renvoyait le fichier de l’espace indiqué. Le stockage ne disposait pas du contexte de propriété utilisateur.[1]

Étape 5

La réponse franchit la frontière entre utilisateurs

L’appelant pouvait recevoir un fichier de flux appartenant à un autre utilisateur. Les listes globales pouvaient révéler des identifiants, sans être nécessaires à l’absence d’autorisation.[1], [2]

Code Source : Vulnérable vs Sécurisé

Ces extraits Python ciblent le parcours documenté ; ils ne constituent pas une application autonome. Le code vulnérable traitait l’espace fourni par le client comme une autorité suffisante. Le correctif 1.9.1 résout d’abord le flux dans le périmètre de l’utilisateur authentifié et, pour un transport MCP de projet, du projet courant ; le stockage ne peut le lire qu’ensuite. Le correctif valide aussi les noms et impose le confinement des chemins dans la couche de stockage pour empêcher tout contournement par d’autres chemins de lecture, d’écriture ou de suppression. Placez l’autorisation dans le service qui connaît l’utilisateur et le projet : un adaptateur de stockage ne peut pas déduire la propriété. Portée de l’extrait : le gestionnaire réel autorise aussi un fichier du compartiment utilisateur uniquement si l’espace de noms correspond à l’ID du demandeur authentifié et si le serveur n’est pas limité à un projet. L’extrait omet cette branche distincte ; il ne faut pas en déduire un accès à n’importe quel espace de noms. La suite de régression amont contient test_handle_read_resource_denies_other_users_flow, qui vérifie le refus d’une lecture entre utilisateurs.[1], [3], [4], [7]

Extrait source: mcp_utils.py — extraits ciblés du code

IMPLÉMENTATION VULNÉRABLE
# Extrait du chemin de lecture concerné : les ID analysés étaient transmis directement au stockage.
content = await storage_service.get_file(
    flow_id=flow_id,
    file_name=filename,
)
PATCH SÉCURISÉ ET ROBUSTE
# Réduction fidèle à la structure source de la limite d’autorisation de la version 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,
)

Liste de Contrôle de Sécurité pour l'Ingénierie

Sources