Welke HTTP-libraries halen daadwerkelijk een gopher://-URL op? Een empirisch geteste SSRF-matrix

Alleen HTTP-clients die zijn gebouwd op libcurl openen daadwerkelijk een gopher:// socket, en dat ene feit bepaalt of een server-side request forgery kan worden geëscaleerd van "laat de server een URL ophalen" naar "stuur willekeurige bytes naar elke interne TCP-service." We hebben de standaard HTTP-client van tien programmeertalen getest tegen een live gopher-listener. Zes client-libraries openden de socket, en stuk voor stuk zijn ze een dunne wrapper rond libcurl: de curl-commandline, PHP's ext-curl, Python's pycurl, Guzzle's curl-handler, de Rust curl crate, en Faraday op de typhoeus-adapter. Elke native HTTP-stack die we probeerden weigerde gopher voordat er ooit een socket werd geopend: Go net/http, Java HttpURLConnection en HttpClient, Python requests, httpx, aiohttp en urllib3, Node http en fetch, Ruby Net::HTTP, .NET HttpClient, Rust reqwest, en wget. De wet in één zin: gopher-via-SSRF is een libcurl-verhaal.
ext-curl is kwetsbaar en een PHP-app op stream-wrappers niet; een Ruby-app op Net::HTTP is veilig en dezelfde app op een typhoeus-adapter niet.
Waarom gopher het scheme is dat ertoe doet voor SSRF
Server-side request forgery wordt meestal omschreven als het manipuleren van een applicatie om een door de aanvaller gekozen URL op te halen. Op zichzelf bereikt dat interne HTTP-services, wat ernstig is maar begrensd. gopher neemt die begrenzing weg. libcurl behandelt een gopher-URL als gopher://host:port/<type><selector>, waarbij het eerste pad-teken het gopher-itemtype is en alles daarna letterlijk als selector wordt verzonden, na URL-decodering, gevolgd door CRLF. Omdat de bytes eerst worden URL-gedecodeerd, wordt een percent-encoded newline in het pad een echte newline op het netwerk. Dat is de complete gadget. Het verandert "haal een URL op" in "schrijf elke willekeurige bytereeks naar elke TCP-poort die de server kan bereiken", en daarom is gopher plus een interne Redis of php-fpm de canonieke route van een blind SSRF naar code execution. Het is een van de scherpere randen van een extern aanvalsoppervlak, en het schuilt volledig in de keuze van de HTTP-client.
De test: een echte socket, geen aanname
De meeste bronnen beweren dat "veel libraries gopher ondersteunen" en gaan direct door naar payloads. We wilden dat het slagen of falen werd waargenomen in plaats van aangenomen, dus de methode is bewust eenvoudig. Voor elke client starten we een one-shot TCP-listener op localhost, vragen we de client om gopher://127.0.0.1:PORT/_<payload> op te halen, en registreren we of de listener ooit een verbinding accepteert en welke bytes er binnenkomen. Een client die het scheme weigert maakt nooit verbinding, dus rapporteert de listener niets. Een client die de socket opent, levert ons exact de bytes op de wire. Dit is een actief experiment tegen onze eigen wegwerpcontainers en een localhost-listener, zonder enig hostsysteem van derden te raken. Hier is de kern daarvan tegen curl, pycurl en PHP ext-curl, over drie verschillende libcurl-versies:
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)
Twee zaken vallen direct af te lezen uit die output. Ten eerste openen de libcurl-clients de socket op elke versie die we hebben geprobeerd, van een oude 7.81.0 tot een huidige 8.21.0, dus dit is geen eigenaardigheid van één specifieke build. Ten tweede is de CRLF-gadget reëel en exact. De pycurl- en PHP-rijen werden opgehaald met de URL-selector _PYCURL%0d%0aLINE2 en _PHPCURL%0d%0aLINE2, en de binnengekomen bytes bevatten een daadwerkelijke carriage-return line-feed tussen de twee tokens. We hebben dit ook geïsoleerd bevestigd: de URL gopher://host:port/_TESTPAYLOAD%0d%0aSECONDLINE komt op de socket aan als b'TESTPAYLOAD\r\nSECONDLINE\r\n'. Dat is een protocolbericht van twee regels dat rechtstreeks in een TCP-service wordt geschreven door niets meer dan een URL.
De matrix
Rijen gemarkeerd met live zijn voor dit artikel opnieuw getest tegen een socket. De rest is geverifieerd op basis van broncode, protocolregisters en de gedocumenteerde scheme-afhandeling van elke client. Het patroon wijkt niet af: de kolom "opent een socket" is exact de set libcurl-bindings, plus één modulaire uitzondering in Perl.
| Client / library | Taal | gopher:// socket | Waarom |
|---|---|---|---|
| curl CLI / libcurl (live) | C | JA | gopher, gophers, dict, ftp, file allemaal meegecompileerd; standaard transfer-allowlist schakelt ze in |
| wget / wget2 (live) | C | nee | alleen http/https/ftp ingebouwd; wget2 is https-only |
PHP ext-curl (curl_exec) (live) | PHP | JA | dunne libcurl-binding; de klassieke SSRF-naar-Redis/FastCGI-sink |
PHP streams (file_get_contents) | PHP | nee | alleen geregistreerde wrappers; geen gopher/dict-wrapper (file en ftp wel) |
| Guzzle (curl-handler, standaard) | PHP | JA | delegeert naar ext-curl; stream-handler doet dat niet |
| pycurl (live) | Python | JA | directe libcurl-binding; hetzelfde risico als PHP curl |
| requests (live) | Python | nee | InvalidSchema: no connection adapters |
| httpx / aiohttp / urllib3 | Python | nee | UnsupportedProtocol / NonHttpUrlClientError / URLSchemeUnknown |
| urllib (stdlib) (live) | Python | nee | geen gopher-opener in Python 3 (file en ftp wel) |
| net/http | Go | nee | unsupported protocol scheme "gopher"; alleen http/https round-trippers |
| HttpURLConnection / HttpClient | Java | nee | handlers zijn alleen http/https/ftp/file/jar/mailto; unknown protocol: gopher |
| Apache HttpClient 4/5 / OkHttp | Java | nee | scheme-register is opzettelijk alleen http/https |
| Net::HTTP | Ruby | nee | specifiek voor http/https; OpenURI stuurt gopher naar File.open (lokaal lezen, geen TCP-pivot) |
| Faraday | Ruby | nee* | op Net::HTTP; de typhoeus (libcurl)-adapter is de uitzondering die gopher bereikt |
| http / https / undici / fetch (live) | Node.js | nee | protocolspecifiek; WHATWG fetch is alleen http/https/file/data/blob |
| HttpClient / HttpWebRequest | .NET | nee | alleen http/https; de verouderde GopherWebRequest is verwijderd in .NET Core |
| reqwest / ureq | Rust | nee | hyper en pure-Rust-stacks, alleen http/https |
| curl crate | Rust | JA | directe libcurl-binding, hetzelfde voorbehoud als elke andere binding |
| LWP::UserAgent | Perl | JA* | alleen als LWP::Protocol::gopher is geïnstalleerd; LWP is modulair per scheme |
Tel de JA-rijen en het verhaal is overduidelijk. Zes client-libraries openen een gopher-socket, en alle zes zijn ze libcurl: curl, PHP ext-curl, pycurl, Guzzle-curl, de Rust curl-crate en Faraday-op-typhoeus. De zevende en enige niet-curl positieve uitkomst is Perl's LWP::UserAgent, en zelfs die vereist dat een beheerder de optionele LWP::Protocol::gopher-plugin heeft geïnstalleerd, dus staat standaard uit. Al het andere, elke gangbare native HTTP-stack in Go, Java, Python, Node, Ruby, .NET en Rust, weigert het scheme voordat er een verbinding wordt geopend. Er is geen slimme payload die dat verandert, omdat de weigering plaatsvindt bij de URL-parser of het adapter-register, ruim vóór enige socket.
Nuance één: redirect naar gopher staat standaard uit
"De app gebruikt curl, dus het is exploiteerbaar" is op een moderne build slechts de halve waarheid, en het ontbrekende deel is waar mensen tijdens een assessment tijd verliezen. Er zijn twee afzonderlijke allowlists in libcurl. De transfer-allowlist regelt een URL die direct wordt meegegeven, en staat gopher nog steeds toe. De redirect-allowlist bepaalt naar welke schemes een Location:-header mag overschakelen, en sinds libcurl 7.65.2 in mei 2019 is deze ingeperkt tot uitsluitend http, https, ftp en ftps. Een open-redirect die wijst naar gopher:// wordt standaard dus geweigerd. We zagen dit gebeuren op 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'
De redirect bereikt gopher alleen als de applicatie zelf de redirect-protocollen opnieuw heeft verruimd, door CURLOPT_REDIR_PROTOCOLS_STR in te stellen met gopher of door de CLI --proto-redir all mee te geven. Die misconfiguratie is niet hypothetisch. CVE-2026-33752 is exact deze categorie kwetsbaarheid: de curl_cffi-library volgde redirects naar willekeurige protocollen omdat het de redirect-allowlist niet beperkte, waardoor een eenvoudige http-SSRF weer veranderde in een volledige gopher-pivot. De praktische les voor een aanvaller is dat je bij een actueel libcurl-doelwit het volledige scheme moet kunnen plaatsen, een directe gopher://-URL, in plaats van te vertrouwen op een open-redirect. De praktische les voor een verdediger is dat het verruimen van redirect-protocollen een geladen wapen is, en de standaardinstelling die dit ontwapent is al sinds 2019 correct.
Nuance twee: de payload is CRLF, en het is triviaal
De reden dat gopher belangrijker is dan dict:// of file:// is de controle op byteniveau. libcurl verzendt het selector-gedeelte van de URL als raw bytes na URL-decodering, waardoor de aanvaller het netwerkverkeer samenstelt in het URL-pad. Een percent-encoded %0d%0a wordt een echte carriage-return line-feed, en elk regelgeoriënteerd protocol verwerkt het resultaat als afzonderlijke commando's:
- Redis. Een enkele gopher-URL kan
SET, vervolgensCONFIG SET direnCONFIG SET dbfilename, en daarnaSAVEverzenden, waarmee een webshell, een cron-taak of een authorized_keys-bestand naar schijf wordt geschreven. Dit is de chain die openbare payload-generators genereren. - FastCGI (php-fpm op 9000). Een geprepareerd FastCGI-record met
PHP_VALUEofSCRIPT_FILENAMEleidt tot code execution tegen een php-fpm die alleen op localhost luistert. - memcached, SMTP, Postgres, Zabbix. Elk regelgeoriënteerd of length-prefixed protocol dat bereikbaar is vanaf de SSRF-oorsprong is een kandidaat, omdat je de bytes volledig controleert.
Niets hiervan vereist een geheugenfout of een race condition. Het is een URL die decodeert naar een protocolconversatie, en daarom maakt een client met gopher-ondersteuning van een bescheiden SSRF een ernstige kwetsbaarheid. Ditzelfde principe, een agent of service die een URL ophaalt die jij beïnvloedt, maakt machine-clients die verbinding maken met willekeurige endpoints het controleren waard op dezelfde klasse van pivots.
Wat hiertegen te doen
De verdediging is niet "stop met het gebruik van curl." libcurl functioneert prima wanneer het protocolbereik expliciet is vastgezet. De fout zit in het vertrouwen op standaardinstellingen en het toestaan dat gebruikersinvoer het scheme bepaalt.
- Zet de scheme-allowlist expliciet vast. Stel voor libcurl, ext-curl en pycurl
CURLOPT_PROTOCOLS_STR = "http,https"in en verruimCURLOPT_REDIR_PROTOCOLS_STRnooit verder danhttp,https. Op de CLI:--proto -all,http,https --proto-redir -all,http,https. - Laat gebruikersinvoer nooit het URL-scheme bepalen. Accepteer een host en pad, hardcode
https://, en weiger al het andere. De meeste SSRF-naar-gopher chains stranden hier. - Behandel een redirect naar een niet-http-scheme als een fatale fout, en valideer het geresolvde scheme na elke redirect-stap opnieuw aan de serverzijde, niet alleen op de initiële URL.
- Kies bij voorkeur voor een http-only client voor verzoeken die geen meerdere protocollen vereisen. Go
net/http, JavaHttpClient, en Pythonrequestsofhttpxbieden standaard immuniteit tegen gopher, dict en file. - Verwijder ongecodeerde en gecodeerde CRLF (
%0d,%0a) uit elk door gebruikers beïnvloed uitgaand URL-pad of query. - Pas egress-filtering toe op de netwerklaag zodat zelfs een succesvolle pivot geen interne service kan bereiken: blokkeer 6379, 11211, 9000, 25 en 5432 voor uitgaand verkeer vanaf de applicatie, en bind interne services aan localhost met authenticatie.
- Controleer de indirecte libcurl-gebruikers die verborgen zitten in je stack: Guzzle's curl-handler, Faraday op typhoeus, de Rust curl-crate en Perl LWP met de gopher-plugin hebben hetzelfde risicoprofiel als PHP curl, en worden gemakkelijk over het hoofd gezien in een dependency graph.
Methode en beperkingen, zodat de cijfers betrouwbaar zijn
De dataset is van onszelf, opgebouwd door sockets te observeren, en is reproduceerbaar.
- Hoe we hebben getest. De live-rijen zijn uitgevoerd in wegwerpcontainers (php:8.3-cli, python:3.12-slim) en op een standaard Linux-host, tegen een localhost
python3TCP-listener die de verbinding en de eerste ontvangen bytes registreert. Drie libcurl-versies zijn onderzocht: 7.81.0, 8.14.1 en 8.21.0. - Wat "nee" betekent. Een "nee" staat voor een client die de socket nooit opent, omdat deze het scheme weigert bij de URL-parser, het adapter-register of de protocol-handler. Dat is veiliger dan een verbinding die wordt geopend en direct gesloten, omdat de bytes van de aanvaller het proces nooit verlaten.
- De rijen met een asterisk. Faraday en Perl LWP zijn afhankelijk van de geconfigureerde adapter of geïnstalleerde plugin, vandaar de asterisk: de standaardconfiguratie is veilig, een specifieke configuratie niet.
- Scope. Dit onderzoek gaat over de vraag of een socket opent voor gopher, wat bepalend is voor de SSRF-pivot. Het zegt niets over SSRF via http, wat elke geteste client hier uitvoert, noch over
file://-blootstelling, wat Java'sURLen enkele andere nog steeds vertonen.
Er is op geen enkel moment contact opgenomen met hosts van derden. Elke verbinding in dit onderzoek was gericht op een listener die we zelf op loopback hebben gestart, wat precies het verschil is tussen het meten van gedrag en het scannen van andermans systemen.
Veelgestelde vragen
Ondersteunt Python's requests-library het gopher-protocol?
Nee. In een live test genereert requests een InvalidSchema: No connection adapters were found voor een gopher-URL en opent het nooit een socket. Hetzelfde geldt voor httpx, aiohttp, urllib3, en de standaardlibrary urllib. De enige Python-uitzondering is pycurl, een dunne binding over libcurl, die wel een gopher-socket opent. Als je Python-code requests of httpx gebruikt, kan een gopher-SSRF-payload via die weg geen interne service bereiken.
Waarom ondersteunt curl gopher wel, maar wget niet?
Verschillende protocolsets. libcurl compileert standaard gopher, gophers, dict, ftp en file mee, en de transfer-allowlist schakelt deze in. wget is uitsluitend gebouwd voor http, https en ftp, en wget2 is https-only. Dit is een verschil in compileeropties en ontwerp, geen runtime-schakelaar, en daarom is curl het klassieke SSRF-pivotpunt en wget niet.
Welke protocollen ondersteunt libcurl voor SSRF?
Op een standaardbuild kan libcurl naast http en https ook gopher, gophers, dict, ftp, ftps, file, tftp, ldap en de mailprotocollen openen. Voor SSRF zijn gopher en dict het belangrijkst, omdat beide de aanvaller willekeurige bytes op een raw socket laten plaatsen. gopher is het krachtigst, omdat de selector na het itemtype-teken letterlijk wordt verzonden, waardoor een gecodeerde CRLF een echte regelafbreking wordt.
Hoe escaleert het gopher-protocol een SSRF naar RCE?
Een gewone SSRF zorgt ervoor dat de server een http-URL ophaalt. gopher zet dat om in het verzenden van willekeurige bytes naar elke bereikbare TCP-poort, doordat libcurl de selector URL-decodeert en raw verstuurt. Een enkele URL kan een Redis SET plus CONFIG SET dir en SAVE verzenden om een webshell te plaatsen, of een FastCGI-record naar php-fpm sturen. De SSRF is het transport, gopher is de payload, en een regelgeoriënteerde interne service is het doelwit.
Ondersteunt Java's HttpURLConnection gopher?
Nee. java.net.URL en HttpURLConnection registreren uitsluitend handlers voor http, https, ftp, file, jar en mailto, en een gopher-URL levert een unknown protocol: gopher op. Het modernere HttpClient, Apache HttpClient en OkHttp weigeren elk niet-http-scheme opzettelijk. Java is in de praktijk immuun voor gopher-SSRF, al ondersteunt java.net.URL nog steeds file://, wat een afzonderlijk risico vormt.
Waarom volgt libcurl geen redirect naar een gopher://-URL?
Sinds libcurl 7.65.2 in mei 2019 is de standaard redirect-allowlist beperkt tot http, https, ftp en ftps, waardoor een 302 naar gopher:// wordt geweigerd en er geen socket opent. Directe invoer van een volledige gopher-URL werkt nog steeds, omdat de transfer-allowlist afzonderlijk functioneert. Een applicatie kan het redirect-pad heropenen door CURLOPT_REDIR_PROTOCOLS_STR te verruimen, wat precies de kwetsbaarheid was achter CVE-2026-33752 in curl_cffi. Geef op een modern doelwit het volledige gopher-scheme op in plaats van te vertrouwen op een open-redirect.
Hoe stuur je CRLF naar Redis via een gopher-SSRF-payload?
Codeer de newlines. libcurl URL-decodeert de selector, dus gopher://127.0.0.1:6379/_SET%20k%20v%0d%0aSAVE%0d%0aQUIT verzendt SET k v, een echte CRLF, SAVE, een echte CRLF, en daarna QUIT. In onze test kwam _TESTPAYLOAD%0d%0aSECONDLINE binnen als de bytes TESTPAYLOAD\r\nSECONDLINE\r\n. Redis gebruikt een door regeleinden gescheiden protocol, waardoor dit een geldige commandoreeks vormt; daarom is gopher plus Redis de klassieke SSRF-naar-RCE-keten.
Aanbevolen artikelen
- Wat is een extern aanvalsoppervlak? Waar een SSRF zich bevindt binnen het overzicht dat een aanvaller opstelt van wat jouw organisatie exposeert.
- Beveiliging van externe MCP-servers: de helft van het register is andermans server. Hetzelfde patroon van "een client haalt een URL op die jij beïnvloedt", op het niveau van machine-agents.
- Check versus scan versus pentest. Waarom een gecombineerde bevinding zoals SSRF-naar-gopher-naar-RCE precies is wat een echte test aan het licht brengt en een scanner mist.
Zou een gopher-pivot werken tegen jouw omgeving?
Onze check van $100 brengt jouw daadwerkelijke externe aanvalsoppervlak in kaart zoals een aanvaller dat doet, binnen een scope waarvan schriftelijk is vastgesteld dat jij de eigenaar bent en waarvoor toestemming is verleend, met toelichting door een senior specialist. Of een interne Redis of php-fpm één SSRF verwijderd is van code execution is precies het soort keten waarnaar we zoeken.
Boek een check van $100