FlawOpen

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

● CVE-2026-105699 · CVSS v4 7.1 · 高
セキュリティ研究 · FlawOpen

CVE-2026-105699:Langflow MCPのファイル漏えい

LangflowはMCP接続を認証しても、要求されたファイルごとの認可を行っていませんでした。別ユーザーのフローファイルを読める問題は1.9.1で修正されています。[1], [2], [5]

CVSS ベクトル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
💡 わかりやすい解説 (ELI5)

ホテルが鍵を確認した後、荷札を伝えるだけでどの部屋の荷物でも渡す状況を想像してください。鍵はホテルに入れる証明であり、他の宿泊客の荷物の所有者である証明ではありません。LangflowのMCP読み取りは、ファイルを開く前に所有者とプロジェクトを確認する必要があります。[1]

主要な概念と専門用語

Model Context Protocol(MCP)
AIアプリケーションがサーバーにツールやリソースを要求するためのプロトコルです。サーバーは要求された各オブジェクトを個別に認可する必要があります。[1]
プロジェクト限定MCPエンドポイント
特定のプロジェクトに関連付けられたLangflowのMCP接続です。接続できても、すべてのプロジェクトのファイルにアクセスできるわけではありません。[1]
resources/read
URIで指定されたリソースをサーバーに返すよう求めるMCP操作です。[1]
オブジェクト単位の認可
認証済みユーザーがエンドポイントだけでなく、要求した個別のレコードやファイルへアクセスできるかを確認することです。[1], [6]
CWE-639
ユーザーが制御するキーによる認可回避です。識別子を変更すると、別ユーザーのレコードに到達できます。[6], [2]

フレームワーク対応

  • このガイドで説明する弱点の CWE 参照: CWE-639

根本原因の分析 (Root Cause)

影響を受けるLangflow 1.6.8~1.9.0では、プロジェクト限定MCPトランスポートが接続URL内のプロジェクトIDへのアクセスを認証していました。その後、resources/readハンドラーはリソースURIを受け取り、フローIDとファイル名を解析してstorage_service.get_file()へ渡しましたが、そのフローが認証済みユーザーまたは現在のプロジェクトに属するかを確認していませんでした。ストレージ層は名前空間とファイル名で読み取るだけで、Langflowのユーザーやプロジェクトの権限を知りません。そのため、自分のプロジェクトMCPエンドポイントへアクセスできるアカウントユーザーは、別ユーザーのフロー名前空間にあるファイルを要求できました。別途報告されたグローバルなresources/listとtools/listの挙動は識別子を漏らして標的選択を容易にする可能性がありますが、読み取り自体には不要です。これは認証後に発生するユーザー間の機密性侵害であり、匿名アクセスやコード実行ではありません。CVEレコードはCVSS 4.0 7.1 High、CWE-639としています。Langflow 1.9.1ではhandle_read_resource()が認証済みユーザーを取得し、Flow.user_id == current_user.idでフローを検索します。プロジェクト限定トランスポートではさらにFlow.folder_id == project_idを必須にし、未認可の名前空間をストレージアクセス前に拒否します。パッチはパス区切り文字とtraversal文字列も拒否し、読み取り・書き込み・削除のストレージ経路でパス境界を確認し、グローバル一覧を現在のユーザーに限定します。メンテナーの回帰テストは他ユーザーのフローの読み取りを拒否します。[1], [2], [3], [5]

ステップ・バイ・ステップの攻撃フロー

ステップ 1

ユーザーが自分のプロジェクトMCPエンドポイントに認証する

攻撃者にはLangflowアカウントと、少なくとも1つのプロジェクト限定MCP接続へのアクセスが必要です。この問題はログインを回避しません。[1]

ステップ 2

クライアントがリソースURIを要求する

MCPのresources/read要求には、末尾のパス要素がフロー名前空間とファイルを示すURIが含まれます。[1]

ステップ 3

脆弱なハンドラーが識別子を信頼する

handle_read_resource()はIDとファイル名を解析しましたが、フローを呼び出し元やエンドポイントのプロジェクトに結び付けませんでした。[1], [3]

ステップ 4

ストレージが名前空間で読み取る

storage_service.get_file()は指定された名前空間のファイルを返しました。ストレージにはユーザー所有権のコンテキストがありません。[1]

ステップ 5

応答がユーザー境界を越える

呼び出し元は別ユーザーのフローファイルを受け取る可能性がありました。グローバル一覧は識別子を漏らし得ますが、認可欠落の悪用に必須ではありません。[1], [2]

ソースコード比較:脆弱 vs 堅牢化

これは報告された処理経路のPython抜粋であり、単独で動作するアプリケーションではありません。脆弱な処理はクライアントが指定した名前空間を十分な権限として扱っていました。1.9.1の修正では、まず認証済みユーザー、さらにプロジェクトMCP接続では現在のプロジェクトの範囲でフローを解決し、その後にストレージから読み取ります。パッチはファイル名も検証し、ストレージ層でパス境界を強制するため、他の読み取り・書き込み・削除経路から回避できません。ユーザーとプロジェクトを把握するサービス層で認可してください。ストレージアダプターだけでは所有者を判断できません。 抜粋の範囲:実際のハンドラーは、名前空間が認証済みユーザー本人のIDと一致し、サーバーがプロジェクトスコープでない場合に限り、ユーザーバケット内のファイルも許可します。この別分岐は抜粋では省略しており、任意の名前空間へのアクセスを意味しません。上流の回帰テストには、ユーザー間読み取りの拒否を検証する test_handle_read_resource_denies_other_users_flow が含まれます。[1], [3], [4], [7]

ソース抜粋: mcp_utils.py — 対象ソースの抜粋

脆弱な実装
# 影響を受けた読み取り経路の抜粋: 解析済み ID がそのままストレージに渡されていた。
content = await storage_service.get_file(
    flow_id=flow_id,
    file_name=filename,
)
堅牢化されたセキュアパッチ
# 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,
)

エンジニアリング&システム堅牢化チェックリスト

参考資料