Flowise — визуальный конструктор для LLM-приложений: перетаскиваете на холст модель, векторное хранилище, инструменты и память, соединяете их линиями и получаете чат-бота, RAG-поиск по документам или агента. У проекта 55 тысяч звёзд на GitHub, 25 тысяч форков и почти 7 миллионов скачиваний Docker-образа. Его разворачивают у себя компании, которым нельзя отправлять данные в облачные конструкторы.
29 июля 2026 года команда объявила о закрытии: «разработчики всё чаще доверяют сложные задачи агентам для программирования, а жёсткие low-code-процессы упираются в потолок». 13 августа репозиторий заархивировали, 31 августа поддержка закончилась. В объявлении прямо сказано: код под Apache 2.0 ваш, форкайте.
Установки при этом никуда не делись, а бюллетени об уязвимостях продолжили выходить. Десятого сентября GitHub опубликовал ещё шесть advisory для Flowise — все с пометкой «исправленной версии нет». Я решил сделать то, что предложили авторы, и продолжить проект под именем Keelflow (GitHub, Apache-2.0). Киль (keel) держит судно на курсе — примерно этого и хочется от проекта, у которого кончилась команда.
Оказалось, что форкнуть Flowise «как есть» нельзя. Об этом, о том, что пришлось переписать, и о том, что нашлось в коде по дороге, — эта статья.
Ловушка в LICENSE.md
Первым делом я открыл лицензию. Она начинается не с текста Apache, а с преамбулы:
Portions of this software are licensed as follows: All content that resides under
packages/server/src/enterprisedirectory and files with explicit copyright notice such asIdentityManager.tsare licensed under Commercial License.
Дальше идёт сама коммерческая лицензия FlowiseAI: использовать в продакшене — только с подпиской, модифицировать можно, но все права на изменения принадлежат FlowiseAI, а копировать, публиковать и распространять запрещено. Разрешено лишь копировать и менять код «для разработки и тестирования».
Само по себе это распространённая схема open core: открытое ядро плюс закрытые корпоративные функции. Проблема в том, что именно лежит в enterprise. Я посчитал: 130 файлов, 13,4 тысячи строк на TypeScript — пятая часть сервера. И 64 файла открытой части эту папку импортируют:
28 enterprise/rbac/PermissionCheck
12 enterprise/utils/ControllerServiceUtils
11 enterprise/database/entities/workspace.entity
7 enterprise/database/entities/organization.entity
1 enterprise/middleware/passport
...
Вход в систему, JWT в cookie, сущности «пользователь», «организация» и «рабочее пространство», проверка прав на каждом маршруте, 48 миграций базы — всё это коммерческий код. Бесплатная версия Flowise 3.x без него даже не запускается: она использует тот же passport, те же сессии и ту же таблицу пользователей, просто с одним владельцем.
Получается, что распространять форк Flowise 3.x с этой папкой, в том числе в виде Docker-образа, лицензия не разрешает. Форк без неё не соберётся. Вариантов два: откатиться на Flowise 2.x, где авторизации ещё не было (и потерять два года изменений: agentflow v2, MCP, расписания), или вырезать коммерческий код и написать замену. Я выбрал второе.
Вырезать из истории, а не только из дерева
Удалить папку новым коммитом недостаточно: файлы остаются в истории, и любой, кто клонирует репозиторий, получает их вместе с ним. Поэтому я переписал историю с помощью git filter-repo:
git filter-repo --invert-paths
--path packages/server/src/enterprise
--path packages/server/src/IdentityManager.ts
--path packages/server/test/enterprise
Из 3634 коммитов остались 3614: двадцать меняли только коммерческие файлы и стали пустыми. Авторство и история остальных сохранились.
Перед этим я сделал локальную копию удалённого кода вне репозитория — это прямо разрешено лицензией «для разработки». Смотрел я в неё только ради двух вещей: схемы таблиц (чтобы новая реализация работала со старыми базами) и формата ответов API, который ожидает веб-интерфейс. Интерфейс лежит в открытой части, и по нему видно почти всё: какие поля пользователя он хранит, куда идёт при первом запуске, что делает при 401.
Свой вход: владелец и API-ключи
Нужно было решить, что именно писать. Корпоративный Flowise умел несколько организаций, пространства, роли, приглашения по почте и SSO через Google, Azure, Auth0 и GitHub. Бесплатный — одного владельца. Для продолжения, задача которого — безопасность, я выбрал минимум: одна учётная запись владельца плюс API-ключи с правами на отдельные действия. Меньше кода — меньше мест для ошибки. Кстати, две из шести свежих уязвимостей как раз в SSO: захват учётной записи через совпадение e-mail у разных провайдеров и обход токена приглашения. В Keelflow их просто нет.
Модуль packages/server/src/identity получился на ~800 строк:
-
Регистрация работает, только пока в базе нет ни одного пользователя. В Flowise 3.0.1 эндпоинт регистрации позволял неавторизованному пользователю создать учётную запись и войти (GHSA-v5w9-prxf-w882). Здесь любая существующая запись закрывает регистрацию, даже если это пользователь из корпоративной базы.
-
Пароли — bcrypt. Flowise тоже хранил bcrypt-хеши, поэтому владелец переносится со своим паролем.
-
Сессии — на сервере, а не JWT. В cookie лежит случайный 32-байтовый токен, в базе — только его SHA-256. Утёкшая база не даёт рабочих сессий. Сессия живёт 7 дней без активности и 30 дней максимум; смена пароля или сброс из командной строки удаляют все сессии пользователя. С JWT так просто не сделать: пришлось бы хранить чёрный список токенов.
-
Ограничение попыток входа: 10 неудачных за 15 минут с одного IP и столько же на один e-mail. Для несуществующего e-mail сервер всё равно тратит время на bcrypt, чтобы по времени ответа нельзя было подобрать адрес владельца.
-
API-ключи остались совместимыми: права хранятся в той же колонке
apikey.permissionsстроками видаchatflows:view,documentStores:upsert-config. Ключи, созданные во Flowise, работают с прежними ограничениями. До эндпоинтов учётной записи ключ не дотягивается вообще.
Проверка прав сводится к двум строкам: владельцу можно всё, ключу — только выданное.
const hasAny = (req: Request, required: string[]): boolean => {
const user = req.user
if (!user) return false
if (user.isOrganizationAdmin) return true
return required.some((permission) => (user.permissions ?? []).includes(permission))
}
Имена полей вроде isOrganizationAdmin и activeWorkspaceId я сохранил. На них завязаны сотни мест в открытом коде сервисов (только req.user?.activeWorkspaceId встречается 132 раза) и весь веб-интерфейс. Переименовать их было бы красиво, но это тысяча строк правок в чужом коде без единой пользы для пользователя.
Эндпоинты тоже прежние: /auth/resolve решает, показать форму создания владельца или форму входа, /auth/login, /account/register, /account/logout, /user. Интерфейс почти не пришлось трогать — поменял две строки в проверке маршрутов и ссылку «Забыли пароль?»: писем больше нет, пароль сбрасывается командой keelflow user <email> <пароль> на сервере.
Миграции, которые ничего не делают
Самая неприятная часть. В базе Flowise 3.x таблицы user, organization, workspace и колонки workspaceId в десятке таблиц создавали коммерческие миграции. Удалив их, я получаю две разные ситуации:
-
Старая база (Flowise 3.x): всё уже создано. Новые миграции не должны ничего сломать.
-
Новая база или база от Flowise 2.x: таблиц нет, и без них открытые миграции, которые идут дальше, упадут.
Второе оказалось хитрее, чем выглядит. Например, миграция ModifyChatflowType для SQLite пересоздаёт таблицу chat_flow с внешним ключом на workspace:
CREATE TABLE "temp_chat_flow" (
...
"workspaceId" TEXT,
FOREIGN KEY ("workspaceId") REFERENCES "workspace"("id")
);
Значит, workspace должна существовать раньше. Тут помогает поведение TypeORM 0.3: он запускает все миграции, которых нет в таблице migrations, по имени, отсортировав их по отметке времени в названии класса. Раз так, я дал своим миграциям отметки времени рядом с удалёнными:
// между AddApiKey1720230151480 и следующей открытой миграцией
export class KeelflowIdentityTables1720230151483 implements MigrationInterface { ... }
// сразу после AddExecutionEntity1738090872625
export class KeelflowWorkspaceColumns1738090872626 implements MigrationInterface { ... }
На новой базе они выполнятся ровно там, где раньше выполнялись коммерческие. На старой — тоже выполнятся (их имён в таблице migrations нет), но каждый шаг сначала проверяет, есть ли таблица, колонка или индекс, и ничего не делает, если есть.
Четыре СУБД — SQLite, Postgres, MySQL и MariaDB — отличаются типами. Во Flowise для каждой был отдельный набор файлов; я написал одну реализацию с небольшой функцией, которая выбирает тип под диалект: uuid DEFAULT uuid_generate_v4() в Postgres, varchar(36) в MySQL, varchar в SQLite. Колонки добавляются через API TypeORM, который сам знает синтаксис каждой базы.
Отдельный случай — открытая миграция AddApiKeyPermission: она чистит права в таблице role, которой в Keelflow нет. Хватило одной строки if (!(await queryRunner.hasTable('role'))) return.
Как проверить обновление, не имея старой установки
Утверждение «ваша база от Flowise заработает» нужно проверять на настоящей базе от Flowise, а не на той, что я себе представляю. Поэтому в тестах есть скрипт, который скачивает опубликованный пакет flowise@3.1.4 из npm и запускает его скомпилированные миграции — все 57, включая коммерческие. Код я не распространяю: он скачивается из npm во время теста, как при обычной установке Flowise.
Скомпилированные миграции Flowise импортируют пакет flowise-components, которого в Keelflow нет: он переименован. Проблему решает перехват разрешения модулей:
const resolveFilename = Module._resolveFilename
Module._resolveFilename = function (request, ...rest) {
return resolveFilename.call(this, request === 'flowise-components' ? 'keelflow-components' : request, ...rest)
}
Потом скрипт создаёт владельца так, как это делал Flowise 3.x (с ролью owner, записями в organization_user и workspace_user), flow и API-ключ с правом только на просмотр. После этого запускается Keelflow, и тест проверяет: вместо создания владельца предлагается вход, регистрация закрыта, старый пароль подходит, старый flow виден, старый API-ключ читает flow, но получает 403 на учётные данные. В CI это отдельная задача, плюс сквозной тест входа и ключей на каждой из четырёх СУБД в контейнерах.
Есть и тонкость с папкой данных. Flowise хранит базу, ключ шифрования и загрузки в ~/.flowise. Keelflow по умолчанию использует ~/.keelflow, но если её нет или она пуста, а ~/.flowise есть, берёт старую. «Или пуста» появилось из-за Docker: в образе папка /home/node/.keelflow создаётся заранее с правами пользователя node, иначе Docker создаст пустой том от root. Без этого условия пустая новая папка перекрыла бы смонтированный старый том.
Что нашлось по дороге
Когда проходишь по коду с вопросом «что тут может навредить пользователю», находятся вещи, которые не выглядят как уязвимости, пока на них не посмотришь.
Трекер партнёрской программы в админке
В packages/ui/index.html — странице, которую отдаёт каждая самостоятельно развёрнутая копия Flowise, — было вот это:
<script async src="https://r.wdfl.co/rw.js" data-rewardful="9a3a26"></script>
Это скрипт Rewardful — сервиса партнёрских программ для SaaS: он отслеживает переходы по реферальным ссылкам и регистрации. А у формы создания владельца стоял атрибут data-rewardful, по которому этот скрипт находит формы регистрации. Для облачной версии Flowise это, видимо, имело смысл. Но из-за общего кода интерфейса скрипт загружался и в каждой self-hosted установке — внутри админки, где хранятся ключи ко всем LLM-провайдерам и базам.
Я не видел, чтобы скрипт делал что-то плохое. Но сторонний JavaScript в админке — это доверие к чужому CDN на уровне «может прочитать всё, что видит пользователь». Если домен wdfl.co когда-нибудь сменит владельца или скрипт подменят, пострадают все, у кого открыт интерфейс Flowise. В Keelflow скрипта нет.
Заодно там же загружались шрифты с Google Fonts, а шапка на каждой странице запрашивала у GitHub API число звёзд репозитория. Шрифт Inter теперь идёт в составе приложения через @fontsource/inter, кнопка GitHub стала обычной ссылкой. После открытия интерфейса Keelflow в браузере все запросы идут только на свой сервер — я проверил по вкладке «Сеть».
Список моделей с GitHub при каждом обращении
Список моделей OpenAI, Anthropic и других провайдеров, который вы видите в выпадающих меню узлов, по умолчанию загружался так:
const modelFile =
process.env.MODEL_LIST_CONFIG_JSON ?? 'https://raw.githubusercontent.com/FlowiseAI/Flowise/main/packages/components/models.json'
const resp = await axios.get(modelFile)
То есть сервер ходил на GitHub при каждом открытии узла с моделью — без таймаута и без кеша. В закрытой сети каждое открытие висело, пока соединение не отвалится по таймауту ОС. А содержимое определял тот, кто управляет репозиторием FlowiseAI. Теперь список берётся из models.json в составе приложения; если нужен свой, его можно указать файлом или URL, и URL кешируется на час.
«Доверять всем прокси»
Настройка TRUST_PROXY по умолчанию была true: Express верил заголовку X-Forwarded-For от кого угодно. Это значит, что клиент сам выбирал IP-адрес, который видят ограничители частоты запросов, — достаточно добавить заголовок со случайным адресом. Новое значение по умолчанию — loopback, linklocal, uniquelocal: заголовку верят, только если запрос пришёл от прокси на этой же машине или в частной сети (Docker, Kubernetes, nginx в локалке). Кому нужно старое поведение, ставит TRUST_PROXY=true.
Учётные данные чужого пространства
Одна из сентябрьских уязвимостей без исправления, GHSA-27w2-26m5-x82c: при выполнении flow учётные данные искались по одному id, без проверки пространства. Flow из одного пространства мог сослаться на UUID ключа OpenAI из другого и получить его расшифрованным. Исправление — одно условие в запросе:
const credential = await appDataSource.getRepository(databaseEntities['Credential']).findOneBy({
id: selectedCredentialId,
...(options.workspaceId ? { workspaceId: options.workspaceId } : {})
})
Три другие «неисправленные» уязвимости из того же набора (история чатов и загрузок без проверки прав) при проверке оказались уже закрыты в 3.1.4 — просто в бюллетенях не проставили исправленную версию.
Платные функции с открытым кодом
Наборы данных, оценки качества ответов, оценщики и просмотр логов сервера лежат в открытой части и распространяются под Apache. Но маршруты к ним были обёрнуты в проверку тарифа, которая в бесплатной версии всегда отвечала 403. В Keelflow тарифов нет, и эти разделы просто работают.
Зависимости: 385 → 153
Lock-файл Flowise содержит 4200 пакетов. Сервис osv.dev из моей сети не открывался, поэтому я воспользовался тем же эндпоинтом, что и npm audit: registry.npmjs.org/-/npm/v1/security/advisories/bulk принимает список «пакет → версии» и возвращает бюллетени, которые к ним относятся. Результат для Flowise 3.1.4: 385 бюллетеней, из них 19 критических и 145 высоких.
Худший — vm2 3.11.2. В этой песочнице Flowise выполняет пользовательский JavaScript из узлов Custom Function и Custom Tool, если не подключён платный облачный E2B. Для 3.11.2 опубликовано девять критических выходов из песочницы, то есть выполнение кода на сервере. Проект vm2 в 2026 году ожил и выпускал исправления почти каждую неделю: 3.11.6, 3.11.7 (двадцать бюллетеней разом), 3.11.8, 3.12.0–3.12.2. Обновление на 3.12.2 не меняет API, но я не питаю иллюзий: vm2 — это не граница безопасности, и в документации Keelflow так и написано. Редактировать flow должны только те, кому вы доверяете выполнение кода на сервере.
Остальное я делал так: для каждого уязвимого пакета скрипт находит самую раннюю версию в той же мажорной ветке, к которой не относится ни один бюллетень, и записывает её в pnpm.overrides. Так обновились около 60 пакетов: axios, handlebars, fast-xml-parser, langchain 0.3, langsmith, mysql2, xmldom, lodash, js-yaml, multer, ws, sharp, playwright и другие. Три случая потребовали ручной работы:
-
expr-eval — выполнение кода, исправленной версии нет, проект заброшен. Его тянет калькулятор из
@langchain/community. Помог форкexpr-eval-forkс тем же API:"expr-eval": "npm:expr-eval-fork@^3.0.3". -
xlsx — SheetJS перестал публиковать в npm, последняя версия там 0.18.5 с известными уязвимостями. Исправленная 0.20.3 лежит на CDN самого SheetJS.
-
jsondiffpatch — исправленная версия есть, но только как ES-модуль, а его подключает CommonJS-сборка Vercel AI SDK. Оставил старую и записал в SECURITY.md.
Ещё часть ушла вместе с коммерческим кодом: passport со всеми стратегиями, express-session и хранилища сессий, jsonwebtoken, nodemailer, stripe. Итог — 153 бюллетеня, одна критическая уязвимость (в tar, который работает только при установке пакетов) и 20 высоких, в основном в инструментах сборки: vite, svgo, postcss.
Что вышло
Keelflow 3.2.0 — это Flowise 3.1.4 плюс:
-
весь код под Apache-2.0, коммерческий удалён из истории;
-
вход владельца и API-ключи с серверными сессиями и ограничением попыток;
-
исправленные уязвимости, vm2 3.12.2, зависимости без 232 бюллетеней;
-
никаких запросов наружу по умолчанию;
-
наборы данных, оценки и логи без тарифов;
-
Docker-образ для amd64 и arm64 от непривилегированного пользователя.
Проверено на 901 юнит-тесте сервера, 65 тестах интерфейса, сквозном тесте входа на четырёх СУБД и обновлении с базы, созданной миграциями настоящего Flowise 3.1.4.
Переезд с Flowise в Docker — одна строка:
docker run -d -p 3000:3000 -v ~/.flowise:/home/node/.flowise ghcr.io/perruer/keelflow:latest
Что не переносится: другие пользователи из корпоративной версии, SSO, роли и несколько рабочих пространств. Если это нужно — пишите в issue, обсудим; но в первую очередь проект про то, чтобы то, что у вас уже работает, работало безопасно.
Код: github.com/Perruer/keelflow. Если вы держите Flowise в продакшене и что-то сломалось при переезде — issue очень помогут: это ровно те случаи, которые тесты не поймали.
Автор: Perruer


