Toutes les recherches

Quelles bibliothèques HTTP récupèrent réellement une URL gopher:// ? Une matrice SSRF testée empiriquement

Matrice SSRF gopher : 6 clients basés sur libcurl ouvrent un socket gopher, 0 pile HTTP native n'atteint gopher, redirection vers gopher désactivée par défaut depuis libcurl 7.65.2 en 2019.

Seuls les clients HTTP basés sur libcurl ouvrent réellement un socket gopher:// socket, et ce simple fait détermine si un server-side request forgery peut être escaladé de « faire récupérer une URL par le serveur » à « envoyer des octets arbitraires à n'importe quel service TCP interne ». Nous avons testé le client HTTP par défaut de dix langages face à un listener gopher actif. Six bibliothèques clientes ont ouvert le socket, et chacune d'entre elles est un simple wrapper autour de libcurl : la ligne de commande curl, le ext-curl, le pycurl de Python, le handler curl de Guzzle, la crate Rust curl, et Faraday avec l'adaptateur typhoeus. Chaque pile HTTP native testée a refusé gopher avant même d'ouvrir un socket : Go net/http, Java HttpURLConnection et HttpClient, Python requests, httpx, aiohttp et urllib3, Node http et fetch, Ruby Net::HTTP, .NET HttpClient, Rust reqwest, et wget. La règle en une phrase : gopher via SSRF est une affaire de libcurl.

La version directe Si le serveur vulnérable récupère des URL avec libcurl ou l'un de ses bindings, traitez une SSRF gopher comme un socket brut vers chaque service interne et prévoyez une exécution de code à distance via Redis ou FastCGI. S'il utilise une pile HTTP native, gopher est tout simplement exclu, car le schéma est rejeté au niveau de l'URL ou de l'adaptateur avant toute tentative de connexion. Ne vous fiez pas au langage. Une application PHP utilisant ext-curl est exposée, tandis qu'une application PHP utilisant les stream wrappers ne l'est pas ; une application Ruby utilisant Net::HTTP est protégée, mais la même application avec un adaptateur typhoeus ne l'est pas.

Pourquoi gopher est le schéma déterminant pour les SSRF

Le server-side request forgery est généralement décrit comme le fait de tromper une application pour lui faire récupérer une URL choisie par l'attaquant. En soi, cela permet d'atteindre des services HTTP internes, ce qui est fâcheux mais limité. gopher fait sauter cette limite. libcurl traite une URL gopher comme gopher://host:port/<type><selector>, où le premier caractère du chemin correspond au type d'élément gopher et tout ce qui suit est envoyé textuellement comme sélecteur, après décodage de l'URL, suivi de CRLF. Comme les octets sont d'abord décodés, un saut de ligne encodé en pourcentages dans le chemin devient un véritable saut de ligne sur le réseau. C'est là tout le gadget. Cela transforme « récupérer une URL » en « écrire n'importe quelle séquence d'octets sur n'importe quel port TCP accessible par le serveur », ce qui fait de gopher combiné à un Redis ou php-fpm interne la voie classique menant d'une SSRF aveugle à l'exécution de code. C'est l'un des aspects les plus critiques d'une surface d'attaque externe, et cela dépend entièrement du choix du client HTTP.

Le test : un vrai socket, pas une simple affirmation

La plupart des références affirment que « de nombreuses bibliothèques supportent gopher » et passent directement aux payloads. Nous voulions observer le succès ou l'échec plutôt que de nous contenter d'affirmations, la méthode est donc volontairement basique. Pour chaque client, nous démarrons un listener TCP à usage unique sur localhost, demandons au client de récupérer gopher://127.0.0.1:PORT/_<payload>, et enregistrons si le listener reçoit une connexion ainsi que les octets reçus. Un client qui refuse le schéma ne se connecte jamais, le listener ne rapporte donc rien. Un client qui ouvre le socket nous transmet exactement les octets envoyés sur le réseau. Il s'agit d'une expérience active menée sur nos propres conteneurs jetables et un listener localhost, sans solliciter aucun hôte tiers. Voici l'essentiel des résultats face à curl, pycurl et PHP ext-curl, sur trois versions différentes de 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)

