
Маршруты инициализации терминала, доступные только администраторам, проверяются только на состояние входа в систему, что позволяет обычному члену команды управлять терминалом реального времени Coolify и выполнять команды на серверах команды.
Маршруты начальной загрузки терминала, доступные только администраторам, проверяли только состояние входа в систему, что позволяло обычному участнику команды управлять серверной частью терминала Coolify в реальном времени и выполнять команды на серверах команды.
Я обнаружил эту проблему при проверке Coolify, открытого PaaS с самостоятельным хостингом, имея в виду очень прямой вопрос безопасности:
Действительно ли доступ к терминалу контролируется на уровне серверной части (backend trust boundary) или только в интерфейсе?
В данном случае ответ был плохим.
Coolify предполагал, что доступ к терминалу ограничен администраторами и владельцами команды, но маршруты начальной загрузки терминала в реальном времени проверяли только, авторизован ли пользователь. Это позволяло участнику команды с низкими привилегиями удовлетворить проверки доверия веб-сокета терминала и получить доступ к выполнению команд на серверах команды.
Я провел полную валидацию в локальной лаборатории, построенной на уязвимой ревизии, и затем сообщил о проблеме приватно. Проблеме был присвоен CVE-2026-34048 со следующим показателем:
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
Coolify: Coolify на GitHub
CVE: CVE-2026-34048
Это касалось Coolify, открытого PaaS с самостоятельным хостингом. На официальном сайте Coolify заявляется, что у него 3,641+ облачных клиентов, и он позиционируется как платформа для развертывания веб-сайтов, баз данных, веб-приложений и 280+ сервисов в один клик. В официальном журнале изменений v4.0 также сказано, что тысячи компаний и людей используют Coolify в production в течение 1–2 лет.
photo0
Сессия участника команды с низкими привилегиями -> /terminal/auth и /terminal/auth/ips проверяют только состояние входа -> веб-сокет реального времени доверяет этим ответам -> участник перечисляет серверы команды и видимый UUID SSH-ключа -> /terminal/ws принимает сессию -> через SSH создается PTY -> доступ к оболочке на сервере команды
Coolify — это PaaS с самостоятельным хостингом и платформа для развертывания.
Он управляет:
Последняя возможность здесь важна.
Как только платформа может открывать терминалы на управляемых хостах, ее модель авторизации перестает быть просто логикой приложения. Она становится границей доверия инфраструктуры.
Важным вопросом было не то, выглядит ли страница /terminal доступной только администраторам.
Настоящий вопрос был:
Действительно ли серверный путь терминала применяет ту же границу авторизации при создании сессии веб-сокета?
В данном случае — нет.
Функции терминала — одни из самых ценных поверхностей в инфраструктурном ПО.
Почему?
Потому что любое несоответствие между:
может превратить обычного пользователя приложения в оператора с доступом к оболочке.
Именно поэтому эту поверхность стоило тестировать.
Я не искал случайные сбои или косметические ошибки разрешений.
Я искал более сильный класс ошибок:
Использует ли функция, доступная только администратору, более слабую проверку доверия на сервере, чем предполагает интерфейс?
Это был правильный вопрос.
Я не подходил к Coolify, фаззя случайные конечные точки и надеясь на что-то интересное.
Более сильным путем было сначала определить самую рискованную границу.
Для Coolify этой границей был рабочий процесс терминала:
Это сделало маршруты начальной загрузки правильным местом для поиска.
И там была проблема.
Первопричиной было несоответствие авторизации между интерфейсом терминала и маршрутами начальной загрузки веб-сокета терминала.
В уязвимой ревизии:
GET /terminal защищался can.access.terminalPOST /terminal/auth проверял только auth()->check()POST /terminal/auth/ips проверял только auth()->check()Это означает, что интерфейс шлюзовался авторизацией терминала, но серверная граница доверия шлюзовалась простым наличием аутентифицированной сессии.
Служба реального времени полностью доверяла этим двум маршрутам.
В docker/coolify-realtime/terminal-server.js:
verifyClient() отправлял POST на /terminal/auth/terminal/auth/ipsЭто вся цепочка ошибки.
Потому что обычный участник команды мог получить необходимые данные из обычного интерфейса приложения:
/servers показывал видимые UUID серверов/server/{uuid} показывал ip, user и port в отображаемых полях формы/security/private-key показывал видимые UUID приватных ключей команды/var/www/html/storage/app/ssh/keys/ssh_key@<uuid>
Таким образом, путь эксплуатации был прямолинейным:
/terminal/auth/terminal/auth/ips/terminal/wsЭто не теоретическое несоответствие. Это практический сбой авторизации на сервере.
Важное различие — доверие на сервере и выполнение команд.
Множество ошибок выглядят так:
Этого самого по себе недостаточно.
Настоящий вопрос:
Может ли пользователь с меньшими привилегиями все равно удовлетворить важные проверки доверия на сервере?
В данном случае ответ был «да».
Это не было:
Это было:
Вот почему это была настоящая проблема безопасности.
Я подтвердил это в контролируемой локальной лаборатории, построенной на ревизии:
06f60c9a98bead0c932c6adf7fd43a45d9149048
В лаборатории использовались:
http://127.0.0.1:18000[email protected]localhost -> coolify-testing-host:22 as rootsshws://127.0.0.1:6002/terminal/wsУчетная запись участника не должна была иметь доступ к терминалу через обычный интерфейс для администраторов.
Это установило ожидаемую границу безопасности.
Используя сессию участника, я отправил:
POST /terminal/authPOST /terminal/auth/ipsОба успешно выполнились.
/terminal/auth/ips вернул хосты, авторизованные для терминала, включая:
coolify-testing-host
host.docker.internal
localhost
127.0.0.1
Это подтвердило, что маршруты начальной загрузки на сервере доверяли сессии участника.