1 pontos por GN⁺ 3 시간 전 | 2 comentários | Compartilhar no WhatsApp
  • Como um protocolo de proxy para encaminhar requisições por socket a backends de longa execução, pode ser adotado quase sem alterar a estrutura existente dos handlers HTTP
  • O proxy reverso com HTTP/1.1 facilita divergências de interpretação dos limites das mensagens entre implementações, o que continua gerando problemas graves de segurança, como desync e request smuggling
  • O FastCGI fornece framing de mensagens claro desde 1996 e separa estruturalmente os headers do cliente das informações confiáveis adicionadas pelo proxy
  • No Go, net/http/fcgi preenche REMOTE_ADDR em Request.RemoteAddr e também reflete o uso de HTTPS em Request.TLS, permitindo lidar com a transmissão de informações confiáveis sem middleware adicional
  • Há limitações, como falta de suporte a WebSockets, um ecossistema de ferramentas fraco e menor throughput em algumas cargas de trabalho, mas, se WebSockets não forem necessários e o desempenho for suficiente, ele ainda parece uma opção prática

Posição do FastCGI e forma de adoção

  • O FastCGI não é usado apenas no modelo de executar um processo por arquivo; ele também pode ser usado como protocolo entre proxy e backend para enviar requisições a daemons de longa execução via socket TCP ou UNIX
  • Em Go, basta importar o pacote net/http/fcgi e trocar http.Serve por fcgi.Serve
    • Os handlers existentes continuam usando http.ResponseWriter e http.Request
    • O restante da estrutura da aplicação também permanece igual
  • Principais proxies como Apache, Caddy, nginx e HAProxy suportam backends FastCGI, e a configuração tende a ser simples

Problemas de parsing ao usar HTTP como protocolo de backend

  • O reverse proxy com HTTP se aproxima de um campo minado de segurança, e continuam surgindo problemas como a vulnerabilidade de desync no proxy de mídia do Discord, que permitia espiar anexos privados
  • O HTTP/1.1 parece, à primeira vista, um protocolo de texto simples, mas há formas demais de representar a mesma mensagem e muitas exceções de tratamento, o que facilita diferenças de interpretação entre implementações
  • O maior problema é que mensagens HTTP não têm framing explícito
    • O fim da mensagem é descrito pela própria mensagem de várias maneiras
    • Implementações diferentes podem interpretar de forma diferente onde uma mensagem termina e a próxima começa
  • Essas divergências formam a base de HTTP desync attacks, ou request smuggling, criando problemas graves de segurança quando o proxy reverso e o backend entendem os limites das mensagens de forma diferente
  • Continuar corrigindo diferenças entre parsers dificilmente é uma solução fundamental

Tratamento dos limites de mensagem em FastCGI e HTTP/2

  • O HTTP/2, se usado de forma consistente entre proxy e backend, pode resolver problemas de desync ao tornar claros os limites das mensagens
  • O FastCGI já oferece essa separação clara de limites desde 1996, com um protocolo mais simples
  • O nginx oferece suporte a backends FastCGI desde sua primeira versão, mas o suporte a backend HTTP/2 só foi adicionado no fim de 2025
  • No Apache, o suporte a backend HTTP/2 ainda permanece em estado "experimental"

O problema dos headers não confiáveis e a forma de separação do FastCGI

  • O problema não é só desync: o HTTP também carece de um meio robusto para transportar dados que o proxy precisa encaminhar com confiança, como o IP real do cliente, o nome do usuário autenticado pelo proxy ou informações do certificado do cliente em mTLS
  • Na prática, essas informações acabam sendo colocadas em headers HTTP, mas não existe separação estrutural entre os dados confiáveis adicionados pelo proxy e os headers não confiáveis enviados pelo cliente
  • Headers como X-Real-IP são usados com frequência para transmitir o IP real do cliente, mas isso só é seguro se o proxy remover completamente qualquer header pré-existente com esse nome, inclusive variações de maiúsculas e minúsculas, antes de adicioná-lo de novo
  • Essa abordagem é um terreno muito perigoso, com muitos caminhos para o backend acabar confiando em dados inseridos por um atacante
  • O proxy precisa apagar não apenas X-Real-IP, mas qualquer header usado para esse fim
  • Por exemplo, o middleware Chi define o IP real do cliente verificando True-Client-IP primeiro e só usa X-Real-IP se ele não existir
    • Mesmo que o proxy trate X-Real-IP corretamente, um atacante ainda pode causar problemas ao enviar True-Client-IP
  • O FastCGI separa as informações adicionadas pelo proxy e os headers do cliente por separação de domínios
    • Ambos são enviados como listas de parâmetros chave/valor, mas nomes de headers HTTP recebem o prefixo HTTP_
    • Assim, não se cria uma estrutura em que um header enviado pelo cliente possa ser interpretado como dado confiável do proxy

