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

Чужой flow по UUID в Langflow: как закрыть дыру и понять, чьи ключи уже сработали

Автор: Семёнов Евгений Сергеевич, директор ООО «АйТи-Фреш» · · ~27 мин чтения
Чужой flow по UUID в Langflow: как закрыть дыру и понять, чьи ключи уже сработали
Иллюстрация к статье «Чужой flow по UUID в Langflow: как закрыть дыру и понять, чьи ключи уже сработали».

Если у вас Langflow, в котором работает больше одного человека, и вы считаете, что логин с паролем — это и есть разграничение доступа, у меня плохие новости. Аутентификация в Langflow работает, а вот проверки владельца объекта долгое время не было на всех путях: до версии 1.9.1 авторизованный пользователь мог выполнить чужой флоу через эндпоинт `/api/v1/responses`, просто подставив его UUID. И вместе с флоу выполнялись чужие credentials — SMTP, ключи LLM, токены CRM. Ниже — что именно ломалось, какие ещё CVE закрывать заодно (включая CVE-2025-3248, из-за которой Langflow массово ломали из интернета), в каких версиях это починили, как выставить LANGFLOW_AUTO_LOGIN и суперпользователя, как закрыть инстанс снаружи и как по базе восстановить, чьи ключи уже успели сработать.

«Логин прошёл — значит можно всё»: где Langflow ломается

Классическая ошибка low-code платформ: разработчики честно закрывают периметр аутентификацией и на этом останавливаются. Дальше в коде идёт session.get(Flow, flow_id) — достать объект по первичному ключу. Владельца никто не спрашивает. Это BOLA, она же IDOR: Broken Object Level Authorization. В Langflow она жила в функции get_flow_by_id_or_endpoint_name в src/backend/base/langflow/helpers/flow.py и была занятна тем, что проверка владельца там всё-таки была — но только на одной из двух веток.

Логика была такая. Если флоу ищут по человекочитаемому endpoint_name, к запросу добавляется условие Flow.user_id == current_user.id — всё правильно. А если тот же флоу запрашивают по UUID, код уходит в другую ветку и просто берёт объект из базы. Никаких условий. Получается, что безопасность объекта зависела от того, каким именно идентификатором вы его назвали. Это и есть та самая «непоследовательная реализация», которую потом зафиксировали в advisory.

