FlawOpen

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

● CVE-2026-105699 · CVSS v4 7.1 · High
FlawOpen Security Research

CVE-2026-105699: Langflow MCP File Disclosure

Langflow authenticated the MCP connection but did not authorize each requested file. A user could read another user’s flow files; 1.9.1 fixes the ownership check.[1], [2], [5]

CVSS vectorCVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N
💡 Plain English Explainer (ELI5)

A hotel checks that you have a room key, then lets you ask for any room’s luggage by naming its tag. The key proves you may enter the hotel, not that another guest’s bag belongs to you. Langflow’s MCP read must check the file’s owner and project before opening it.[1]

Core Concepts & Subsystem Terms

Model Context Protocol (MCP)
A protocol that lets an AI application request tools and resources from a server. The server still has to authorize every requested object.[1]
Project-scoped MCP endpoint
A Langflow MCP connection associated with one project. Access to that endpoint does not grant access to files in every project.[1]
resources/read
An MCP operation that asks the server to return a resource identified by a URI.[1]
Object-level authorization
A check that the authenticated user may access the specific record or file named in a request, not merely the endpoint receiving it.[1], [6]
CWE-639
Authorization bypass through a user-controlled key: changing an identifier lets one user reach another user’s record.[6], [2]

Framework mappings

  • CWE reference for the weakness described in this guide: CWE-639

Root Cause Analysis

In affected Langflow versions 1.6.8 through 1.9.0, the project-scoped MCP transport authenticated access to the project ID in its connection URL. Its resources/read handler then accepted a resource URI, parsed a flow ID and filename, and passed those identifiers to storage_service.get_file() without checking that the flow belonged to the authenticated user or the current project. The storage layer reads by namespace and filename; it does not know Langflow’s user or project permissions. Therefore, a user with an account and access to their own project MCP endpoint could request a file in another user’s flow namespace. The separately documented global resources/list and tools/list behavior could expose flow identifiers and make targeting easier, but enumeration was not required for the read. This is an authenticated cross-user confidentiality issue, not unauthenticated access or code execution. The CVE record assigns CVSS 4.0 7.1 High (AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N) and CWE-639. Langflow 1.9.1 fixes the path: handle_read_resource() obtains the authenticated user, resolves the flow with both Flow.user_id == current_user.id and, for a project transport, Flow.folder_id == project_id, and rejects an unauthorized namespace before storage access. The patch also rejects path separators and traversal sequences, adds containment checks across storage read/write/delete paths, and scopes global resource/tool listings to the caller. The maintainer’s regression test denies a read of another user’s flow.[1], [2], [3], [5]

Step-by-Step Attack Flow

Step 1

A user authenticates to their own project MCP endpoint

The attacker needs a Langflow account and access to at least one project-scoped MCP connection. The issue does not bypass login.[1]

Step 2

The client requests a resource URI

The MCP resources/read request carries a URI whose final path components identify a flow namespace and filename.[1]

Step 3

The vulnerable handler trusts the identifiers

handle_read_resource() parsed the supplied flow ID and filename but did not bind that flow to the caller or the endpoint’s project.[1], [3]

Step 4

The storage service reads by namespace

storage_service.get_file() returned the file for the supplied namespace. Storage had no user-ownership context to enforce.[1]

Step 5

The response crosses the user boundary

The caller could receive another user’s flow-backed file. Global resource and tool listings could reveal identifiers, but were not required for the missing authorization check.[1], [2]

Source Code: Flaw vs. Secure Implementation

These are focused Python excerpts from the documented code path, not a standalone application. The affected path treated a caller-supplied namespace as sufficient authority. The 1.9.1 fix first resolves the flow under the authenticated owner and, on project-scoped transports, the current project; only then may storage read it. The upstream patch also validates filenames and enforces path containment in the storage layer, so alternate read/write/delete call sites cannot bypass the boundary. Keep authorization in the service path that has the user and project context; a storage adapter alone cannot infer ownership. Scope of the excerpt: the real handler also permits a user-bucket file only when the namespace equals the authenticated caller’s own user ID and the server is not project-scoped. The snippet omits that separate branch; it must not be generalized into arbitrary namespace access. The upstream regression suite includes test_handle_read_resource_denies_other_users_flow, which asserts that the cross-user read is denied.[1], [3], [4], [7]

Source excerpt: mcp_utils.py — focused source excerpts

UNPATCHED FLAW
# Excerpt of the affected read path: parsed IDs went directly to storage.
content = await storage_service.get_file(
    flow_id=flow_id,
    file_name=filename,
)
HARDENED SECURE PATCH
# Source-shaped reduction of the 1.9.1 authorization boundary.
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,
)

Engineering & System Hardening Checklist

Sources