Tratamento de informações confiáveis com FastCGI no Go

  • O FastCGI define parâmetros padrão como REMOTE_ADDR para transmitir o IP real do cliente
  • O net/http/fcgi do Go preenche automaticamente esse valor em http.Request.RemoteAddr, funcionando sem middleware adicional
  • O proxy também pode transmitir por parâmetros não padronizados informações como uso de HTTPS, a cipher suite TLS negociada e o certificado do cliente
  • No Go, quando a requisição usou HTTPS, o campo TLS de Request é definido automaticamente com um valor diferente de nil
    • Mesmo vazio, isso é útil para verificar a imposição de HTTPS
  • Com fcgi.ProcessEnv, é possível acessar o conjunto completo de parâmetros confiáveis enviados pelo proxy

Por que a adoção foi lenta e quais são os limites práticos

  • Se o FastCGI é melhor, por que não é amplamente usado? Parece que isso se deve tanto ao ar datado do próprio nome quanto à baixa percepção sobre os problemas de segurança do reverse proxy com HTTP
  • A Watchfire já tratava de ataques de desync em 2005 e alertava que a solução não seria simples, mas esses ataques ficaram sem receber atenção adequada por mais de dez anos
  • O FastCGI continua viável em uso real, e a SSLMate o utiliza em produção há mais de dez anos
  • Ainda assim, por ser uma tecnologia antiga, ele tem fraquezas
    • Não foi atualizado para oferecer suporte a WebSockets
    • O ecossistema de ferramentas é limitado
    • Por exemplo, o curl suporta até FTP, Gopher e SMTP, mas não consegue enviar requisições FastCGI
  • Em benchmarks de um servidor FastCGI em Go atrás de vários reverse proxies, algumas cargas de trabalho tiveram throughput menor que HTTP/1.1 ou HTTP/2
    • Isso é visto menos como um limite do protocolo em si e mais como resultado de caminhos de código FastCGI menos otimizados do que os de HTTP

Julgamento final

  • Se WebSockets não forem necessários e o desempenho atual for suficiente, o FastCGI continua sendo uma opção válida
  • Mesmo que surja um gargalo, parece melhor adicionar hardware do que aceitar a complexidade e o pesadelo de segurança do reverse proxy com HTTP

2 comentários

 
rtyu1120 3 시간 전

