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
Hi Xavier,
thanks for the RFC
Le jeu. 12 nov. 2026 à 11:45, Xavier Leune xavier.leune+internals@gmail.com
a écrit :
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_opensocketfunctionIt 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
As I put on the PR, I'd drop the enum and use an int port:
function (CurlHandle $handle, string $address, int $port): bool
The address already tells IPv4, IPv6 and UNIX sockets apart, and
filter_var() rejects a socket path anyway. The enum can even mislead:
::ffff:127.0.0.1 is reported as IpV6.
For UNIX sockets, port 0 is what CURLOPT_PREREQFUNCTION and
CURLINFO_PRIMARY_PORT already report/. Using null would be inconsistent and
thus surprizing.
Cheers,
Nicolas