Quais bibliotecas HTTP realmente buscam URLs gopher://? Uma matriz SSRF testada empiricamente

Apenas clientes HTTP construídos sobre a libcurl realmente abrirão um gopher:// socket, e esse único fato decide se um server-side request forgery pode ser escalado de "fazer o servidor buscar uma URL" para "enviar bytes arbitrários para qualquer serviço TCP interno". Testamos o cliente HTTP padrão de dez linguagens contra um listener gopher ativo. Seis bibliotecas de cliente abriram o socket, e todas elas são wrappers finos em torno da libcurl: a linha de comando curl, o ext-curl do PHP, o pycurl do Python, o curl handler do Guzzle, a crate Rust curl, e Faraday no adapter typhoeus. Todas as stacks HTTP nativas que testamos recusaram gopher antes mesmo de abrir um socket: Go net/http, Java HttpURLConnection e HttpClient, Python requests, httpx, aiohttp e urllib3, Node http e fetch, Ruby Net::HTTP, .NET HttpClient, Rust reqwest, e wget. A regra em uma frase: gopher via SSRF é uma história de libcurl.
ext-curl está exposta e uma aplicação PHP em stream wrappers não está; uma aplicação Ruby em Net::HTTP é segura e a mesma aplicação em um adapter typhoeus não é.
Por que gopher é o scheme que realmente importa para SSRF
O Server-side request forgery é geralmente descrito como induzir uma aplicação a buscar uma URL escolhida pelo atacante. Por si só, isso alcança serviços HTTP internos, o que é ruim, mas limitado. O gopher é o que remove esse limite. A libcurl trata uma URL gopher como gopher://host:port/<type><selector>, onde o primeiro caractere do path é o tipo de item gopher e tudo após ele é enviado literalmente como o selector, após o URL-decode, seguido por CRLF. Como os bytes passam por URL-decode primeiro, uma quebra de linha percent-encoded no path torna-se uma quebra de linha real no tráfego de rede. Esse é todo o gadget. Ele transforma "buscar uma URL" em "escrever qualquer sequência de bytes em qualquer porta TCP que o servidor consiga alcançar", razão pela qual gopher somado a um Redis interno ou php-fpm é o caminho canônico de um blind SSRF para execução de código. É uma das pontas mais afiadas de uma superfície de ataque externa, e se esconde inteiramente na escolha do cliente HTTP.
O teste: um socket real, não uma suposição
A maioria das referências afirma que "muitas bibliotecas suportam gopher" e passa direto para os payloads. Queríamos que o sucesso ou falha fosse observado, não apenas presumido, então o método é deliberadamente simples. Para cada cliente, iniciamos um TCP listener de execução única no localhost, solicitamos que o cliente busque gopher://127.0.0.1:PORT/_<payload>, e registramos se o listener chega a aceitar uma conexão e quais bytes chegam. Um cliente que recusa o scheme nunca se conecta, portanto o listener não reporta nada. Um cliente que abre o socket nos entrega os bytes exatos no tráfego. Este é um experimento ativo contra nossos próprios containers descartáveis e um listener em localhost, sem tocar em nenhum host de terceiros. Aqui está o núcleo do teste contra curl, pycurl e PHP ext-curl, em três versões diferentes da libcurl:
curl gopher:// (libcurl 7.81.0) -> CONNECTED b'AAAA\r\n'
curl gophers:// (libcurl 7.81.0) -> CONNECTED (TLS ClientHello on the wire)
curl dict:// (libcurl 7.81.0) -> CONNECTED b'CLIENT libcurl 7.81.0\r\nAAAA\r\nQUIT\r\n'
pycurl gopher:// (libcurl 8.21.0) -> CONNECTED b'PYCURL\r\nLINE2\r\n'
php ext-curl gopher:// (libcurl 8.14.1) -> CONNECTED b'PHPCURL\r\nLINE2\r\n'
wget gopher:// (wget 1.21.2) -> NO-CONNECT
python requests -> NO-CONNECT (InvalidSchema: no connection adapters)
python urllib -> NO-CONNECT (unknown url type: gopher)
node fetch / http.get -> NO-CONNECT (scheme rejected before connect)
Vale notar duas coisas diretamente dessa saída. Primeiro, os clientes libcurl abrem o socket em todas as versões que testamos, desde uma antiga 7.81.0 até a atual 8.21.0, logo isso não é uma peculiaridade de uma build específica. Segundo, o gadget de CRLF é real e exato. As linhas de pycurl e PHP foram executadas com o seletor de URL _PYCURL%0d%0aLINE2 e _PHPCURL%0d%0aLINE2, e os bytes recebidos continham um carriage-return line-feed autêntico entre os dois tokens. Confirmamos isso de forma isolada também: a URL gopher://host:port/_TESTPAYLOAD%0d%0aSECONDLINE chega ao socket como b'TESTPAYLOAD\r\nSECONDLINE\r\n'. Trata-se de uma mensagem de protocolo de duas linhas escrita diretamente em um serviço TCP usando apenas uma URL.
A matriz
Linhas marcadas com live foram retestadas contra um socket para esta publicação. O restante foi verificado a partir do código-fonte, registros de protocolos e documentação de manipulação de schemes de cada cliente. O padrão não varia: a coluna "abre um socket" é exatamente o conjunto de bindings da libcurl, além de uma exceção modular em Perl.
| Cliente / biblioteca | Linguagem | Socket gopher:// | Motivo |
|---|---|---|---|
| curl CLI / libcurl (live) | C | SIM | gopher, gophers, dict, ftp, file todos compilados internamente; allowlist padrão de transferência os habilita |
| wget / wget2 (live) | C | não | apenas http/https/ftp integrados; wget2 suporta apenas https |
PHP ext-curl (curl_exec) (live) | PHP | SIM | binding fino da libcurl; o sink clássico de SSRF para Redis/FastCGI |
PHP streams (file_get_contents) | PHP | não | apenas wrappers registrados; sem wrapper para gopher/dict (file e ftp sim) |
| Guzzle (curl handler, padrão) | PHP | SIM | delega para ext-curl; stream handler não |
| pycurl (live) | Python | SIM | binding direto da libcurl; o mesmo risco que o PHP curl |
| requests (live) | Python | não | InvalidSchema: no connection adapters |
| httpx / aiohttp / urllib3 | Python | não | UnsupportedProtocol / NonHttpUrlClientError / URLSchemeUnknown |
| urllib (stdlib) (live) | Python | não | sem opener para gopher no Python 3 (file e ftp sim) |
| net/http | Go | não | unsupported protocol scheme "gopher"; apenas round-trippers http/https |
| HttpURLConnection / HttpClient | Java | não | handlers são http/https/ftp/file/jar/mailto; unknown protocol: gopher |
| Apache HttpClient 4/5 / OkHttp | Java | não | registro de schemes é apenas http/https por design |
| Net::HTTP | Ruby | não | específico para http/https; OpenURI envia gopher para File.open (leitura local, não um pivot TCP) |
| Faraday | Ruby | não* | no Net::HTTP; o adapter typhoeus (libcurl) é a exceção que alcança gopher |
| http / https / undici / fetch (live) | Node.js | não | específico por protocolo; fetch do WHATWG suporta apenas http/https/file/data/blob |
| HttpClient / HttpWebRequest | .NET | não | apenas http/https; o legado GopherWebRequest foi removido no .NET Core |
| reqwest / ureq | Rust | não | hyper e stacks pure-Rust, apenas http/https |
| curl crate | Rust | SIM | binding direto da libcurl, mesma ressalva de qualquer outro binding |
| LWP::UserAgent | Perl | SIM* | apenas se LWP::Protocol::gopher estiver instalado; o LWP é modular por scheme |
Conte as linhas SIM e a conclusão é incontestável. Seis bibliotecas de cliente abrem um socket gopher, e todas as seis utilizam libcurl: curl, ext-curl do PHP, pycurl, Guzzle-curl, a crate curl do Rust e Faraday com typhoeus. O sétimo e único resultado positivo sem curl é o LWP::UserAgent do Perl, e até mesmo isso exige que um operador tenha instalado o plugin opcional LWP::Protocol::gopher, logo ele vem desabilitado por padrão. Todo o resto, toda stack HTTP nativa convencional em Go, Java, Python, Node, Ruby, .NET e Rust, rejeita o scheme antes de abrir uma conexão. Não existe payload engenhoso que mude isso, pois a recusa acontece no parser de URL ou no registro de adapters, antes de qualquer socket.
Nuance um: redirect para gopher está desabilitado por padrão
"A aplicação usa curl, então é explorável" é apenas meia verdade em builds modernas, e a metade que falta é onde muito tempo de teste é desperdiçado. Existem duas allowlists separadas na libcurl. A allowlist de transferência controla uma URL passada diretamente, e ela ainda habilita gopher. A allowlist de redirecionamento controla para quais schemes um header Location: pode alternar, e desde a libcurl 7.65.2 em maio de 2019 ela foi restringida apenas para http, https, ftp e ftps. Portanto, um open redirect apontando para gopher:// é recusado por padrão. Observamos isso acontecer na libcurl 7.81.0:
# default: an HTTP 302 to gopher:// is blocked
$ curl -sL http://listener/ -> Location: gopher://127.0.0.1:PORT/_REDIRPWN
* Protocol "gopher" not supported or disabled in libcurl
listener: (never reached)
# widened: the same redirect now opens the gopher socket
$ curl -sL --proto-redir all http://listener/
listener: GOPHER SOCKET OPENED b'REDIRPWN\r\n'
O redirecionamento só alcança gopher se a própria aplicação ampliou novamente os protocolos de redirecionamento, definindo CURLOPT_REDIR_PROTOCOLS_STR para incluir gopher ou passando para a CLI --proto-redir all. Essa má configuração não é hipotética. CVE-2026-33752 é exatamente essa classe de bug: a biblioteca curl_cffi seguiu redirecionamentos para protocolos arbitrários porque não restringiu a allowlist de redirecionamento, transformando um SSRF http simples de volta em um pivot gopher completo. A interpretação prática para um atacante é que, em um alvo moderno com libcurl, é necessário que o SSRF permita injetar o scheme completo, uma URL gopher:// direta, em vez de depender de um open redirect. A interpretação prática para um defensor é que ampliar protocolos de redirecionamento é uma arma carregada, e o padrão que a desarma está correto desde 2019.
Nuance dois: o payload é CRLF, e é trivial
A razão pela qual gopher importa mais do que dict:// ou file:// é o controle a nível de bytes. A libcurl envia a porção do selector da URL como bytes raw após o URL-decode, de modo que o atacante compõe o tráfego de rede diretamente no path da URL. Um %0d%0a em formato percent-encoded torna-se um carriage-return line-feed real, e qualquer protocolo orientado a linhas processará o resultado como comandos separados:
- Redis. Uma única URL gopher pode enviar
SET, depoisCONFIG SET direCONFIG SET dbfilename, depoisSAVE, gravando uma webshell, uma entrada no cron ou um arquivo authorized_keys no disco. Essa é a cadeia que os geradores públicos de payload produzem. - FastCGI (php-fpm na 9000). Um registro FastCGI forjado com
PHP_VALUEouSCRIPT_FILENAMEatinge execução de código contra um php-fpm associado apenas ao localhost. - memcached, SMTP, Postgres, Zabbix. Qualquer protocolo orientado a linhas ou com prefixo de tamanho acessível a partir da origem do SSRF é um candidato, pois você tem controle total dos bytes.
Nada disso precisa de uma falha de memória ou de uma race condition. Trata-se de uma URL que se decodifica em uma conversa de protocolo, razão pela qual um cliente compatível com gopher transforma um SSRF modesto em algo crítico. O mesmo princípio, um agente ou serviço buscando uma URL sob sua influência, é o que faz com que clientes automatizados que se conectam a endpoints arbitrários mereçam auditoria para a mesma classe de pivot.
O que fazer a respeito
A defesa não é "parar de usar curl". A libcurl é segura quando seu escopo de protocolos é fixado. A falha reside em confiar nos padrões e permitir que o input do usuário escolha o scheme.
- Fixe explicitamente a allowlist de schemes. Para libcurl, ext-curl e pycurl, defina
CURLOPT_PROTOCOLS_STR = "http,https"e nunca amplieCURLOPT_REDIR_PROTOCOLS_STRalém dehttp,https. Na CLI:--proto -all,http,https --proto-redir -all,http,https. - Nunca permita que o input do usuário controle o scheme da URL. Aceite um host e path, fixe via código
https://, e rejeite qualquer outra coisa. A maioria das cadeias de SSRF para gopher é mitigada aqui. - Trate o redirecionamento para um scheme não-http como uma falha crítica, e revalide o scheme resolvido após cada salto de redirecionamento, no lado do servidor, não apenas na URL inicial.
- Prefira um cliente exclusivamente http para requisições que não precisam de múltiplos protocolos. Go
net/http, JavaHttpClient, e Pythonrequestsouhttpxfornecem imunidade contra gopher, dict e file por padrão. - Remova CRLF bruto e codificado (
%0d,%0a) de qualquer path ou query de URL de saída influenciada pelo usuário. - Aplique filtragem de egress na camada de rede para que mesmo um pivot bem-sucedido não alcance serviços internos: bloqueie 6379, 11211, 9000, 25 e 5432 no tráfego de saída da aplicação, e associe serviços internos ao localhost com autenticação.
- Audite os consumidores indiretos de libcurl que sua stack esconde: o handler curl do Guzzle, Faraday com typhoeus, a crate curl do Rust e o LWP do Perl com o plugin gopher têm a mesma exposição que o PHP curl, sendo fáceis de passar despercebidos em um grafo de dependências.
Método e limitações, para que os números sejam confiáveis
O conjunto de dados é próprio, construído observando sockets, e é reproduzível.
- Como testamos. As linhas live rodaram em containers descartáveis (php:8.3-cli, python:3.12-slim) e em um host Linux padrão, contra um listener TCP em localhost
python3que reporta a conexão e os primeiros bytes recebidos. Três versões da libcurl foram avaliadas: 7.81.0, 8.14.1 e 8.21.0. - O que significa "não". Um "não" representa um cliente que nunca abre o socket, porque rejeita o scheme no parser de URL, no registro de adapters ou no handler de protocolo. Isso é mais seguro do que uma conexão aberta e depois fechada, pois os bytes do atacante nunca deixam o processo.
- As linhas com asterisco. Faraday e Perl LWP dependem do adapter configurado ou do plugin instalado, razão pela qual trazem um asterisco: o padrão é seguro, mas uma configuração específica não é.
- Escopo. Trata-se de determinar se um socket é aberto para gopher, o que define o pivot de SSRF. Isso não diz respeito ao SSRF baseado em http, que todos os clientes aqui realizam, nem sobre a exposição a
file://, que oURLdo Java e alguns outros ainda apresentam.
Nenhum host de terceiros foi contatado em momento algum. Cada conexão nesta pesquisa foi direcionada a um listener iniciado em loopback, o que representa a diferença entre mensurar um comportamento e escanear o sistema de terceiros.
Perguntas frequentes
A biblioteca requests do Python suporta o protocolo gopher?
Não. Em um teste prático, requests gera InvalidSchema: No connection adapters were found para uma URL gopher e nunca abre um socket. O mesmo se aplica a httpx, aiohttp, urllib3, e à biblioteca padrão urllib. A única exceção em Python é pycurl, um binding fino sobre a libcurl, que de fato abre um socket gopher. Se o seu código Python usa requests ou httpx, um payload de SSRF com gopher não consegue alcançar um serviço interno através dele.
Por que o curl suporta gopher mas o wget não?
Conjuntos de protocolos diferentes. A libcurl compila gopher, gophers, dict, ftp e file por padrão, e sua allowlist de transferência os habilita. O wget foi construído apenas para http, https e ftp, e o wget2 suporta apenas https. É uma diferença de compilação e design, não uma opção de runtime, razão pela qual o curl é o pivot clássico para SSRF e o wget não.
Quais protocolos a libcurl suporta para SSRF?
Em uma build padrão, a libcurl pode abrir gopher, gophers, dict, ftp, ftps, file, tftp, ldap e os protocolos de e-mail, além de http e https. Para SSRF, os mais relevantes são gopher e dict, porque ambos colocam bytes controlados pelo atacante em um socket raw. O gopher é o mais poderoso, pois o selector após o caractere de item-type é enviado literalmente, de modo que um CRLF codificado torna-se uma quebra de linha real.
Como o protocolo gopher escala um SSRF para RCE?
Um SSRF comum faz o servidor buscar uma URL http. O gopher transforma isso no envio de bytes arbitrários para qualquer porta TCP acessível, pois a libcurl decodifica a URL do selector e o escreve em formato raw. Uma única URL pode enviar um SET do Redis mais CONFIG SET dir e SAVE para gravar uma webshell, ou um registro FastCGI para o php-fpm. O SSRF é a entrega, gopher é o payload e um serviço interno orientado a linhas é o alvo.
O HttpURLConnection do Java suporta gopher?
Não. java.net.URL e HttpURLConnection registram handlers apenas para http, https, ftp, file, jar e mailto, e uma URL gopher lança unknown protocol: gopher. O moderno HttpClient, Apache HttpClient e OkHttp rejeitam qualquer scheme não-http por design. Java é efetivamente imune a SSRF com gopher, embora o java.net.URL ainda suporte file://, o que é uma preocupação à parte.
Por que a libcurl não segue redirecionamentos para uma URL gopher://?
Desde a libcurl 7.65.2 em maio de 2019, a allowlist de redirecionamento padrão é restrita a http, https, ftp e ftps, de modo que um 302 para gopher:// é recusado e nenhum socket é aberto. O envio direto de uma URL gopher completa ainda funciona, pois a allowlist de transferência é separada. Uma aplicação pode reabrir o caminho de redirecionamento ampliando CURLOPT_REDIR_PROTOCOLS_STR, que é a classe de bug por trás do CVE-2026-33752 no curl_cffi. Em um alvo moderno, insira o scheme gopher completo em vez de depender de um open redirect.
Como enviar CRLF para o Redis através de um payload de SSRF com gopher?
Codifique as quebras de linha. A libcurl decodifica a URL do selector, portanto gopher://127.0.0.1:6379/_SET%20k%20v%0d%0aSAVE%0d%0aQUIT envia SET k v, um CRLF real, SAVE, um CRLF real, depois QUIT. Em nosso teste, _TESTPAYLOAD%0d%0aSECONDLINE chegou como os bytes TESTPAYLOAD\r\nSECONDLINE\r\n. O Redis utiliza um protocolo delimitado por quebras de linha, portanto essa é uma sequência de comandos válida, e é por isso que gopher somado ao Redis é a cadeia clássica de SSRF para RCE.
Leituras relacionadas
- O que é uma superfície de ataque externa? Onde um SSRF se posiciona no mapa que um atacante constrói sobre o que sua organização expõe.
- Segurança de servidores MCP remotos: metade do registro é o servidor de outra pessoa. O mesmo padrão de "um cliente faz fetch de uma URL sob sua influência", na camada de agentes autônomos.
- Check versus scan versus pentest. Por que uma vulnerabilidade encadeada como SSRF para gopher para RCE é o que um teste real revela e um scanner não.
Um pivot com gopher funcionaria contra você?
Nosso check de $100 mapeia sua superfície de ataque externa real da mesma forma que um atacante faz, em um escopo que você verificou possuir e autorizou por escrito, com um operador sênior na apresentação dos resultados. Se um Redis interno ou php-fpm está a um SSRF de distância da execução de código é exatamente o tipo de cadeia que procuramos.
Agendar um check de $100