FlawOpen

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

● CVE-2026-105699 · CVSS v4 7.1 · Высокий
Исследования · FlawOpen

CVE-2026-105699: файлы MCP в Langflow

Langflow аутентифицировал подключение MCP, но не проверял права на каждый запрошенный файл. Пользователь мог читать файлы чужих потоков; проверка владельца исправлена в версии 1.9.1.[1], [2], [5]

Вектор CVSSCVSS: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)

Представьте отель, который проверяет ваш ключ, а затем выдаёт багаж из любого номера, если назвать бирку. Ключ доказывает право войти в отель, но не то, что чемодан другого гостя принадлежит вам. Перед открытием файла чтение MCP в Langflow должно проверить владельца и проект.[1]

Ключевые понятия и термины

Model Context Protocol (MCP)
Протокол, с помощью которого приложение ИИ запрашивает инструменты и ресурсы у сервера. Сервер по-прежнему должен проверять права на каждый объект.[1]
MCP-конечная точка проекта
Подключение Langflow MCP, связанное с проектом. Доступ к этой конечной точке не открывает файлы всех проектов.[1]
resources/read
Операция MCP, запрашивающая у сервера ресурс, указанный URI.[1]
Авторизация на уровне объекта
Проверка, имеет ли аутентифицированный пользователь доступ к конкретной записи или файлу, а не только к конечной точке.[1], [6]
CWE-639
Обход авторизации через ключ под контролем пользователя: изменение идентификатора позволяет обратиться к записи другого пользователя.[6], [2]

Связи со стандартами

  • Ссылка CWE для слабого места, описанного в этом руководстве: CWE-639

Анализ первопричины

В затронутых версиях Langflow с 1.6.8 по 1.9.0 транспорт MCP, привязанный к проекту, аутентифицировал доступ к ID проекта из URL подключения. Затем обработчик 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 текущего пользователя и сервер не ограничен проектом. Этот отдельный путь не показан; фрагмент нельзя трактовать как разрешение доступа к произвольным пространствам имён. В upstream-наборе регрессионных тестов есть 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,
)

Чек-лист по защите системы для инженеров

Источники