[php-src] PR #25414: [RFC] ext/curl: Add CURLOPT_OPENSOCKETFUNCTION
| From: | xavierleune | Date: | Thu, 12 Nov 2026 10:22:59 +0000 |
| Subject: | [php-src] PR #25414: [RFC] ext/curl: Add CURLOPT_OPENSOCKETFUNCTION | ||
| Groups: | php.git-pulls | ||
| Request: | Send a blank email to git-pulls+get-40572@lists.php.net to get a copy of this message | ||
Pull Request: https://github.com/php/php-src/pull/25414
Author: xavierleune
## Description
RFC: https://wiki.php.net/rfc/curl_opensocketfunction
(draft)
Discussion: "SSRF protection in curl", internals@lists.php.net
Supersedes #23657.
This adds
CURLOPT_OPENSOCKETFUNCTION, with libcurl's name and value. Its callback
allows or refuses each socket libcurl is about to create, after DNS resolution and before
connect():
```php
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: on the address actually about to be
contacted, for every redirect hop and every address libcurl tries, whatever spelling the URL used
and whatever DNS answered.
### Semantics
- $handle comes first, as in the other ext/curl callbacks. curl_getinfo($handle,
CURLINFO_EFFECTIVE_URL) tells which URL the address is for, redirects included.
- $family is a case of the new CurlAddressFamily enum: IpV4,
IpV6 or Unix.
- $address is the literal IP address, or the socket path for Unix. An
abstract socket name keeps its leading NUL byte.
- $port is null for Unix.
- When the callback returns:
- true: ext/curl creates the socket with socket(family, socktype,
protocol), which is what libcurl documents as the hook's default behaviour. Everything
libcurl does next is unchanged.
- false: the connection is refused. libcurl tries the next address of the host, if
any, and the transfer fails with CURLE_COULDNT_CONNECT once none is left.
- Any other value: a TypeError is raised and the connection is refused.
- An exception: the connection is refused and the exception propagates.
- Some endpoints cannot be described to the callback: other address families, other socket purposes,
and AF_UNIX on Windows. These are refused without calling it.
- The option behaves like the other callable options:
- null removes it;
- curl_reset() clears it;
- curl_copy_handle() and clone carry it over;
- it is reported to the cycle collector.
- The callback is invoked for each socket created. A connection reused from the pool of the handle,
of its multi handle, or of a share is not vetted again. CURLOPT_PREREQFUNCTION runs
before every request, reused connections included. UPGRADING documents this, along with proxies and
CURLOPT_PROTOCOLS_STR.
No descriptor is ever exposed to userland, and ext/curl gains no dependency on ext/sockets. The
return type can later be widened, and parameters appended, without breaking existing callbacks.
### Tests
- Allow and refuse, including with curl_multi.
- Redirects.
- Every address family, including IPv4-mapped IPv6 and UNIX and abstract sockets.
- file://, which opens no socket.
- Connection reuse, CURLOPT_FRESH_CONNECT and CURLOPT_PREREQFUNCTION.
These use a small keep-alive server, keepalive_server.inc, since the CLI server closes
every connection.
- Copy, clone and reset, and a callback that removes itself.
- Invalid callables and return values, and exceptions.
- Cycle collection, and freeing a handle while its connection stays pooled.
### 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.