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 계정과 최소 하나의 프로젝트 범위 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,
)

엔지니어링 및 시스템 보안 강화 체크리스트

출처