
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:
coolify-testing-host
host.docker.internal
localhost
127.0.0.1
Isso provou que as rotas de inicialização do backend confiavam na sessão do membro.
A partir de páginas normais autenticadas, o mesmo membro podia enumerar:
Isso foi suficiente para direcionar o caminho do terminal sem precisar de divulgação de material de chave secreta.
Usando a mesma sessão autenticada e token XSRF, conectei-me a:
ws://127.0.0.1:6002/terminal/ws
O payload usou o mesmo formato de comando esperado pelo backend do terminal:
{"command":["timeout 30 ssh -i /var/www/html/storage/app/ssh/keys/ssh_key@ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o PasswordAuthentication=no -o ConnectTimeout=10 -o ServerAliveInterval=5 -o RequestTTY=no -o LogLevel=ERROR -p '22' 'root'@'coolify-testing-host' 'bash -se' << \\P0C\nprintf '__COOLIFY_POC_BEGIN__\\n'; id; whoami; hostname; printf '__COOLIFY_POC_END__\\n'\nP0C"]}
O websocket retornou:
pty-ready
__COOLIFY_POC_BEGIN__
uid=0(root) gid=0(root) groups=0(root)
root
efa027413801
__COOLIFY_POC_END__
Essa foi a prova importante.
Não apenas:
Mas sim a execução real de comandos no host gerenciado através do caminho do terminal restrito a administradores.
Uma parte dessa cadeia já teria sido interessante.
Por exemplo:
/terminal/auth/terminal/auth/ipsMas isso ainda deixaria espaço para descarte.
A validação mais forte foi de ponta a ponta:
Isso fecha a lacuna entre "bug de autorização em teoria" e "impacto prático na infraestrutura na realidade".
Também tornou muito mais fácil defender a gravidade.
Este problema foi classificado adequadamente como Crítico.
A classificação foi:
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
Isso faz sentido.
A alegação não é que um atacante não autenticado possa obter acesso ao shell do nada.
A alegação é que:
Isso é uma grande mudança de escopo, de falha de RBAC da aplicação para impacto no host gerenciado.
Portanto, embora os privilégios sejam baixos em vez de nenhum, o resultado ainda é claramente crítico.
Algumas pessoas subestimam vulnerabilidades que começam com PR:L.
Isso é um erro quando a funcionalidade afetada é o acesso ao terminal.
A verdadeira questão não é:
"O atacante já estava logado?"
A verdadeira questão é:
"O que esse usuário com privilégios mais baixos pode alcançar quando a autorização do backend está errada?"
Neste caso, a resposta foi:
Isso está muito além de um bug comum de permissão de membro.
A correção mínima correta é direta:
can.access.terminal tanto a POST /terminal/auth quanto a POST /terminal/auth/ipsIsso resolve a falha imediata da fronteira de confiança.
No meu patch de validação local, aplicar o middleware de autorização do terminal a essas duas rotas removeu o caminho de membro para terminal.
Mas a lição mais forte é que o backend não deve confiar em strings de comando SSH controladas pelo atacante como a principal fonte de metadados do alvo.
O endurecimento recomendado é:
Esse é o tipo de remediação que você deseja para uma funcionalidade de terminal:
Este problema foi reportado de forma privada através do fluxo de relatório de segurança do GitHub.
O relatório incluiu:
O problema foi posteriormente atribuído:
CVE-2026-34048
A principal lição aqui é simples:
uma interface restrita a administradores não importa se o canal de inicialização do backend confia em um estado mais fraco.
Essa é a verdadeira classe de problema.
Nada disso importa se:
Uma vez que uma plataforma gerencia infraestrutura, incompatibilidades de autorização deixam de ser erros comuns de controle de acesso. Elas se tornam vulnerabilidades com impacto na infraestrutura.
Essa é a verdadeira lição.
Esta vulnerabilidade não foi sobre um payload engenhoso.
Foi sobre identificar a fronteira de confiança certa.
O Coolify pretendia que o acesso ao terminal fosse restrito a administradores. Mas o backend do terminal em tempo real confiava em rotas que apenas verificavam se o usuário estava logado.
A partir daí, um membro da equipe com privilégios baixos podia controlar o caminho do terminal websocket e alcançar a execução de shell em um servidor da equipe.
É por isso que isso se tornou o CVE-2026-34048.