If your app fetches URLs that users supply, such as webhooks, link previews or image proxies, an attacker can point it at addresses it should never reach: 127.0.0.1, a cloud metadata endpoint like 169.254.169.254, or a service on your internal network. This is called Server-Side Request Forgery (SSRF). Xavier Leune's RFC adds CURLOPT_OPENSOCKETFUNCTION to ext/curl, so you can allow or refuse each connection based on the address cURL is actually about to contact.
Show me
You register a callback that returns true to allow a connection and false to refuse it:
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; });
Why today's workarounds fall short
The check has to happen after DNS lookup and before the connection opens, and ext/curl has no hook there today. So libraries either resolve the host themselves and pin the result with CURLOPT_RESOLVE, as Symfony's NoPrivateNetworkHttpClient does, or they parse the URL and check the host. The first duplicates cURL's own lookup and has to follow every redirect by hand. The second misses spellings cURL accepts for the same address, like 2130706433 or 127.1, and fails against DNS rebinding, where the name resolves to a different address when cURL connects.
How it works
The callback runs once for every socket cURL is about to open. That covers each address a host resolves to and every redirect hop that opens a new connection. It gets the handle, a new CurlAddressFamily enum (IpV4, IpV6 or Unix), the address as a string, and the port (null for Unix sockets).
- Returning
falsemakes cURL try the next address. When none are left, the transfer fails withCURLE_COULDNT_CONNECT. - An exception refuses the connection and comes out of
curl_exec(). - A connection reused from cURL's pool opens no socket, so the callback isn't called again.
CURLOPT_PREREQFUNCTIONcovers that case. - With a proxy, the callback sees the proxy's address.
The option takes libcurl's name, but returns a bool instead of a socket. Earlier proposals that returned a socket raised concerns about depending on ext/sockets and about who owns the socket.
What it means for existing code
Nothing changes unless you set the option. The new CURLOPT_OPENSOCKETFUNCTION constant and CurlAddressFamily enum are global, so your own declarations of those names would conflict. The author found none outside his polyfill, curl_hardening, which offers the same API on PHP 8.3 to 8.6.
Where it stands
Xavier Leune opened the internals discussion on November 12, 2026, after an earlier thread on SSRF protection in cURL. It targets PHP 8.7, and pull request #25414 has the implementation. The same protection for file_get_contents() and other stream functions is left for a separate RFC. Acceptance needs a two-thirds majority. No vote has been scheduled yet.