cURL Hardening im PHP Kontext | 02.03.2021
Schon mal vom Programm "Client for URLs" gehört? Nein? Doch ganz bestimmt. Aber vermutlich eher unter dem Begriff cURL. Eine Vielzahl von Anwendungen implementieren cURL, um damit Daten zu übertragen. Nachdem cURL schon seit ewigen Zeiten in fast allen Linux-Distributionen implementiert wurde, befindet sich cURL seit 2018 auch an Bord von Windows als Teil der Standardbibliotheken.
cURL selber ist eigentlich das Programm, welches du in der Bash ausführen kannst und eine Implementierung der Bibliothek libcurl ist. Und genau diese Bibliothek hat es zu großer Verbreitung geschafft. Unter anderem ist libcurl auch in PHP implementiert worden und steht dort als prozedurale Funktion zur Verfügung.
cURL unterstützt eine Vielzahl an Protokollen weit jenseits von HTTP und HTTPS. Auch neue Technologien wie QUIC oder TLS 1.3 können problemlos genutzt werden. Als Teil des PHP-Cores steht cURL direkt zur Verfügung und muss nicht extra nachgeladen werden. Durch die vielen Möglichkeiten, die cURL bietet, ist es notwendig, beim Einsatz im Webprojekt auf Absicherung zu achten. Auch wenn viele Stunden von Entwicklungsarbeit in die letzten Veröffentlichungen von cURL eingeflossen sind, wird sich in der breiten PHP-Landschaft sicherlich noch die ein oder andere veraltete Implementierung finden, die weiteren Absicherungsbedarf impliziert. Es wird in diesem Zusammenhang von Hardening gesprochen.
Mit diesem Beitrag möchte ich einige Warnschilder aufstellen und dir zeigen, worauf du achten solltest. Wenn du Feedback oder Fragen zum Thema cURL Hardening hast, kannst du dies gerne in die Kommentare packen. Und bevor ich es vergesse: Dieser Beitrag ist in ähnlicher Art zuerst bei PHP.watch erschienen. Wenn du mehr zum Thema PHP erfahren möchtest, dann kannst du gerne mal bei PHP.watch vorbeischauen.
Mit dem Begriff Härten (englisch Hardening) wird beschrieben, wie ein Gerät vor der Ausnutzung von Verwundbarkeiten (Sicherheitslücken, offene Ports etc.) geschützt werden soll. Einem Hacker soll bspw. so möglichst wenige Einfallstore (Angriffsmethoden) zur Verfügung stehen. Und sollte es doch zu einem erfolgreichen Eindringen kommen, soll die Konfiguration des Systems so wenige Freiheiten wie möglich bieten. Oder anders gesagt, wenn der Hacker in einen Teilbereich des Systems vordringt, soll er aus diesem nicht auf andere Bereiche übergreifen können.
Im Hardening werden unterschiedliche Bereiche isoliert betrachtet. Dies fängt im Netzwerk an, geht über den Server und die eingesetzte Software und endet bei den Nutzerdaten. Du hast Lust auf Beispiele? Ein Artikel bei Tarnkappe behandelt zum Beispiel das Hardening von Windows 10 Systemen. Einen Artikel über Hardening von Linux Systemen gibt es bei RootUsers. Einen auf Software zugeschnittenen Artikel über das Hardening von Nextcloud gibt es in deren Dokumentation. Und einen weiter gefassten Ansatz mit dem Schwerpunkt auf das Hardening der persönlichen IT-Nutzung gibt es beim BSI.
cURL unterstützt über 25 verschiedene Protokolle. Durch die Implementierung von libcurl im PHP-Code unterstützt also auch PHP eben jene Anzahl von Protokollen. Neben den bekannten HTTP und HTTPS gibt es natürlich auch weitere bekannte Protokolle wie FTP und FILE, sowie weniger bekannte wie SCP oder LDAP.
Wenn du nun einen cURL-Request auf eine nutzerbasierte URL ausführst - also dein Websitebesucher hat die URL eingegeben und du besuchst diese - kann die Gefahr eines Server-Side Request Forgery drohen. In diesem Fall entspricht die URL nicht dem erwarteten Format (bspw. http://example.com
), sondern ist darauf ausgelegt, sensible Daten auszulesen (bspw. indem aus http://example.com
ein file:///etc/passwd
wird).
Sollten also deine Besucher eine URL eingeben können, musst du in deinem Skript sicherstellen, dass diese nicht ungewollt einen SSRF ermöglicht.
Wie du das machst? Die Klasse UrlSchemeValidator bietet dir die Möglichkeit zu prüfen, ob eine URL eine Webadresse (also HTTP oder HTTPS) ist.
$validator = new \basteyy\UrlSchemeValidator\UrlSchemeValidator();
var_dump($validator->isWebScheme('https://example.com')); // Returns true
var_dump($validator->isWebScheme('file:///etc/passwd')); // Returns false
Eine weitere Möglichkeit, diesem Problem zu begegnen, ist die erlaubten Protokolle von cURL selbst zu limitieren. cURL unterstützt die Option CURLOPT_PROTOCOLS
und bietet so die Möglichkeit, eine Begrenzung vorzunehmen.
curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS | CURLPROTO_HTTP);
In der Standardeinstellung ist CURLOPT_PROTOCOLS
so konfiguriert, dass alle URLs inklusive file://
akzeptiert werden.
cURL verfügt über die Funktionalität, automatisch HTTP-Weiterleitungen zu folgen. Dadurch wird ein Angriffsvektor geöffnet, bei dem eine vom Benutzer bereitgestellte URL eine harmlose URL zu sein scheint. Der Remoteserver gibt jedoch eine HTTP-Umleitungsantwort aus, der cURL dann automatisch folgt. Beispielsweise kann der Benutzer eine gültige https://
URL angeben, der Remoteserver antwortet jedoch mit einer HTTP-Umleitung, die zu file://
, ftp://
oder einem anderen URL-Schema führt, das die Anwendung möglicherweise nicht erwartet. Seit libcurl 7.19.4 sind keine ftp://
und scp://
URLs mehr für Weiterleitungen zulässig. Es gibt jedoch andere Protokolle, die möglicherweise eine Sicherheitslücke öffnen. cURL folgt standardmäßig nicht den Weiterleitungen und erfordert die Aktivierung der Option CURLOPT_FOLLOWLOCATION
. Entwickler aktivieren diese Funktion jedoch oft.
Ein weiterer Angriffsvektor kann darin bestehen, dass der Zielserver mit einer langen Umleitungssequenz oder, schlimmer noch, einer Umleitungsschleife antwortet. PHP bietet intern den Schutz davor, indem der Standardwert für die Option CURLOPT_MAXREDIRS
auf 20 gesetzt wird.
Die PHP-Dokumentation für CURLOPT_FOLLOWLOCATION warnt derzeit davor, dass PHP einer unbegrenzten Anzahl von Weiterleitungen folgt.
Wenn automatische Weiterleitungen aktiviert sind und die Beschränkung für maximale Weiterleitungen aufgehoben ist, wird die Anwendung aufgrund der Synchronität der Standard-cURL-Anforderungen auf unbestimmte Zeit effektiv angehalten.
Es bietet sich an - auch aus Performancegründen - das Limit der Weiterleitungen zu überdenken (einer der Gründe sind Timeouts - lies ein bisschen weiter). Je nach Anwendungszweck solltest du also die Anzahl der zu folgenden Weiterleitungen reduzieren (oder ggf. musst du diese erhöhen).
curl_setopt($ch, CURLOPT_MAXREDIRS, 5);
Standardmäßig erzwingt cURL keine Zeitüberschreitung für eine Anforderung. Die INI-Einstellung default_socket_timeout
von PHP wirkt sich auch nicht auf das Timeout von cURL aus. Solange eine PHP max_execution_time
nicht erreicht wurde oder der Netzwerkstapel des Betriebssystems entscheidet, die Anforderung zu beenden, kann ein böswilliger oder fehlerhafter Remote-Server eine cURL-Anforderung unbegrenzt warten lassen, indem er einfach keine Daten sendet, nachdem er die Anforderung empfangen hat.
Du solltest also unbedingt einen Timeout-Wert festlegen. Diesen gibst du in Sekunden an.
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
Dieses Timeout-Limit gilt dann für den gesamten cURL-Prozess. D.h. wenn die 10 Sekunden aus dem Beispiel gelten nicht für einzelne Anfragen. Bei einer sehr langsamen Antwort in Verbindung mit drei oder vier Weiterleitungen könnte dies zu einem vorzeitigen Abbruch des Requests führen.
Du solltest also in etwa eine Vorstellung davon haben, wie lange deine Requests dauern könnten. Weiterhin ist es hier sinnvoll, wenn du eine Fallbackmöglichkeit umsetzt oder die Rückgabe darauf validierst, ob der Request gegebenenfalls vorzeitig geschlossen wurde.