Чужой 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. Это опт-ин на «достать чужой флоу» для сценариев расшаривания. То есть небезопасное поведение из дефолтного стало явным и требующим осознанного включения. Ровно так и надо было делать сразу.
- Проверка владельца была только в ветке поиска по endpoint_name
- Поиск по UUID шёл мимо неё — достаточно было знать ID флоу
- CVE-2026-55255, CVSS 9.9, эндпоинт `/api/v1/responses`, нужна лишь валидная учётка или API-ключ
- Исправлено в 1.9.1 (PR #12832); для чужого объекта теперь возвращается 404
Разбор: языковая школа «Речь без границ», чат-бот записи и 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, а не на сам апгрейд.
- Langflow 1.8.2, PostgreSQL 16, nginx + TLS, 5 учёток, 18 флоу
- 11 чужих флоу выполнились с API-ключом методиста
- UUID флоу утёк не через взлом, а через ссылку в общем рабочем чате
- В истории чат-бота лежали телефоны родителей — это ПДн, а не просто логи
- Итог: апгрейд с 1.8.2, ротация 9 credential-переменных и 3 API-ключей, разделение на два инстанса
Почему это опаснее обычного «прочитал чужое»
Обычный 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. Для школы с публичным ботом записи это самый прямой путь к полной компрометации сервера.
- GHSA-qrpv-q767-xqq2 / CVE-2026-55255 — запуск чужого флоу через `/api/v1/responses`, до 1.9.1
- GHSA-9c59-2mvc-vfr8 / CVE-2026-33760 — 7 эндпоинтов /api/v1/monitor без проверки владельца, по 1.7.3, исправлено в 1.9.0
- GHSA-jxw3-mjmx-3pqm — слабый Fernet-ключ, все сохранённые credentials, по 1.10.0, исправлено в 1.10.1
- GHSA-vwmf-pq79-vjvx / CVE-2026-33017 — неаутентифицированный RCE через `build_public_tmp`, по 1.8.2, исправлено в 1.9.0
- CVE-2025-3248 — неаутентифицированный RCE в `/api/v1/validate/code`, до 1.3.0, в каталоге CISA KEV
Что чинить в первую очередь: версии и переменные окружения
Порядок обновления определяется тем, какие дыры вы закрываете. 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)'- 1.3.0 — фикс CVE-2025-3248 (RCE без аутентификации в /api/v1/validate/code)
- 1.9.0 — фикс monitor-IDOR и публичного build-RCE (CVE-2026-33760, CVE-2026-33017)
- 1.9.1 — фикс запуска чужого флоу по UUID (CVE-2026-55255)
- 1.10.1 — фикс деривации Fernet-ключа (после апгрейда с коротким SECRET_KEY credentials вводятся заново)
- 1.12.1 (08.09.2026) — актуальный релиз; в 1.12.0 бандлы компонентов и удаление OSS Admin Page
Чьи ключи уже сработали: форензика по базе 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 он утёк бы в лог целиком. Это отдельный повод резать такие запросы на прокси.
- `apikey` — кто, когда и сколько раз пользовался ключом (если трекинг не отключён)
- `transaction` — фактические исполнения узлов с `flow_id`, `inputs`, `outputs`, `status`
- `flow` — владелец (`user_id`), `access_type`, `endpoint_name`, `mcp_enabled`, `workspace_id`
- `variable` — какие credential-переменные принадлежат кому (значение зашифровано)
- Журнал обратного прокси с отпечатком ключа — единственный источник, где вызов связывается с конкретным ключом
Изоляция и закрытие снаружи без надежд на вендора
Мой основной вывод после «Речи без границ» звучит непопулярно: не стройте разграничение доступа внутри одного инстанса 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 примете как бонус, когда он доедет и обрастёт документацией.
- Один контур = один инстанс + своя БД + свой SECRET_KEY + своя сеть; для небольшой школы это публичный бот и внутренний контур
- Порт 7860 слушает только 127.0.0.1, редактор и логин — только через VPN
- Egress только через прокси с allowlist доменов; прямого выхода в интернет у контейнера нет
- Наружу открыт один маршрут бота с limit_req, всё остальное — 404/deny на обратном прокси
- Запрет передачи API-ключа query-параметром и ключ не в браузере, а на сервере сайта
- RBAC/workspaces в Langflow пока не задокументированы — не закладывайтесь на них в плане
Приоритеты: что делать сегодня, а на что можно забить
Порядок действий, если вы читаете это и у вас в проде 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_ENABLE_SIGNUP=false`, `LANGFLOW_AUTO_LOGIN=false`, `LANGFLOW_API_KEY_SOURCE=db`, порт 7860 закрыт снаружи
- На этой неделе: срез по ключам и `transaction`, отзыв бесхозных и бессрочных ключей
- В ближайший спринт: апгрейд параллельным инстансом до актуальной ветки
- После апгрейда: ротация всех credential-переменных и всех API-ключей
- Дальше: вынести публичного бота в отдельный инстанс и закрыть egress белым списком
Частые вопросы
С какой версии 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-переменные и ключи провайдеров.
Источники
- GitHub Security Advisory GHSA-qrpv-q767-xqq2 — «IDOR Vulnerability in /api/v1/responses Endpoint Allows Authenticated Attackers to Access Another User's Flow», CVE-2026-55255, CVSS 9.9 (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:L), затронуты версии < 1.9.1, исправлено в 1.9.1 (PR #12832). https://github.com/langflow-ai/langflow/security/advisories/GHSA-qrpv-q767-xqq2
- GitHub Security Advisory GHSA-9c59-2mvc-vfr8 — «IDOR/BOLA in Monitor API — Missing Ownership Enforcement on 7 Endpoints», CVE-2026-33760, CVSS 8.8, затронуты версии ≤ 1.7.3, исправлено в 1.9.0. https://github.com/langflow-ai/langflow/security/advisories/GHSA-9c59-2mvc-vfr8
- GitHub Security Advisory GHSA-jxw3-mjmx-3pqm — «Weak Fernet Key via random.seed()», CVE не присвоен, CVSS 9.1 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N), затронуты версии ≤ 1.10.0, исправлено в 1.10.1 (SHA-256 для коротких ключей + MultiFernet). https://github.com/langflow-ai/langflow/security/advisories/GHSA-jxw3-mjmx-3pqm
- GitHub Security Advisory GHSA-vwmf-pq79-vjvx — «Unauthenticated Remote Code Execution in Langflow via Public Flow Build Endpoint», CVE-2026-33017, CVSS 9.3, эндпоинт POST /api/v1/build_public_tmp/{flow_id}/flow, затронуты версии ≤ 1.8.2, исправлено в 1.9.0. https://github.com/langflow-ai/langflow/security/advisories/GHSA-vwmf-pq79-vjvx
- NVD — CVE-2025-3248 — Langflow до 1.3.0: code injection в /api/v1/validate/code без аутентификации, CVSS 3.1 9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H), CWE-94/CWE-306, в каталоге CISA KEV с 05.05.2025. https://nvd.nist.gov/vuln/detail/CVE-2025-3248
- Langflow Docs — API keys and authentication — Официальная документация: LANGFLOW_AUTO_LOGIN (по умолчанию True), LANGFLOW_SUPERUSER / LANGFLOW_SUPERUSER_PASSWORD, LANGFLOW_SKIP_AUTH_AUTO_LOGIN, LANGFLOW_ENABLE_SIGNUP (True), LANGFLOW_NEW_USER_IS_ACTIVE (False), LANGFLOW_ENABLE_SUPERUSER_CLI (true), LANGFLOW_API_KEY_SOURCE (db/env), LANGFLOW_DISABLE_TRACK_APIKEY_USAGE, LANGFLOW_CORS_ORIGINS (*), передача ключа заголовком x-api-key или query-параметром. https://docs.langflow.org/api-keys-and-authentication
- Langflow Docs — OpenAI Responses API — POST /api/v1/responses: поле model = ID или имя флоу, поле input, заголовок x-api-key. https://docs.langflow.org/api-openai-responses
- Исходный код Langflow (ветка main) — Сигнатура get_flow_by_id_or_endpoint_name с флагом widen_for_shares в src/backend/base/langflow/helpers/flow.py; модели ApiKey (last_used_at, total_uses, is_active, expires_at, api_key_hash), Flow (access_type, workspace_id), Variable и TransactionTable (__tablename__ = "transaction", без поля инициатора). https://github.com/langflow-ai/langflow/tree/main/src/backend/base/langflow
- Langflow releases — v1.12.1 от 08.09.2026 (актуальный); v1.12.0 от 01.09.2026 — «RBAC authorization foundations», бандлы компонентов, удаление OSS Admin Page, opt-in exec-sandbox microVM; v1.11.6 от 01.09.2026 — security-бэкпорты. https://github.com/langflow-ai/langflow/releases
