АйТи Фреш
Главная / Статьи / Безопасность
Безопасность

Почему Langflow 1.8.2 не закрыл public build endpoint и как я проверяю установленный wheel

Автор: Семёнов Евгений Сергеевич, директор ООО «АйТи-Фреш» · · ~9 мин чтения
Почему Langflow 1.8.2 не закрыл public build endpoint и как я проверяю установленный wheel

В интерфейсе уже 1.8.2, контейнер пересоздан, задача на обновление закрыта. А backend всё ещё принимает чужое описание графа с Python-кодом. Я, Семёнов Евгений Сергеевич, директор ООО «АйТи-Фреш», разбираю, почему здесь нельзя доверять одной версии. Покажу, как связать работающий процесс с установленными пакетами, проверить сам wheel и принять обновление по поведению сервиса. Техническая сверка — на 5 сентября 2026 года.

Что произошло с «исправленной» версией 1.8.2

Langflow 1.8.2 действительно оставался уязвимым к CVE-2026-33017. Это не предположение по подозрительному фрагменту кода: 26 марта 2026 года исследователи JFrog сообщили об успешном воспроизведении RCE и в установке из PyPI, и в официальном Docker-образе. Они же зафиксировали, что release notes и часть публичных карточек создавали впечатление исправления. Историческое объявление нужно смотреть в их публикации: сегодняшняя страница релиза уже может выглядеть иначе. [Проверка JFrog](https://research.jfrog.com/post/langflow-latest-version-was-not-fixed/).

В актуальном advisory для этой CVE указаны уязвимые версии до 1.8.2 включительно и исправление начиная с 1.9.0. Однако наличие исправляющего commit в репозитории, тег релиза и публикация установочного артефакта — разные события. Нельзя переносить свойства ветки main на wheel только потому, что номер версии выглядит подходящим. Именно это расхождение разработчики зафиксировали в issue №12345. [Advisory проекта](https://github.com/langflow-ai/langflow/security/advisories/GHSA-vwmf-pq79-vjvx), [сообщение об уязвимом пакете](https://github.com/langflow-ai/langflow/issues/12345).

Есть ещё смысловая ловушка. Исправить public build endpoint не означает удалить его или потребовать пароль у каждого посетителя. Публичный запуск нужен Shareable Playground. Исправление должно закрывать возможность подменить исполняемое описание графа. Поэтому я разделяю два требования: разрешён ли анонимный запуск утверждённого сценария и может ли посетитель принести собственный код. Это разные проверки, даже если обе обращаются к одному URL.

Обновление до 1.8.2 не закрывало эту RCE. Но и ответ HTTP 200 от public endpoint сам по себе не доказывает, что исправление отсутствует.
Цифры и версии: Что произошло с «исправленной» версией 1.8.2 — схема
Цифры и версии: Что произошло с «исправленной» версией 1.8.2

Где входные данные превращались в исполняемый Python

Проблемный маршрут — POST /api/v1/build_public_tmp/{flow_id}/flow. В уязвимой реализации обработчик принимал необязательный data и передавал его дальше в сборку графа. Вместо сохранённой сервером схемы использовалось описание из запроса. Код компонента доходил до механизма создания Python-класса и exec без отдельной песочницы. Выполнение могло начаться уже при построении графа, поэтому ошибка последующего запуска не доказывает, что код не исполнялся. [Техническое описание CVE-2026-33017](https://github.com/langflow-ai/langflow/security/advisories/GHSA-vwmf-pq79-vjvx).

Для описанного сценария нужен существующий публичный flow и его идентификатор. Cookie client_id используется для отслеживания посетителя, а не подтверждает его полномочия. Отсюда два практических вывода. Случайный UUID с ответом 404 — плохой тест. А отсутствие опубликованных flows действительно уменьшает доступную поверхность именно этой атаки: не надо объявлять каждый установленный Langflow уже взломанным. При этом публичную ссылку нельзя считать полноценным секретом доступа.

Python работает с правами серверного процесса. Если контейнер запущен не от root, это полезное ограничение, но доступные процессу API-ключи, база и сетевые подключения остаются под угрозой. Сам по себе этот дефект не означает автоматический выход на хост. Я в первую очередь проверяю доступ к секретам и внутренним сервисам; спор о том, насколько страшно слово exec, здесь ничего не исправляет.

В исправляющем изменении №12160 параметр data убрали из сигнатуры публичного обработчика, а в start_flow_build стали передавать data=None. Сохранённый граф загружается на стороне сервера. Это конкретный признак мартовского исправления, который удобно искать при аудите старой ветки. Само присутствие exec где-то в Langflow таким признаком не является: платформа поддерживает программируемые компоненты. [Diff исправления](https://github.com/langflow-ai/langflow/commit/73b6612e3ef25fdae0a752d75b0fabd47328d4f0).

LANGFLOW_AUTO_LOGIN=False не заменяет исправление публичного маршрута: отключение автологина и запрет подмены графа решают разные задачи.
Почему Langflow 1.8.2 не закрыл public build endpoint и как я проверяю установленный wheel — схема

Сначала я нахожу Python, который обслуживает сервис

Обычная ошибка при проверке — выполнить pip show langflow на хосте, хотя приложение работает в virtualenv внутри контейнера. Сначала я фиксирую контейнер и его image ID: docker inspect --format '{{.Config.Image}} {{.Image}}' langflow. Первое значение — указанное имя образа, второе — идентификатор реально использованного образа. Registry digest сохраняю отдельно через docker image inspect. Это позволяет отличить намерение развернуть версию от фактически запущенного образа. [Docker inspect](https://docs.docker.com/reference/cli/docker/inspect/).

Затем сверяю команду запуска, virtualenv, рабочий каталог и mounts. Ниже контейнер называется langflow, а интерпретатор условно расположен в /app/.venv/bin/python: подставьте путь из своего запуска. Проверка выводит метаданные трёх пакетов, реальный путь импортируемого модуля, его хеш и обработчик. Импорт выполняется в отдельном процессе и может запускать инициализацию пакета; это диагностическая команда для администрируемого вами окружения. ```bash docker exec -i langflow /app/.venv/bin/python - <<'PY' import hashlib import inspect import sys from pathlib import Path from importlib import metadata print('Python:', sys.executable) for name in ('langflow', 'langflow-base', 'lfx'): try: dist = metadata.distribution(name) except metadata.PackageNotFoundError: print(name, 'NOT INSTALLED') continue print(name, dist.version, dist.locate_file('')) from langflow.api.v1 import chat path = Path(inspect.getfile(chat)).resolve() print('Module:', path) print('File SHA256:', hashlib.sha256(path.read_bytes()).hexdigest()) print(inspect.getsource(chat.build_public_tmp)) PY ```

Почему я смотрю одновременно метаданные и путь? Distribution langflow-base поставляет код, импортируемый под именем langflow. Кроме того, исходники из bind mount, editable-установка или PYTHONPATH могут изменить выбор Python. Надпись в UI этого не показывает. importlib.metadata сообщает о найденной установленной distribution, а inspect помогает увидеть, какой модуль действительно импортировал этот диагностический процесс. [Документация importlib.metadata](https://docs.python.org/3/library/importlib.metadata.html).

Это всё ещё не снимок памяти работающего worker. Если файлы обновили поверх запущенного приложения, старый процесс мог сохранить прежние объекты. Поэтому я предпочитаю новый образ и пересоздание всех реплик. Проверку повторяю с теми же настройками запуска; отдельно убеждаюсь, что балансировщик больше не отправляет запросы старым контейнерам. Один проверенный pod не подтверждает состояние всего deployment.

pip check проверяет совместимость зависимостей. Наличие нужного security fix и фактический путь импорта он не подтверждает.
Обратите внимание: Сначала я нахожу Python, который обслуживает сервис — схема
Обратите внимание: Сначала я нахожу Python, который обслуживает сервис

Как проверить сам wheel, не запуская Langflow

Wheel — ZIP-архив. Для статического разбора не нужны ни сервер Langflow, ни модель, ни GPU. Я скачиваю конкретные артефакты без зависимостей и читаю METADATA вместе с исходником обработчика. Флаг --no-deps здесь намеренный: мы исследуем файлы, а не собираем работоспособное окружение. --only-binary=:all: исключает сборку исходного дистрибутива. [Параметры pip download](https://pip.pypa.io/en/stable/cli/pip_download/).

Команды ниже воспроизводят проверку исторической пары. Первая требует установленного pip; последующий разбор использует только стандартную библиотеку Python. Он не импортирует Langflow и не исполняет код из архива. ```bash python3 -m pip download --no-deps --only-binary=:all: \ --dest wheel-audit 'langflow==1.8.2' 'langflow-base==0.8.2' python3 - <<'PY' import ast import hashlib from pathlib import Path from zipfile import ZipFile for wheel in sorted(Path('wheel-audit').glob('*.whl')): print(wheel.name, hashlib.sha256(wheel.read_bytes()).hexdigest()) with ZipFile(wheel) as archive: for name in archive.namelist(): if name.endswith('.dist-info/METADATA'): for line in archive.read(name).decode().splitlines(): if line.startswith(('Name:', 'Version:', 'Requires-Dist: langflow-base')): print(line) route = 'langflow/api/v1/chat.py' if route in archive.namelist(): source = archive.read(route).decode() for node in ast.walk(ast.parse(source)): if isinstance(node, ast.AsyncFunctionDef): if node.name == 'build_public_tmp': print(ast.get_source_segment(source, node)) PY ```

Ключевая находка: langflow 1.8.2 требует langflow-base[complete]~=0.8.2. Это диапазон от 0.8.2 до версии ниже 0.9.0, а не жёсткое равенство. Сам base 0.8.2 требует lfx~=0.3.2. Поэтому строка langflow==1.8.2 не описывает всю установленную систему; resolver выбирает зависимости в разрешённых диапазонах. Старый lock-файл и сегодняшняя установка могут дать разные наборы. [Метаданные langflow 1.8.2](https://pypi.org/pypi/langflow/1.8.2/json), [метаданные langflow-base 0.8.2](https://pypi.org/pypi/langflow-base/0.8.2/json).

Хеш скачанного wheel я сверяю с опубликованным PyPI SHA256, затем сравниваю нужный файл из архива с файлом, путь которого показал inspect. Совпадение доказывает соответствие этих байтов выбранному артефакту, но не безопасность всего приложения. Установленного файла .whl обычно уже нет: пакет распакован. Поэтому выражение «проверить установленный wheel» на практике означает восстановить происхождение файлов и проверить их содержимое.

Не устанавливайте поверх langflow 1.8.2 произвольный новый langflow-base через --no-deps. Так легко получить неподдерживаемую смесь пакетов.

Практический разбор: стенд для условного ООО «Вектор»

Возьму учебный сценарий: производственная компания на 120 рабочих мест, два разработчика и один Shareable Playground для демонстрации внутреннего помощника. Название ООО «Вектор» и бизнес-вводные условные. Фактическая часть разбора — статический аудит опубликованных wheel на Linux x86_64 с Python 3.12.3. Уязвимый сервер для этой проверки не поднимался, клиентские данные и ключи не использовались.

Проверены семь артефактов: langflow 1.8.2, 1.9.0, 1.9.2; langflow-base 0.8.2, 0.9.0, 0.9.2; отдельно lfx 0.3.2. Последний выбран вручную как нижняя совместимая версия для старого base. Это набор для сравнения, а не утверждение, что pip сегодня автоматически установит именно такие сочетания. Размер wheel langflow 1.8.2 составил всего 6110 байт, тогда как base 0.8.2 — 15 102 978 байт: смотреть только на верхний пакет здесь особенно обманчиво.

В base 0.8.2 файл langflow/api/v1/chat.py принимает data на строке 586, а на строке 637 передаёт data=data. В base 0.9.0 публичный маршрут остаётся, но параметра уже нет в сигнатуре, а вызов содержит data=None. В base 0.9.2 этот признак также присутствует. Номера строк относятся строго к проверенным архивам. Для повторной сверки SHA256 wheel base 0.8.2: e1be40e3c959b97528935c84b03b263d0cfaa16ba4d1ca191badb9f5e198e5cc. [Исходник внутри опубликованного wheel](https://inspector.pypi.io/project/langflow-base/0.8.2/packages/29/7b/9c97bda0047d22351ecf2bfc7db7a58b8501ee6c50190961e094f045a472/langflow_base-0.8.2-py3-none-any.whl/langflow/api/v1/chat.py#line.637).

Чем закончился разбор: кандидат 1.8.2 не проходит приёмку даже на уровне артефакта. Для такого проекта я оставляю публичную демонстрацию закрытой до проверки нового релиза. Мы получили доказательство отсутствующего исправления в конкретном файле, но не измеряли сетевую эксплуатацию и не можем объявлять новые версии безопасными только по этому сравнению. Именно такой результат я считаю полезным: ограниченный, проверяемый и достаточный, чтобы не выпускать заведомо неподходящее обновление.

Это воспроизводимый аудит артефактов и учебный бизнес-сценарий, а не история о якобы взломанном клиенте.

Что я закрываю до завершения обновления

Если публичный Playground не нужен, я сначала блокирую весь его маршрут на внешнем reverse proxy. Для Nginx ниже показан фрагмент внутри нужного server. Он предполагает публикацию API без дополнительного префикса. Если снаружи сервис живёт под /langflow/, правило нужно согласовать с реальными rewrite и location. После изменения проверяю конфигурацию через nginx -t и применяю штатную перезагрузку. [Правила выбора location](https://nginx.org/en/docs/http/ngx_http_core_module.html#location). ```nginx location = /api/v1/build_public_tmp { return 403; } location ^~ /api/v1/build_public_tmp/ { return 403; } ```

Блокировка бесполезна, если backend доступен в обход proxy. Для Nginx на том же Docker-хосте я публикую порт только на loopback: в Compose это ports: ["127.0.0.1:7860:7860"]. Если proxy находится в отдельном контейнере общей сети, host-публикация обычно вообще не требуется. Проверяю внешний порт, другие ingress и прямые адреса реплик. Отключение автологина оставляю частью общей настройки доступа, но не засчитываю как исправление этой CVE. [Публикация портов Docker](https://docs.docker.com/engine/network/port-publishing/).

На уровне приложения мой выбор — свежий согласованный образ с зафиксированными зависимостями, запуск без лишних привилегий и только необходимые секреты. Если уязвимый сервис был доступен недоверенной сети, разбираю логи и доступные ему учётные данные. При признаках исполнения чужого кода одного обновления недостаточно: нужны пересоздание окружения и ротация затронутых секретов. При этом отсутствие необычного HTTP-статуса ничего не гарантирует: сборка графа и её результат могут обрабатываться асинхронно.

Временный запрет на proxy снижает доступность атаки, но не меняет уязвимый код. Проверяйте и внешний URL, и возможность обходного доступа.

По каким признакам я принимаю исправление в 2026 году

На 5 сентября 2026 года в PyPI опубликован стабильный Langflow 1.12.0 от 1 сентября. Его я выбираю кандидатом для staging, проверяя совместимость своих компонентов и актуальные advisories. Мартовский совет перейти на langflow-nightly относился к периоду без доступного стабильного исправления; повторять его сегодня без контекста не нужно. [Langflow 1.12.0 на PyPI](https://pypi.org/project/langflow/1.12.0/).

Версия 1.9.0 — историческая граница исправления конкретной CVE-2026-33017, а не универсальная безопасная точка. Позднее advisory CVE-2026-48519 для Shareable Playground указало уязвимые версии до 1.9.1 включительно и исправление в 1.9.2. В его описании есть неоднозначность относительно аутентификации, поэтому без дополнительного исследования я не называю его точной регрессией мартовской ошибки. Для выбора версии достаточно практического вывода: останавливаться на старой границе одной CVE нельзя. [Позднее advisory](https://github.com/langflow-ai/langflow/security/advisories/GHSA-v5ff-9q35-q26f).

Функциональную проверку провожу на изолированном staging с собственным публичным flow, известным UUID и без рабочих секретов. Сначала проверяю обычный запуск, затем отправляю альтернативное описание графа с безвредным уникальным маркером вместо ожидаемого результата. Готовую схему запроса беру из своей версии приложения. Если нужна проверка именно исполнения Python, маркер должен срабатывать при инициализации компонента, без shell-команд и внешних подключений. Смотрю завершение задания и наблюдаемый эффект, а не только начальный ответ HTTP.

В исходном исправлении лишний data мог молча игнорироваться, а сохранённый flow успешно выполняться. Поэтому тест «ожидаем исключительно 403» проверяет собственное предположение. Я принимаю обновление, когда происхождение артефакта подтверждено, все реплики заменены, штатный сценарий работает, а клиентское описание не становится исполняемым графом. Если публичный запуск запрещён политикой проекта, отдельно проверяю блокировку на входе. [Регрессионные тесты первоначального исправления](https://github.com/langflow-ai/langflow/pull/12160/files).

Номер версии — начало проверки. Завершает её подтверждённое поведение конкретного развёрнутого сервиса.
Цифры и версии: По каким признакам я принимаю исправление в 2026 году — схема
Цифры и версии: По каким признакам я принимаю исправление в 2026 году

Частые вопросы

Достаточно ли версии в UI или тега контейнера?

Нет. Проверьте image ID и digest, интерпретатор приложения, версии зависимостей и фактически импортируемый файл обработчика. После обновления замените все реплики.

Почему в выводе langflow 1.8.2, а langflow-base 0.8.2?

Это разные distributions с собственной нумерацией. В проверенном релизе langflow зависит от langflow-base, который содержит backend-код.

Ответ 200 означает, что public endpoint уязвим?

Нет. Исправленный обработчик может игнорировать подменённый data и выполнить сохранённый flow. Проверять нужно источник исполняемого графа и результат задания.

Проверим пакеты и публичные маршруты
Я помогу проверить установленный Langflow, происхождение пакетов и доступность публичных маршрутов. Обратитесь в rf-buh — разберём ваше развёртывание и подготовим проверяемый план обновления.
Бесплатная консультация →
© ООО «АйТи-Фреш» · Москва · Все статьи