Deux constats ressortent directement de cette sortie. Premièrement, les clients basés sur libcurl ouvrent le socket sur toutes les versions testées, d'une ancienne 7.81.0 à l'actuelle 8.21.0, ce n'est donc pas une particularité d'un build spécifique. Deuxièmement, le gadget CRLF est bien réel et exact. Les lignes pycurl et PHP ont été exécutées avec le sélecteur d'URL _PYCURL%0d%0aLINE2 et _PHPCURL%0d%0aLINE2, et les octets reçus contenaient un véritable retour chariot suivi d'un saut de ligne entre les deux tokens. Nous l'avons également confirmé de manière isolée : l'URL gopher://host:port/_TESTPAYLOAD%0d%0aSECONDLINE arrive sur le socket sous la forme b'TESTPAYLOAD\r\nSECONDLINE\r\n'. C'est un message protocolaire de deux lignes injecté directement dans un service TCP au moyen d'une simple URL.

La matrice

Les lignes marquées live ont été retestées face à un socket pour cet article. Les autres ont été vérifiées à partir du code source, des registres de protocoles et de la documentation de gestion des schémas de chaque client. La tendance est constante : la colonne « ouvre un socket » correspond exactement à l'ensemble des bindings libcurl, plus une exception modulaire en Perl.

Le client HTTP ouvre-t-il un socket gopher:// ? Testé en 2026-08
Client / bibliothèqueLangageSocket gopher://Raison
CLI curl / libcurl (live)COUIgopher, gophers, dict, ftp, file tous compilés ; la allowlist de transfert par défaut les active
wget / wget2 (live)Cnonseuls http/https/ftp intégrés ; wget2 est https-only
PHP ext-curl (curl_exec) (live)PHPOUIsimple binding libcurl ; le sink classique de SSRF vers Redis/FastCGI
PHP streams (file_get_contents)PHPnonuniquement les wrappers enregistrés ; aucun wrapper gopher/dict (file et ftp oui)
Guzzle (handler curl, par défaut)PHPOUIdélègue à ext-curl ; le handler stream ne le fait pas
pycurl (live)PythonOUIbinding direct libcurl ; même piège que PHP curl
requests (live)PythonnonInvalidSchema: no connection adapters
httpx / aiohttp / urllib3PythonnonUnsupportedProtocol / NonHttpUrlClientError / URLSchemeUnknown
urllib (stdlib) (live)Pythonnonaucun opener gopher dans Python 3 (file et ftp oui)
net/httpGononunsupported protocol scheme "gopher" ; uniquement des round-trippers http/https
HttpURLConnection / HttpClientJavanonles handlers sont http/https/ftp/file/jar/mailto ; unknown protocol: gopher
Apache HttpClient 4/5 / OkHttpJavanonle registre de schémas est limité à http/https par conception
Net::HTTPRubynonspécifique à http/https ; OpenURI transmet gopher à File.open (lecture locale, pas un pivot TCP)
FaradayRubynon*sur Net::HTTP ; l'adaptateur typhoeus (libcurl) est l'exception qui atteint gopher
http / https / undici / fetch (live)Node.jsnonspécifique au protocole ; WHATWG fetch gère uniquement http/https/file/data/blob
HttpClient / HttpWebRequest.NETnonhttp/https uniquement ; l'ancien GopherWebRequest a été supprimé dans .NET Core
reqwest / ureqRustnonpiles hyper et pure-Rust, http/https uniquement
crate curlRustOUIbinding direct libcurl, même mise en garde que pour tous les autres bindings
LWP::UserAgentPerlOUI*seulement si LWP::Protocol::gopher est installé ; LWP est modulaire par schéma

Comptez les lignes OUI et la conclusion s'impose d'elle-même. Six bibliothèques clientes ouvrent un socket gopher, et toutes les six reposent sur libcurl : curl, PHP ext-curl, pycurl, Guzzle-curl, la crate curl en Rust et Faraday avec typhoeus. Le septième et unique résultat positif hors libcurl est LWP::UserAgent, et cela nécessite encore qu'un opérateur ait installé le plugin optionnel LWP::Protocol::gopher plugin, il est donc désactivé par défaut. Tout le reste, chaque pile HTTP native majeure en Go, Java, Python, Node, Ruby, .NET et Rust, rejette le schéma avant même d'ouvrir une connexion. Aucun payload ingénieux ne peut changer cela, car le refus se produit au niveau du parseur d'URL ou du registre d'adaptateurs, en amont de tout socket.