É impressionante o comentário sobre FastCGI do Twisted que encontrei nos comentários do Lobsters: https://web.archive.org/web/20160723091923/…

 
GN⁺ 3 시간 전
Comentários do Hacker News
  • Concordo com a ideia do texto. Para esse tipo de uso, FastCGI me parece melhor que HTTP
    Também queria divulgar um protocolo chamado WAS (Web Application Socket). Há 16 anos, no trabalho, senti que até o FastCGI ainda não era bom o suficiente e acabei projetando um eu mesmo
    Em vez de framing no socket principal, ele usa 1 socket de controle e 2 pipes para os corpos brutos de requisição/resposta, e tanto o app WAS quanto o servidor web podem usar splice() nos pipes
    Não precisa de framing, permite cancelar requisições e foi feito para que os três file descriptors possam sempre ser recuperados
    Usei isso por anos em aplicações internas e em ambientes de hospedagem web, e também escrevi um PHP SAPI por conta própria. Bastante site roda internamente sobre WAS
    Tudo é open source
    library: https://github.com/CM4all/libwas
    documentation: https://libwas.readthedocs.io/en/latest/
    non-blocking library: https://github.com/CM4all/libcommon/tree/master/src/was/asyn...
    our web server: https://github.com/CM4all/beng-proxy
    WebDAV: https://github.com/CM4all/davos
    PHP fork with WAS SAPI: https://github.com/CM4all/php-src

    • FastCGI e HTTP não estão no mesmo nível
      HTTP serve para transportar dados entre as duas pontas, como navegador e servidor, enquanto FastCGI serve para processar esses dados entre o servidor e a aplicação
      Dei uma passada rápida no texto e parece que o autor escreve de um jeito que confunde os dois como se fossem substituíveis. Na prática, não são nem um pouco
      Para referência, eu também usei fcgi por 10 anos em serviços web voltados a clientes
  • Este texto é interessante justamente pelo que deixa de fora
    Quando o debate FastCGI vs. SCGI vs. HTTP estava pegando fogo, fundei uma startup Web2.0 e montei a stack de frontend por conta própria, e no fim HTTP venceu pela simplicidade
    Se você já precisa lidar com HTTP no gateway, usar HTTP também dali para frente evitava adicionar outro protocolo à stack, e isso tornava muito fácil colocar várias camadas de reverse proxy ou separar preocupações transversais como autenticação, sessão, terminação de SSL e filtragem de DDoS em servidores com papéis distintos
    No ambiente de desenvolvimento, dava para conectar direto ao app server por HTTP; em produção, o reverse proxy cuidava de SSL, autenticação e detecção de abuso, reaproveitando exatamente o mesmo app server
    Na época também pesava o fato de que o nginx era muito mais rápido e estável do que a maioria dos módulos FastCGI/SCGI. Começamos com HTTP -> Lighttpd -> FastCGI -> Django, mas usar só nginx era muito mais rápido
    Usar HTTP funcionava como uma espécie de End-to-End Principle da web. A ideia é que a rede e o protocolo devem ser indiferentes ao conteúdo transportado, e que a lógica da aplicação deve ficar nas pontas, não em nós de rede que filtram ou redirecionam
    Ainda assim, o ponto central do texto é que, do ponto de vista de segurança, muitas vezes é melhor seguir o princípio do menor privilégio. É preciso deixar passar só as comunicações previstas por allowlist para não acabar contribuindo sem querer para comprometimentos em outros pontos
    No fim, existe uma tensão entre os dois. E2E dá flexibilidade, mas essa flexibilidade também amplia o espaço para abuso; PoLP dá segurança, mas limita o sistema ao que foi planejado, dificultando a adaptação a novos requisitos
    [1] https://en.wikipedia.org/wiki/End-to-end_principle
    [2] https://en.wikipedia.org/wiki/Principle_of_least_privilege

    • Acho que essa analogia não encaixa bem, especialmente no contexto de connection caching e multiplexing
      Se um gateway intermediário multiplexa várias requisições HTTP em um único canal HTTP diferente, esse canal vai direto até o serviço que está escutando e não é demultiplexado antes do socket da aplicação, então isso quebra de forma fundamental a lógica end-to-end de várias maneiras
      Essa analogia só se sustenta minimamente quando a simetria de conexão 1:1 é preservada
      Na minha visão, todas as vulnerabilidades de reverse proxy surgiram diretamente de violações do end-to-end
      Se a analogia fosse correta, a entrega de SMTP passando por vários MX também deveria ser end-to-end, mas não é, e ali aparecem muitos problemas parecidos com os de reverse proxy, como desync de fronteira de mensagem
      Entendo a intenção de mapear requisições HTTP para mensagens, mas isso desmorona rápido por causa da semântica real de TCP e HTTP e de toda a infinidade de detalhes do protocolo
      O princípio end-to-end não permite tratar a semântica de forma frouxa. Ele exige disciplina muito rígida no gerenciamento de estado e nos limites da camada de transporte. Algo meio parecido com end-to-end não é end-to-end
    • Para desenvolvedores de webapp, a semântica de HTTP é útil, mas o próprio protocolo HTTP no wire é péssimo
      Por exemplo, nem havia multiplexing antes do HTTP 2.0, então usar HTTP puro entre reverse proxy e backend é bastante desperdiçador
      Também há problemas de segurança. Parsers diferentes podem nem concordar sobre onde termina a fronteira de uma requisição
      O Google também, há muito tempo, usa seu protocolo próprio Stubby entre o servidor web de frontend e as aplicações, em vez de HTTP direto
      É muito mais rápido que o wire protocol de HTTP e tem mais recursos. Para a maioria das empresas isso é exagero, mas em grande escala o custo de criar outro wire protocol e o tooling em volta dele se justifica plenamente
    • Aplicar o end-to-end principle dentro do datacenter não faz muito sentido e, como o texto mostra, acaba permitindo comportamentos inseguros
    • O que eu odeio no nginx é a documentação. Acho ela praticamente inútil
      O httpd também foi, em algum momento, na direção de tornar a configuração mais difícil, e eu abandonei quando mudaram de repente o formato de configuração
      Eu até poderia ter me adaptado, mas em vez disso migrei para lighttpd, e depois o ruby passou a automatizar a geração da configuração, então tecnicamente até poderia voltar para httpd
      Mesmo assim, não quero voltar. Se você desenvolve servidor web, precisa ter muito cuidado antes de forçar os usuários a se enquadrarem em um formato novo
      Se a ideia é mudar o formato de configuração por uma decisão aparentemente simples, então pelo menos ofereça algo como configuração em yaml como opção adicional, em vez de de repente empurrar um novo estilo de configuração com if-clause
  • Agora que WHATWG streams se espalharam bastante pelos navegadores, ficou bem fácil implementar algo parecido com WebSocket em cima de uma requisição HTTP de longa duração
    Basta enviar um stream de bytes e colocar um cabeçalho antes de cada mensagem; em muitos casos, só um campo de tamanho já basta
    Há vantagens também. Não exige um caminho especial extra na camada do servidor como o WebSocket, permite usar backpressure, herda de graça as melhorias do HTTP/2 e HTTP/3 e ainda tem menos overhead de framing
    Mas, AFAIK, ainda não há suporte para continuar fazendo streaming do corpo da requisição enquanto se recebe resposta ao mesmo tempo, então para streaming bidirecional completo ainda são necessárias duas requisições

  • Redescobri o antigo plain CGI, e ele é ótimo para permitir que usuários façam vibe code de páginas customizadas na nossa plataforma [1]
    Temos task list e data viewer como recursos prontos, mas os usuários frequentemente querem customizações bem mais detalhadas, como visualizações Kanban ou dashboards personalizados com filtros e gráficos
    Essa caixa tem um coding agent, então em vez de criarmos um report builder tradicional, o usuário pode simplesmente programar o que quiser
    A stdlib do Go dá ótimo suporte tanto no lado do servidor quanto no user space, e se o coding agent criar page-name/main.go para se comunicar via CGI, o servidor delega a requisição para lá
    Como o volume de dados e de pageviews é todo em escala pessoal, nem faz muita falta uma otimização como FastCGI
    Na era dos agentes, tecnologias antigas voltam a parecer novas

    1. https://housecat.com
    • Ao contrário do FastCGI, o CGI passa os cabeçalhos HTTP por variáveis de ambiente, então é preciso tomar cuidado com uma armadilha bem grande: https://httpoxy.org/
      A implementação de servidor CGI do Go não define $HTTP_PROXY, então essa parte é segura, mas ainda assim não gosto da forma como o CGI usa variáveis de ambiente
  • O lado do reverse proxy em geral só fazia tarefas simples, então os recursos embutidos do Nginx já bastavam
    Mesmo assim, a ideia de recorrer a FastCGI quando fosse preciso algo mais complexo provavelmente nem teria me ocorrido
    Há uns 10 anos usei um pouco de FastCGI para rodar parte de um código C++ na web, mas depois disso quase não usei mais

    • Hoje em dia embedded server é muito mais comum
      Você coloca um servidor HTTP dentro da própria aplicação e simplesmente faz o que precisa, sem gateway
  • A configuração PHP/Apache distribuída no ecossistema Red Hat é FPM (FastCGI Process Manager)
    Não sei se as distribuições RHEL usam FastCGI em outros lugares também
    $ rpm -qi php-fpm | grep ^Summary
    Summary : PHP FastCGI Process Manager

  • Também existe o uwsgi protocol
    Na prática, ele também tem um caráter quase de RPC para tudo

  • FCGI também é um sistema de orquestração
    Quando a carga sobe, ele inicia mais tasks de servidor; quando a carga cai, reduz; se uma task morre, sobe uma nova cópia
    É como uma espécie de Kubernetes de sistema único

    • Pela minha experiência, essa funcionalidade não era grande coisa
      No papel parece ótima, mas era comum funcionar bem em carga baixa e, quando vinha carga alta, começar a criar mais workers até estourar a memória
      Por isso, quase sempre era melhor trabalhar com número estático de workers
      Ainda assim, recuperação de crash pode ser útil se você precisar dela
    • Nós também usávamos exatamente desse jeito
  • Vale a pena parar por um momento para admirar a absurdidade dos cabeçalhos HTTP
    Se você só usa X-Real-IP quando True-Client-IP não existe, então mesmo que o proxy preencha X-Real-IP corretamente, um atacante ainda pode te pegar simplesmente enviando o cabeçalho True-Client-IP
    Tem X-Forwarded-For, X-Real-IP e até cabeçalhos customizados diferentes para cada CDN; alguns são listas separadas por vírgula e geralmente ainda incluem, sem utilidade nenhuma, o IP do nosso próprio LB
    Eu entendo por que isso aconteceu, mas não ajuda em nada
    Além disso, todos esses cabeçalhos também podem ser injetados por um user-agent malicioso. Parece que ninguém conseguiu chegar a um acordo sobre como servidores confiáveis deveriam transmitir informações importantes ao longo do pipeline
    Essa confusão combina bem com a absurdidade do cabeçalho User-Agent
    Nesse caso, a Apple levou isso a um extremo ainda maior ao decidir enviar informações totalmente falsas em nome da privacidade, como versões de sistema operacional inventadas

  • Há muito sentido nessa tese, mas o FastCGI perde informação por seguir o CGI/1.1 em partes como PATH_INFO
    A decodificação de URL é forçada, então não dá para representar uma encoded slash como %2F
    Dependendo da implementação, // no caminho também pode virar /, embora esse também seja um problema presente em várias implementações HTTP
    Em termos de expressividade, ele fica abaixo do HTTP, e a importância disso depende da aplicação
    Eu prefiro lidar com URLs de forma exata