Оформлено это как GHSA-qrpv-q767-xqq2 / CVE-2026-55255, опубликовано 19 июня 2026 года, CVSS 9.9 по вектору CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:L. В advisory уязвимым назван эндпоинт /api/v1/responses — OpenAI-совместимый API Langflow, где в поле model передаётся ID или имя флоу. Обратите внимание на PR:L — нужны привилегии обычного пользователя, не больше. И на S:C — смена области, потому что выполнение чужого флоу выходит за границы своей учётки. Затронуты все версии до 1.9.1, починено в 1.9.1 (PR #12832). Фикс сверяет flow.user_id с текущим пользователем на обеих ветках поиска, а для чужого флоу отдаёт 404, а не 403 — чтобы по коду ответа нельзя было подтвердить существование UUID.

Сегодня в main сигнатура этой функции выглядит так, и стоит понимать, что именно там появилось:

async def get_flow_by_id_or_endpoint_name(
    flow_id_or_name: str,
    user_id: str | UUID | None = None,
    *,
    widen_for_shares: bool = False,
) -> FlowRead:

Появился явный keyword-only флаг widen_for_shares. Это опт-ин на «достать чужой флоу» для сценариев расшаривания. То есть небезопасное поведение из дефолтного стало явным и требующим осознанного включения. Ровно так и надо было делать сразу.

Если у вас Langflow ниже 1.9.1 и в нём больше одного пользователя — считайте, что разграничения доступа между ними нет вообще. Не «есть, но с оговорками». Нет.
Цифры и версии: «Логин прошёл — значит можно всё»: где Langflow ломается — схема
Цифры и версии: «Логин прошёл — значит можно всё»: где Langflow ломается. Открыть схему в полном размере

Разбор: языковая школа «Речь без границ», чат-бот записи и 11 чужих флоу

Кейс, который я разбирал руками: языковая школа «Речь без границ», 19 рабочих мест — администраторы, методисты, преподаватели и небольшой отдел продаж. Год назад маркетолог поднял Langflow 1.8.2 в Docker «на попробовать», чтобы сделать чат-бота записи учеников: бот на сайте и в мессенджере отвечает на вопросы о курсах, подбирает группу по уровню и отправляет заявку в CRM, а родителю — письмо с подтверждением. Прижилось. Рядом с Langflow работал PostgreSQL 16 в отдельном контейнере, спереди nginx с TLS. К моменту разбора в Langflow было 5 учёток (маркетолог, два администратора, старший методист и директор), 18 флоу, LANGFLOW_AUTO_LOGIN=false, у каждого свой пароль, у троих — персональные API-ключи для вызова флоу из виджета сайта и из сервиса автоматизации.

Поводом стал не аудит, а бытовая жалоба: в ящике рассылки школы нашлись письма родителям с «уточнением расписания», которых никто не отправлял. Логи SMTP показывали отправку из Langflow. Дальше — простая проверка. Я взял учётку методиста, её персональный API-ключ и попробовал вызвать флоу маркетолога через OpenAI-совместимый эндпоинт. UUID флоу я взял не «хакерски» — он был в ссылке, которую маркетолог сам кинул в общий рабочий чат месяцем раньше.

curl -sS -o /dev/null -w '%{http_code}\n' \
  -X POST "https://lf.school.example/api/v1/responses" \
  -H "x-api-key: $METHODIST_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"model":"<uuid-чужого-флоу>","input":"test","stream":false}'

Ответ — 200. Флоу отработал. Внутри него был компонент отправки почты, а SMTP-логин и пароль лежали в глобальной переменной типа Credential, принадлежащей маркетологу.

Дальше я снял список всех флоу из базы и прогнал по нему тот же вызов с ключом методиста. Из 18 флоу 11 принадлежали другим людям. Все 11 запустились. Не «отдали метаданные» — именно выполнились: сходили в LLM-провайдера по чужому ключу, дёрнули вебхук CRM с заявкой, один дописал строку в таблицу расписания. Тестовые прогоны я делал только по флоу с безопасными выходами, но картина была очевидна по всем. Хуже того, флоу записи учеников хранил в истории сообщений имена и телефоны родителей — а это персональные данные, и отдельный разговор про их утечку.

Чем закончилось. За выходные подняли рядом инстанс на актуальной версии, перенесли базу, проверили флоу, переключили upstream в nginx. Все глобальные переменные типа Credential пересоздали заново — 9 штук, включая 2 ключа LLM-провайдеров, SMTP-пару и токен CRM. Старые отозвали у провайдеров. API-ключи Langflow пересоздали все три. Публичного чат-бота вынесли в отдельный инстанс, а внутренние флоу методистов оставили во втором, закрытом от интернета, — про это ниже. Общее время работ — около 7 часов, из них почти половина ушла на перевыпуск и перепроверку credentials, а не на сам апгрейд.

UUID — это не секрет. Он попадает в ссылки, в скриншоты, в переписку, в логи прокси и в экспортированные JSON флоу. Любая схема безопасности, где знание идентификатора эквивалентно праву на действие, обречена.
Чужой flow по UUID в Langflow: как закрыть дыру и понять, чьи ключи уже сработали — схема
Схема к статье. Открыть схему в полном размере

Почему это опаснее обычного «прочитал чужое»

Обычный IDOR — это утечка чтения. Здесь другое: вы не читаете чужой секрет, вы заставляете сервер применить чужой секрет от своего имени. В литературе это называется confused deputy — «сбитый с толку заместитель». Пользователь никогда не увидит значение SMTP-пароля маркетолога: оно лежит в таблице variable в зашифрованном виде и расшифровывается на сервере в момент исполнения. Но письмо уйдёт. Счёт за токены LLM выставится. Запись в CRM появится. С точки зрения любого внешнего наблюдателя действие совершил владелец credential.

Это же ломает и разбор инцидента. Когда служба безопасности спрашивает «кто отправил письмо» — все технические следы указывают на маркетолога. Ни в SMTP-логе, ни в API-логе провайдера LLM нет ни следа настоящего инициатора. Атрибуция восстанавливается только на уровне самого Langflow и обратного прокси, и по умолчанию она там неполная. Об этом — отдельный раздел.

Теперь про сами секреты. Есть отдельная критическая история: GHSA-jxw3-mjmx-3pqm (CVE не присвоен), версии по 1.10.0 включительно, CVSS 9.1. Langflow выводил ключ Fernet через random.seed() — то есть через Mersenne Twister, который вообще не криптографический PRNG. Если ваш LANGFLOW_SECRET_KEY был короче 32 символов, производный ключ шифрования полностью детерминирован и воспроизводим любым, кто знает исходное значение. А если длиннее — сам секрет использовался напрямую как ключ, то есть утечки файла secret_key (в Docker это обычно /app/data/.cache/langflow/secret_key) достаточно для офлайн-расшифровки всех сохранённых credentials. Починено в 1.10.1: для коротких ключей перешли на деривацию через SHA-256, а старые данные читаются через MultiFernet.

Третий слой — история переписки. GHSA-9c59-2mvc-vfr8 / CVE-2026-33760, CVSS 8.8, версии по 1.7.3, починено в 1.9.0. Семь эндпоинтов роутера /api/v1/monitor принимали идентификаторы ресурсов без джойна к таблице flow и без проверки Flow.user_id == current_user.id. Это чтение чужих билдов и чужих транзакций (то есть буквально промптов и ответов LLM по чужому flow_id), перезапись любого сообщения по UUID, переименование и массовое удаление чужих сессий. Комбинация «выполнить чужой флоу» + «прочитать его транзакции» даёт полный контроль над чужим рабочим контуром.

И четвёртый слой — удалённое исполнение кода, из-за которого инстансы, торчащие в интернет, ломают без всякой учётки. CVE-2025-3248: эндпоинт /api/v1/validate/code в версиях до 1.3.0 выполнял присланный Python-код без аутентификации, CVSS 9.8 (AV:N/AC:L/PR:N/UI:N), 5 мая 2025 года CISA внесла её в каталог активно эксплуатируемых уязвимостей (KEV). Её закрыли, добавив аутентификацию, но через год появилась CVE-2026-33017 (GHSA-vwmf-pq79-vjvx, CVSS 9.3): публичный POST /api/v1/build_public_tmp/{flow_id}/flow принимал данные флоу с кодом узлов, и для атаки хватало UUID публичного флоу — ровно того, что встраивается в виджет чат-бота на сайте. Затронуты версии по 1.8.2, исправлено в 1.9.0. Для школы с публичным ботом записи это самый прямой путь к полной компрометации сервера.

«Ключ же не показали, значит не утёк» — самое опасное успокоение в этой истории. Если чужой флоу выполнился, считайте использованные им credentials скомпрометированными и меняйте их. Стоимость ротации API-ключа LLM — десять минут. Стоимость ошибки — счёт и разбирательство.
Цифры и версии: Почему это опаснее обычного «прочитал чужое» — схема
Цифры и версии: Почему это опаснее обычного «прочитал чужое». Открыть схему в полном размере

Что чинить в первую очередь: версии и переменные окружения

Порядок обновления определяется тем, какие дыры вы закрываете. 1.3.0 — давно пройденный минимум против CVE-2025-3248. 1.9.0 закрывает monitor-IDOR и неаутентифицированный RCE через публичный build-эндпоинт. 1.9.1 закрывает запуск чужого флоу по UUID. 1.10.1 чинит деривацию Fernet-ключа. Дальше ветка 1.11.x шла с пачками security-исправлений (последний бэкпорт — 1.11.6 от 1 сентября 2026 года), а актуальный релиз на момент написания — 1.12.1 от 8 сентября 2026 года. Ниже 1.9.1 в проде с несколькими людьми жить нельзя, ниже 1.10.1 — нельзя хранить боевые credentials; целевая версия — актуальная ветка.

Скажу честно, где апгрейд не бесплатный: в 1.12.0 компоненты разнесли по бандлам и убрали OSS Admin Page. То есть после обновления часть флоу может потребовать доустановки бандлов, а привычная страница администрирования пользователей исчезнет — проверьте заранее, как вы будете заводить и блокировать учётки. Поэтому я обычно делаю не in-place апгрейд, а параллельный инстанс на новой версии с копией базы, прогоняю по нему флоу и только потом переключаю upstream в nginx. Откат при этом занимает одну строку конфига.

Дальше — переменные окружения. Это то место, где чаще всего сами себе стреляют в ногу. Мой рабочий набор для стенда, где работает больше одного человека:

LANGFLOW_AUTO_LOGIN=false
LANGFLOW_SUPERUSER=lf-admin
LANGFLOW_SUPERUSER_PASSWORD=<из парольного менеджера>
LANGFLOW_SECRET_KEY=<случайные 64 символа, не короче 32>
LANGFLOW_ENABLE_SIGNUP=false
LANGFLOW_NEW_USER_IS_ACTIVE=false
LANGFLOW_ENABLE_SUPERUSER_CLI=false
LANGFLOW_API_KEY_SOURCE=db
LANGFLOW_CORS_ORIGINS=https://www.school.example
LANGFLOW_HOST=127.0.0.1
LANGFLOW_DATABASE_URL=postgresql://lf_public:<пароль>@pg/lf_public

Разберу по пунктам, что здесь важно. LANGFLOW_AUTO_LOGIN по умолчанию True: каждый, кто открыл интерфейс, автоматически входит суперпользователем — для инстанса, доступного хоть кому-то кроме вас, это недопустимо. При false обязателен LANGFLOW_SUPERUSER_PASSWORD (LANGFLOW_SUPERUSER по умолчанию langflow, имя лучше сменить). LANGFLOW_ENABLE_SIGNUP по умолчанию True, то есть кто угодно может создать учётку через эндпоинт регистрации; при выключенном auto-login это ровно та дыра, через которую посторонний получает «валидную учётку» из вектора PR:L. LANGFLOW_NEW_USER_IS_ACTIVE по умолчанию False — новые учётки ждут активации суперпользователем, это правильно. LANGFLOW_ENABLE_SUPERUSER_CLI по умолчанию true, и документация сама рекомендует выключать его, чтобы никто не наплодил суперпользователей командой langflow superuser. LANGFLOW_CORS_ORIGINS по умолчанию * — ограничьте доменом сайта. LANGFLOW_SECRET_KEY без явного значения генерируется автоматически и лежит в файле рядом с данными; задайте свой и храните в менеджере секретов.

И отдельно про LANGFLOW_SKIP_AUTH_AUTO_LOGIN. Эта переменная работает только вместе с LANGFLOW_AUTO_LOGIN=true и дополнительно отключает проверку API-ключа для запросов к API. На ноутбуке разработчика это удобно, на сервере это означает, что любой, кто достучался до порта, вызывает любой флоу и любой служебный эндпоинт. Проверить, что реально действует в контейнере, а не в compose-файле, проще всего так:

docker exec langflow env | grep -E '^LANGFLOW_(AUTO_LOGIN|SKIP_AUTH|ENABLE_SIGNUP|API_KEY_SOURCE|SUPERUSER=|CORS|HOST|DISABLE_TRACK)'
Никогда не ставьте `LANGFLOW_API_KEY_SOURCE=env` на стенд с несколькими пользователями. В этом режиме все запросы проверяются по одному ключу из переменной окружения, и успешная аутентификация даёт права суперпользователя. Это не «упрощение», это выключение модели доступа целиком. Только `db` (значение по умолчанию).

Чьи ключи уже сработали: форензика по базе Langflow

Хорошая новость: сама платформа считает использование ключей. Таблица ключей (SQLModel-класс ApiKey; имя таблицы в разных сборках проверьте командой \dt в psql — у меня это была apikey) содержит id, name, api_key, api_key_hash, user_id, created_at, last_used_at, total_uses, is_active, expires_at. Первое, что я делаю на подозрительном стенде — снимаю срез по ключам:

SELECT k.name, u.username, k.total_uses, k.last_used_at,
       k.is_active, k.expires_at, k.created_at
FROM apikey k JOIN "user" u ON u.id = k.user_id
ORDER BY k.last_used_at DESC NULLS LAST;

Здесь сразу видно забытые ключи с давним created_at и свежим last_used_at, ключи без срока годности и ключи людей, которые уже не работают в школе.

Важная оговорка, из-за которой я видел неверные выводы: счётчик можно отключить. Переменная LANGFLOW_DISABLE_TRACK_APIKEY_USAGE=True существует специально, чтобы не создавать конкуренцию за запись в базу при высокой нагрузке. Если её кто-то включил «для производительности», то total_uses и last_used_at у вас просто не заполняются, и нулевой счётчик не означает неиспользованный ключ. Проверьте окружение контейнера перед тем, как строить выводы.

Второй источник — таблица transaction (у неё явно задан __tablename__ = "transaction"), поля: id, timestamp, vertex_id, target_id, inputs, outputs, status, error, flow_id. Это фактические исполнения узлов. Сопоставляем их с владельцами флоу и ищем аномалии по времени:

SELECT f.name AS flow, u.username AS owner,
       date_trunc('hour', t.timestamp) AS h, count(*) AS runs
FROM transaction t
JOIN flow f ON f.id = t.flow_id
LEFT JOIN "user" u ON u.id = f.user_id
WHERE t.timestamp > now() - interval '90 days'
GROUP BY 1,2,3
ORDER BY h DESC;

И вот главный неприятный вывод, который надо принять сразу. В таблице transaction нет поля инициатора — только flow_id. То есть база честно скажет вам, что чужой флоу выполнялся в 03:12 ночи в субботу, но не скажет, чьим ключом. Полная атрибуция «пользователь → вызов» в Langflow по умолчанию отсутствует, и восстанавливать её приходится корреляцией: всплеск запусков чужого флоу в таблице transaction сопоставляем с last_used_at из таблицы ключей и с журналом обратного прокси. Прокси при этом надо научить логировать не сам ключ, а его отпечаток.

В nginx штатного SHA-256 нет, поэтому отпечаток ключа я считаю модулем njs — ключ в открытом виде в лог не попадает никогда, а по отпечатку его можно связать с конкретной записью в базе, посчитав тот же хэш по действующим ключам:

# /etc/nginx/njs/keyfp.js:
#   function fp(r) {
#     const k = r.headersIn['X-Api-Key'];
#     if (!k) return '-';
#     return require('crypto').createHash('sha256').update(k).digest('hex').slice(0, 16);
#   }
#   export default { fp };

js_import keyfp from /etc/nginx/njs/keyfp.js;
js_set $keyfp keyfp.fp;

log_format lfapi '$remote_addr $time_iso8601 "$request_method $uri" $status '
                 'kfp=$keyfp rt=$request_time';

server {
    # ...
    access_log /var/log/nginx/langflow-api.log lfapi;
}

Обратите внимание: в формате лога стоит $uri, а не $request — строка запроса в журнал не пишется. Это важно, потому что документация Langflow допускает передачу ключа не только заголовком, но и query-параметром ?x-api-key=..., и через $request он утёк бы в лог целиком. Это отдельный повод резать такие запросы на прокси.

Не логируйте `x-api-key` целиком и не пишите в журнал `$request` с query-строкой. Явно запретите на прокси передачу ключа query-параметром: он попадает в access_log, в Referer и в историю браузера. И помните про ПДн: `inputs`/`outputs` в `transaction` у бота записи содержат телефоны родителей — выгрузки для разбора храните как персональные данные.

Изоляция и закрытие снаружи без надежд на вендора

Мой основной вывод после «Речи без границ» звучит непопулярно: не стройте разграничение доступа внутри одного инстанса Langflow. Платформа быстро развивается, модель доступа в ней переписывается прямо сейчас, и за последние полгода в ней нашли и запуск чужого флоу, и чтение чужих транзакций, и неаутентифицированный RCE. Дешевле и надёжнее развести границы доверия наружу: один контур — один инстанс, своя база (или как минимум своя БД и свой пользователь Postgres), свой LANGFLOW_SECRET_KEY, своя docker-сеть. Для школы на 19 мест это два инстанса, а не десять: публичный бот записи с минимумом credentials и внутренний контур методистов, недоступный из интернета. Тогда IDOR или RCE в публичном инстансе физически не дотянется до внутренних секретов — их там просто нет.

Второй контур — сеть. Сам Langflow я никогда не публикую портом наружу: контейнер слушает только 127.0.0.1 или внутреннюю docker-сеть, а в интернет смотрит nginx. Редактор флоу и логин снаружи не нужны вообще — сотрудники ходят в него через VPN, а из интернета доступен только маршрут, который вызывает виджет чат-бота. Проверка, что порт действительно не торчит, делается с внешней машины:

ss -ltnp | grep 7860          # на сервере: должен быть 127.0.0.1:7860
nmap -Pn -p 7860,443 203.0.113.10   # снаружи: 7860 filtered/closed

И сеть на выход. Флоу по определению ходит наружу: LLM-провайдер, вебхуки, SMTP, CRM. Успешный запуск чужого флоу превращается в исходящее действие, поэтому ограничивать надо именно egress: трафик Langflow выпускаю только через прокси с белым списком доменов и запрещаю контейнеру прямой выход в интернет. В 1.12.0 появился опциональный (opt-in) бэкенд exec-sandbox на microVM для компонентов, исполняющих код, но полагаться только на него я бы не стал.

Третий контур — обратный прокси перед API. Наружу нужны буквально один-два маршрута, а не весь API. Служебные и исторически опасные эндпоинты закрываю жёстко:

location /api/v1/validate/          { return 404; }
location /api/v1/build_public_tmp/  { return 404; }
location /api/v1/monitor/           { return 404; }
location /api/v1/users/             { allow 10.8.0.0/24; deny all; }  # только VPN

location /api/ {
    if ($args ~* "(^|&)x-api-key=") { return 400; }
    allow 10.8.0.0/24;
    deny all;
    proxy_pass http://langflow_upstream;
}

location = /api/v1/run/<uuid-бота-записи> {
    if ($args ~* "(^|&)x-api-key=") { return 400; }
    limit_req zone=chatbot burst=10 nodelay;
    proxy_pass http://langflow_upstream;
}

Обратите внимание на проверку query-параметра: переменная вида $arg_x-api-key в nginx не работает, потому что дефис в имени переменной не допускается, поэтому ловим параметр регулярным выражением по $args. Если виджет бота встраивается на сайт, а не ходит через свой бэкенд, ключ в браузер не отдавайте: вызов проксируйте с сервера сайта, который сам добавляет x-api-key. Всё это не заменяет апгрейд — это страховка на период, пока вы его планируете, и на следующую дыру, о которой ещё никто не знает.

Про будущее скажу осторожно, потому что здесь единого положения дел пока нет. В актуальной ветке в таблице flow уже появилось поле workspace_id, а релиз 1.12.0 в описании упоминает «RBAC authorization foundations» — то есть фундамент под ролевую модель закладывается. Но публичной документации по ролям и рабочим пространствам на момент написания я не нашёл: раздел про аутентификацию и API-ключи о ролях молчит, release notes тоже. Поэтому планировать разграничение доступа «вот выйдет RBAC и всё решится» я не советую. Стройте изоляцию инстансами сейчас, а RBAC примете как бонус, когда он доедет и обрастёт документацией.

Разнесение по инстансам стоит памяти, и это честный минус. Но два контейнера по 2 ГБ дешевле одной утечки ключей CRM и телефонов родителей. У «Речи без границ» второй инстанс добавил около 2 ГБ RAM на хосте — и снял с повестки весь класс проблем «публичный бот видит внутренние секреты».

Приоритеты: что делать сегодня, а на что можно забить

Порядок действий, если вы читаете это и у вас в проде Langflow. Первое — снимите версию и сравните с 1.9.1 и 1.10.1, а если инстанс старше 1.3.0 и смотрит в интернет — считайте сервер скомпрометированным и разбирайте его как инцидент, а не просто обновляйте. Второе — прямо сейчас, не дожидаясь апгрейда, закройте саморегистрацию (LANGFLOW_ENABLE_SIGNUP=false), проверьте, что LANGFLOW_AUTO_LOGIN=false, а LANGFLOW_API_KEY_SOURCE не равен env. Это несколько правок в окружении и один рестарт, они снимают самый дешёвый путь атаки — «завёл себе учётку и стал легитимным пользователем».

Третье — снимите срез по таблицам ключей и transaction, как показано выше, и отключите все ключи, у которых нет живого владельца или нет expires_at. Четвёртое — планируйте апгрейд параллельным инстансом, а не in-place. Пятое, и самое трудоёмкое: если вы жили на версии ниже 1.9.1 с несколькими пользователями, ротируйте credentials. Не выборочно, а все, которые фигурировали в глобальных переменных типа Credential. Отдельно напомню про Fernet: по advisory после апгрейда на 1.10.1 деплой с SECRET_KEY короче 32 символов требует заново ввести сохранённые credentials, так что это удобный момент сделать ротацию заодно.

Теперь про то, на что можно забить, потому что бюджет всегда конечен. Не надо форкать Langflow и вписывать туда свой ACL — вы навсегда останетесь на своей ветке и будете вручную портировать каждый security-фикс, а их выходит по несколько в квартал. Не надо строить SIEM-корреляцию ради одной платформы: для начала хватит access-лога прокси с отпечатком ключа и еженедельного SQL-среза. Не надо паниковать из-за самого факта, что флоу выполняет код — это по определению платформа исполнения, вопрос только в границах, и границы вы ставите инстансами и egress-политикой, а не попытками запретить исполнение.

И последнее наблюдение, ради которого всё это писалось. В большинстве небольших компаний, где я встречал Langflow, его ставил один человек «на попробовать», а потом туда тихо пришли остальные — и заодно он оказался опубликован в интернет ради виджета на сайте. Момент, когда прототип стал многопользовательским продом с персональными данными клиентов, никто не заметил и не пересмотрел настройки. Проверьте у себя три вещи: сколько строк в таблице "user", когда в последний раз обновлялся образ и отвечает ли порт Langflow с внешнего адреса. Если пользователей больше одного, образ старше полугода или порт открыт — начинайте с первого абзаца этого раздела.

Практический вывод одной строкой: в Langflow до 1.9.1 знание UUID = право на выполнение. Начните с версии и с саморегистрации, а атрибуцию вызовов стройте на обратном прокси — сама платформа вам её не даст.
Порядок действий: Приоритеты: что делать сегодня, а на что можно забить — схема
Порядок действий: Приоритеты: что делать сегодня, а на что можно забить. Открыть схему в полном размере

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

С какой версии Langflow безопасен для нескольких пользователей?

Минимальная планка против запуска чужого флоу по UUID (CVE-2026-55255) — 1.9.1. Но во всех версиях по 1.10.0 включительно остаётся слабая деривация Fernet-ключа, поэтому для хранения боевых credentials практический минимум — 1.10.1, а целевая версия — актуальная ветка (1.12.1 на 08.09.2026). Ниже 1.9.1 разграничения доступа между пользователями фактически нет.

Можно ли по логам понять, кто именно запускал чужой флоу?

Штатно — нет. В таблице transaction есть только flow_id, поля инициатора там нет. Восстанавливать атрибуцию приходится корреляцией: всплески запусков в transaction сопоставляются с last_used_at и total_uses из таблицы API-ключей и с журналом обратного прокси. Поэтому логирование отпечатка ключа на прокси надо настроить заранее, а не после инцидента.

Нужно ли менять API-ключи LLM-провайдеров, если чужой флоу просто «выполнился»?

Да. Значение credential пользователь не увидит — оно расшифровывается на сервере, — но действие с ним уже совершено, и вы не знаете, что именно флоу вернул наружу. Плюс отдельная история со слабым Fernet-ключом в версиях по 1.10.0: там при коротком SECRET_KEY вся таблица зашифрованных переменных расшифровывается офлайн. Ротируйте все credential-переменные.

Что даёт LANGFLOW_API_KEY_SOURCE=env и почему его нельзя использовать?

В этом режиме все запросы проверяются по одному ключу из переменной окружения, и по документации успешная аутентификация даёт права суперпользователя. Модель доступа выключается целиком: никакой привязки ключа к пользователю не остаётся. Режим годится разве что для однопользовательского стенда, закрытого от сети. Для всего остального — LANGFLOW_API_KEY_SOURCE=db, это и значение по умолчанию.

Решит ли проблему встроенный RBAC, который анонсируют в Langflow?

Возможно, со временем. В актуальной ветке в таблице flow уже есть поле workspace_id, а релиз 1.12 упоминает «RBAC authorization foundations». Но публичной документации по ролям и рабочим пространствам пока нет ни в разделе про аутентификацию, ни в release notes. Планировать разграничение доступа на этом я не советую: стройте изоляцию отдельными инстансами сейчас.

Обязательно ли разносить команды по разным инстансам Langflow?

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

Можно ли оставить Langflow доступным из интернета ради чат-бота на сайте?

Сам Langflow — нет. Порт 7860 должен слушать только localhost или внутреннюю сеть, редактор и вход — только через VPN. Наружу через nginx публикуется один маршрут, который вызывает бот, с ограничением частоты запросов, а служебные эндпоинты (/api/v1/validate, /api/v1/build_public_tmp, /api/v1/monitor, /api/v1/users) закрываются на прокси. Помните: CVE-2025-3248 и CVE-2026-33017 эксплуатировались без всякой учётки именно на открытых инстансах.

Что делать, если LANGFLOW_AUTO_LOGIN всё это время был True?

Считать, что любой, кто достучался до интерфейса, работал суперпользователем: видел все флоу, мог создать API-ключи и выполнить код в кастомных компонентах. Порядок: закрыть доступ снаружи, выставить LANGFLOW_AUTO_LOGIN=false с паролем суперпользователя, проверить таблицу ключей на незнакомые записи и свежий created_at, отключить LANGFLOW_SKIP_AUTH_AUTO_LOGIN, после чего ротировать все credential-переменные и ключи провайдеров.

Столкнулись с похожей задачей? Обращайтесь — решим

Если у вас происходит что-то из описанного в этой статье — или любая другая проблема с ИТ-инфраструктурой, — обращайтесь в любое время. Мои специалисты и я лично разберём ситуацию, найдём настоящую причину и доведём до решения.

Возьмёмся и за разовую задачу, и за постоянное обслуживание. Первичная консультация — бесплатно и без обязательств.

📞 +7 903 729-62-41 💬 MAX: +7 903 729-62-41 ✈ Telegram @ITfresh_Boss

С уважением, Семёнов Евгений Сергеевич, директор «АйТи Фреш» — IT-аутсорсинг для компаний до 50 рабочих мест, 15+ лет практики

Источники

© ООО «АйТи-Фреш» · Москва · Все статьи