
Rotas de bootstrap do terminal apenas para administradores verificadas apenas pelo estado de login, que permitem que um membro normal da equipe acione o backend de terminal em tempo real do Coolify e execute comandos nos servidores da equipe.
Rotas de inicialização do terminal restrito a administradores verificavam apenas o estado de login, o que permitia que um membro normal da equipe controlasse o backend do terminal em tempo real do Coolify e executasse comandos nos servidores da equipe.
Encontrei esse problema ao revisar o Coolify, um PaaS auto-hospedado de código aberto, com uma pergunta de segurança muito direta em mente:
O acesso ao terminal é realmente imposto na fronteira de confiança do backend, ou apenas na interface do usuário?
Neste caso, a resposta foi ruim.
O Coolify pretendia restringir o acesso ao terminal a administradores e proprietários da equipe, mas as rotas de inicialização do terminal em tempo real apenas verificavam se o usuário estava logado. Isso permitia que um membro de equipe com privilégios baixos satisfizesse as verificações de confiança do websocket do terminal e alcançasse a execução de comandos nos servidores da equipe.
Validei isso de ponta a ponta em um laboratório local construído a partir da revisão vulnerável e, posteriormente, reportei-o de forma privada. O problema foi atribuído ao CVE-2026-34048 com:
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
Coolify: Coolify no GitHub
CVE: CVE-2026-34048
Isso afetou o Coolify, um PaaS auto-hospedado de código aberto. Em seu site oficial, o Coolify afirma ter 3.641+ clientes na nuvem e se apresenta como uma plataforma para implantar sites, bancos de dados, aplicativos web e mais de 280 serviços com um clique. Seu changelog oficial v4.0 também afirma que milhares de empresas e pessoas usam o Coolify em produção há 1-2 anos.
foto0
sessão de membro da equipe com privilégios baixos -> /terminal/auth e /terminal/auth/ips verificam apenas estado de login -> websocket em tempo real confia nessas respostas -> membro enumera servidor da equipe e UUID da chave SSH visível -> /terminal/ws aceita a sessão -> PTY baseado em SSH é criado -> acesso ao shell no servidor da equipe
O Coolify é um PaaS auto-hospedado e plataforma de implantação.
Ele gerencia:
Essa última capacidade é a importante aqui.
Uma vez que uma plataforma pode abrir terminais para hosts gerenciados, seu modelo de autorização deixa de ser apenas lógica de aplicação. Torna-se uma fronteira de confiança de infraestrutura.
A questão importante não era se a página /terminal parecia restrita a administradores.
A verdadeira questão era:
O caminho do terminal no backend realmente impõe essa mesma fronteira de autorização quando a sessão websocket é criada?
Neste caso, não impunha.
Funcionalidades de terminal são algumas das superfícies de maior valor em software de infraestrutura.
Por quê?
Porque qualquer incompatibilidade entre:
pode transformar um usuário normal de aplicação em um operador com acesso a shell.
É exatamente por isso que essa superfície valia a pena ser testada.
Eu não estava procurando por travamentos aleatórios ou bugs de permissão cosméticos.
Estava procurando por uma classe mais forte de falha:
Um recurso restrito a administradores depende de uma verificação de confiança do backend mais fraca do que a interface sugere?
Essa era a pergunta certa.
Não abordei o Coolify fazendo fuzzing em endpoints aleatórios e esperando algo interessante.
O caminho mais forte era identificar primeiro a fronteira de maior risco.
Para o Coolify, essa fronteira era o fluxo do terminal:
Isso fez das rotas de inicialização o lugar certo para olhar.
E foi onde o problema estava.
A causa raiz foi uma incompatibilidade de autorização entre a interface do terminal e as rotas de inicialização do websocket do terminal.
Na revisão vulnerável:
GET /terminal era protegido por can.access.terminalPOST /terminal/auth verificava apenas auth()->check()POST /terminal/auth/ips verificava apenas auth()->check()Isso significa que a interface era limitada pela autorização do terminal, mas a fronteira de confiança do backend era limitada pela simples presença de uma sessão autenticada.
O serviço em tempo real então confiava completamente nessas duas rotas.
Em docker/coolify-realtime/terminal-server.js:
verifyClient() fazia POST para /terminal/auth/terminal/auth/ipsEssa é toda a cadeia do bug.
Porque um membro normal da equipe podia construir as entradas necessárias a partir da superfície normal da aplicação:
/servers expunha UUIDs de servidores visíveis/server/{uuid} expunha ip, user e port em campos de formulário renderizados/security/private-key expunha UUIDs de chaves privadas visíveis da equipe/var/www/html/storage/app/ssh/keys/ssh_key@<uuid>
Portanto, o caminho de exploração era direto:
/terminal/auth/terminal/auth/ips/terminal/wsIsso não é uma incompatibilidade teórica. É uma falha prática de autorização do backend.
A distinção importante é a confiança do backend e a execução de comandos.
Muitos bugs parecem:
Isso por si só não é suficiente.
A verdadeira questão é:
O usuário com privilégios mais baixos ainda pode satisfazer as verificações de confiança do backend que importam?
Aqui, a resposta foi sim.
Isso não foi:
Foi:
É por isso que isso era um problema de segurança real.
Validei isso em um laboratório local controlado construído a partir de:
06f60c9a98bead0c932c6adf7fd43a45d9149048
O laboratório usava:
http://127.0.0.1:18000[email protected]localhost -> coolify-testing-host:22 as rootsshws://127.0.0.1:6002/terminal/wsA conta de membro não deveria ter acesso ao terminal através da interface normal voltada para administradores.
Isso estabeleceu a fronteira de segurança esperada.
Usando a sessão do membro, enviei:
POST /terminal/authPOST /terminal/auth/ipsAmbas tiveram sucesso.
/terminal/auth/ips retornou hosts autorizados pelo terminal, incluindo: