[RFC] Vetting cURL connections with CURLOPT_OPENSOCKETFUNCTION
| From: | Xavier Leune | Date: | Thu, 12 Nov 2026 10:42:54 +0000 |
| Subject: | [RFC] Vetting cURL connections with CURLOPT_OPENSOCKETFUNCTION | ||
| Groups: | php.internals | ||
| Request: | Send a blank email to internals+get-133085@lists.php.net to get a copy of this message | ||
Hi internals,
Following the "SSRF protection in curl" thread, I'd like to start the
discussion on this RFC: https://wiki.php.net/rfc/curl_opensocketfunction
It adds CURLOPT_OPENSOCKETFUNCTION to ext/curl, with libcurl's name and
value. The callback is invoked after DNS resolution and before connect(),
once for each socket libcurl is about to create. It returns a bool that
allows or refuses the connection:
curl_setopt($ch, CURLOPT_OPENSOCKETFUNCTION,
function (CurlHandle $handle, CurlAddressFamily $family, string
$address, ?int $port): bool {
// Only publicly routable addresses.
return $family !== CurlAddressFamily::Unix
&& filter_var($address, FILTER_VALIDATE_IP,
FILTER_FLAG_GLOBAL_RANGE) !== false;
});
This is the point where an SSRF policy can be enforced reliably. It applies
to the address actually about to be contacted, on every redirect hop and
for every address libcurl tries, whatever spelling the URL used and
whatever DNS answered.
Compared to the earlier proposals:
- It returns a bool, as Sjoerd suggested in the thread. No socket is
exposed to userland, and ext/curl gains no dependency on ext/sockets.
- It keeps libcurl's option name, rather than a PHP-specific one.
- The handle comes first, as in the other ext/curl callbacks. A new
CurlAddressFamily enum (IpV4, IpV6, Unix) tells a policy whether $address
is an IP or a socket path. $port is null for UNIX sockets.
- The return type can later be widened, and parameters appended, without
breaking existing callbacks.
The implementation is in https://github.com/php/php-src/pull/25414,
which
supersedes GH-23657.
A polyfill, curl_hardening (https://github.com/xavierleune/curl-hardening),
implements the same API for PHP 8.3 to 8.6. It stays inert on a PHP that
provides the option natively.
As discussed with Levi, streams are left to a separate RFC (see Future
Scope).
Thanks to everyone who took part in the previous thread and reviewed the
PRs. Feedback is welcome.
Regards,
Xavier