Première nuance : la redirection vers gopher est désactivée par défaut

« L'application utilise curl, elle est donc exploitable » n'est qu'à moitié vrai sur un build moderne, et l'autre moitié est précisément là où l'on perd du temps en mission. Il existe deux allowlists distinctes dans libcurl. La allowlist de transfert régit les URL transmises directement, et elle active toujours gopher. La allowlist de redirection régit les schémas vers lesquels un en-tête Location: header may switch to, et depuis libcurl 7.65.2 en mai 2019, elle est restreinte à http, https, ftp et ftps uniquement. Ainsi, une open-redirect pointant vers gopher:// est refusée par défaut. Nous l'avons constaté sur 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'

La redirection n'atteint gopher que si l'application a elle-même ré-élargi les protocoles de redirection, en configurant CURLOPT_REDIR_PROTOCOLS_STR pour inclure gopher ou en passant à la CLI --proto-redir all. Cette mauvaise configuration n'est pas hypothétique. CVE-2026-33752 relève exactement de cette classe de vulnérabilité : la bibliothèque curl_cffi suivait les redirections vers des protocoles arbitraires faute de restreindre la allowlist de redirection, transformant une simple SSRF http en un pivot gopher complet. La conclusion pratique pour un attaquant est que, sur une cible libcurl moderne, la SSRF doit vous permettre d'injecter le schéma complet, une URL directe gopher://, plutôt que de compter sur une open-redirect. La conclusion pratique pour un défenseur est qu'élargir les protocoles de redirection revient à jouer avec une arme chargée, et que le comportement par défaut qui la neutralise est en place depuis 2019.

Deuxième nuance : le payload repose sur CRLF, et c'est trivial

La raison pour laquelle gopher importe plus que dict:// ou file:// réside dans le contrôle au niveau de l'octet. libcurl transmet la partie sélecteur de l'URL sous forme d'octets bruts après décodage de l'URL, ce qui permet à l'attaquant de construire le trafic réseau directement dans le chemin de l'URL. Un %0d%0a encodé en pourcentages devient un véritable retour chariot suivi d'un saut de ligne, et tout protocole orienté ligne interprétera le résultat comme des commandes distinctes :

Rien de tout cela ne nécessite de bogue mémoire ou de condition de course. Il s'agit simplement d'une URL qui se décode en un échange protocolaire, ce qui explique pourquoi un client compatible gopher transforme une SSRF modeste en une faille critique. Cette même logique, un agent ou un service qui récupère une URL que vous contrôlez, est ce qui rend les clients automatisés se connectant à des endpoints arbitraires particulièrement intéressants à auditer pour ce même type de pivot.

Comment y remédier

La défense ne consiste pas à « cesser d'utiliser curl ». libcurl convient parfaitement lorsque son périmètre de protocoles est verrouillé. L'erreur consiste à se reposer sur les valeurs par défaut et à laisser les entrées utilisateur choisir le schéma.

Méthode et limites, pour des résultats fiables

Le jeu de données est le nôtre, établi par observation directe des sockets, et il est reproductible.

Aucun hôte tiers n'a été contacté à aucun moment. Chaque connexion dans cette recherche visait un listener démarré sur loopback, ce qui fait toute la différence entre mesurer un comportement et sonder le système d'un tiers.

Foire aux questions

La bibliothèque Python requests prend-elle en charge le protocole gopher ?

Non. Lors d'un test live, requests lève InvalidSchema: No connection adapters were found pour une URL gopher et n'ouvre jamais de socket. Il en va de même pour httpx, aiohttp, urllib3 et le module standard urllib. La seule exception en Python est pycurl, un simple binding autour de libcurl, qui ouvre effectivement un socket gopher. Si votre code Python utilise requests ou httpx, un payload SSRF gopher ne peut pas atteindre un service interne par ce biais.

