[php-src] PR #25414: [RFC] ext/curl: Add CURLOPT_OPENSOCKETFUNCTION

From: 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.

« previous php.git-pulls (#40572) next »