Pourquoi curl supporte-t-il gopher mais pas wget ?

Des ensembles de protocoles différents. libcurl compile par défaut gopher, gophers, dict, ftp et file, et sa allowlist de transfert les active. wget a été conçu uniquement pour http, https et ftp, et wget2 est https-only. Il s'agit d'une différence de conception et de compilation, pas d'un paramètre modifiable à l'exécution, c'est pourquoi curl est le pivot SSRF classique et wget ne l'est pas.

Quels protocoles libcurl supporte-t-il pour les SSRF ?

Sur un build standard, libcurl peut ouvrir gopher, gophers, dict, ftp, ftps, file, tftp, ldap et les protocoles de messagerie, en plus de http et https. Pour les SSRF, les plus importants sont gopher et dict, car tous deux envoient des octets contrôlés par l'attaquant sur un socket brut. gopher est le plus puissant, car le sélecteur après le caractère de type d'élément est transmis textuellement, de sorte qu'un CRLF encodé devient un véritable saut de ligne.

Comment le protocole gopher escalade-t-il une SSRF en RCE ?

Une SSRF classique amène le serveur à récupérer une URL http. gopher transforme cela en un envoi d'octets arbitraires vers n'importe quel port TCP accessible, car libcurl décode le sélecteur de l'URL et l'écrit brut. Une simple URL peut envoyer un SET Redis ainsi que CONFIG SET dir et SAVE pour déposer un webshell, ou un enregistrement FastCGI vers php-fpm. La SSRF est le vecteur de livraison, gopher est le payload, et un service interne orienté ligne est la cible.

HttpURLConnection en Java supporte-t-il gopher ?

Non. java.net.URL et HttpURLConnection enregistrent des handlers uniquement pour http, https, ftp, file, jar et mailto, et une URL gopher lève unknown protocol: gopher. Les bibliothèques modernes comme HttpClient, Apache HttpClient et OkHttp rejettent par conception tout schéma non-http. Java est effectivement immunisé contre les SSRF gopher, bien que java.net.URL supporte toujours file://, ce qui constitue un problème distinct.

Pourquoi libcurl ne suit-il pas une redirection vers une URL gopher:// ?

Depuis libcurl 7.65.2 en mai 2019, la allowlist de redirection par défaut est restreinte à http, https, ftp et ftps uniquement, de sorte qu'une 302 vers gopher:// est refusée et aucun socket ne s'ouvre. La soumission directe d'une URL gopher complète fonctionne toujours, car la allowlist de transfert est distincte. Une application peut rouvrir la voie de redirection en élargissant CURLOPT_REDIR_PROTOCOLS_STR, ce qui constitue la classe de bogue derrière CVE-2026-33752 dans curl_cffi. Sur une cible moderne, injectez le schéma gopher complet plutôt que de compter sur une open-redirect.

Comment envoyer un CRLF à Redis via un payload SSRF gopher ?

Encodez les sauts de ligne. libcurl décode le sélecteur de l'URL, donc gopher://127.0.0.1:6379/_SET%20k%20v%0d%0aSAVE%0d%0aQUIT envoie SET k v, un vrai CRLF, SAVE, un vrai CRLF, puis QUIT. Lors de notre test, _TESTPAYLOAD%0d%0aSECONDLINE est arrivé sous la forme des octets TESTPAYLOAD\r\nSECONDLINE\r\n. Redis utilise un protocole délimité par des sauts de ligne, il s'agit donc d'une séquence de commandes valide, ce qui explique pourquoi gopher associé à Redis constitue la chaîne SSRF vers RCE d'école.

Lectures complémentaires

Un pivot gopher fonctionnerait-il contre vous ?

Notre check à 100 $ cartographie votre surface d'attaque externe réelle comme le ferait un attaquant, sur un périmètre dont vous avez vérifié la propriété et autorisé par écrit, avec un opérateur senior lors de la restitution. Déterminer si un Redis ou php-fpm interne est à une seule SSRF de l'exécution de code est exactement le type de chaîne que nous recherchons.

Réserver un check à 100 $