GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — [RFC] ext/curl: Add CURLOPT_OPENSOCKETFUNCTION by xavierleune · Pull Request #25414 · php/php-src

Conversation

@xavierleune

@xavierleune xavierleune commented Nov 12, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown

Description

RFC: https://wiki.php.net/rfc/curl_opensocketfunction (under discussion)
Discussion: "SSRF protection in curl", [email protected]
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():

curl_setopt($ch, CURLOPT_OPENSOCKETFUNCTION,
    function (CurlHandle $handle, string $address, int $port): bool {
        // Only publicly routable addresses; a UNIX socket path is not an IP.
        return 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.
  • $address is the literal IP address, or the path of a UNIX socket. An abstract socket name keeps its leading NUL byte.
  • $port is 0 for a UNIX socket, as CURLOPT_PREREQFUNCTION and CURLINFO_PRIMARY_PORT report it.
  • 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, and libcurl trying the next address after a refusal.
  • Exceptions, including one thrown by a destructor after the callback returned true.
  • 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 implements the same API for PHP 8.3 to 8.6. It stays inert on a PHP that provides the option natively.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@xavierleune
xavierleune requested review from adoy and kocsismate as code owners November 12, 2026 10:22
@github-actions github-actions Bot added the Extension: curl label Nov 12, 2026
@xavierleune xavierleune mentioned this pull request Nov 12, 2026
ext/curl: add CURLOPT_PRECONNECTFUNCTION to vet outgoing connections #23657
Closed
@nicolas-grekas

nicolas-grekas commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

I'd drop the enum and use an int port, as in #25410:

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. Without a new type, I'm not sure this still needs an RFC. And the family can still be added at the end later if someone needs it.

On the implementation:

  • sun is a predefined macro on Solaris and illumos, so the build breaks there. sa_un would do.
  • calling php_set_sock_blocking() after socket() is a good catch, I'll take it in ext/curl: Add CURLOPT_OPENSOCKETFUNCTION #25410 :)

I opened #25410 before seeing this PR, with you as the commit author since it reuses your code. I'm fine closing either one, as long as we agree on the simpler signature. WDYT?

(UPGRADING entries for curl are usually one paragraph, see CURLOPT_SEEKFUNCTION)

xavierleune reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@xavierleune
ext/curl: Add CURLOPT_OPENSOCKETFUNCTION
30997a5
Lets a callback allow or refuse each socket libcurl is about to create,
after DNS resolution and before connect():

    function (CurlHandle $handle, string $address, int $port): bool

$address is the literal IP address, or the path of a UNIX domain
socket, whose $port is 0, as CURLINFO_PRIMARY_PORT reports. Returning
true creates the socket as libcurl would by default; false refuses the
connection, and libcurl tries the next address, if any. Any other
return value raises a TypeError. Endpoints that cannot be described to
the callback (other address families, other socket purposes) are
refused without calling it.

RFC: https://wiki.php.net/rfc/curl_opensocketfunction
@xavierleune
xavierleune force-pushed the feature/curl-opensocketfunction-8.7 branch from e7ef9d6 to 30997a5 Compare November 12, 2026 12:43
xavierleune added a commit to xavierleune/curl-hardening that referenced this pull request Nov 12, 2026
@xavierleune
Drop CurlAddressFamily, follow the RFC 0.2 signature
210bec6
The callback is now function (CurlHandle $handle, string $address,
int $port): bool, as in php/php-src#25414. A UNIX socket is reported
with port 0, as CURLINFO_PRIMARY_PORT does.
nicolas-grekas
nicolas-grekas reviewed Nov 12, 2026
View reviewed changes

@nicolas-grekas nicolas-grekas left a comment

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update, the code now matches #25410 so I'm closing mine.

The description still shows the enum and ?int $port.

The test that false makes libcurl try the next address might be worth taking from #25410. It uses CURLOPT_RESOLVE with two IPs, so it doesn't need a server: curl_opensocketfunction_next_address.phpt

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

xavierleune reacted with thumbs up emoji
All reactions
  • 👍 1 reaction
Comment thread ext/curl/interface.c Resolved

Uh oh!

There was an error while loading. Please reload this page.

@nicolas-grekas nicolas-grekas mentioned this pull request Nov 12, 2026
ext/curl: Add CURLOPT_OPENSOCKETFUNCTION #25410
Closed
@xavierleune @nicolas-grekas
ext/curl: Refuse the connection when the opensocket callback leaves a…
a3d8a16
…n exception

An exception thrown after the callback returned true, e.g. by a destructor,
left both true and a pending exception, so the socket was still created.

Also take the test that refusing an address makes libcurl try the next one
from php#25410.

Co-authored-by: Nicolas Grekas <[email protected]>
@xavierleune
xavierleune force-pushed the feature/curl-opensocketfunction-8.7 branch from e2ecad9 to a3d8a16 Compare November 12, 2026 16:16
@xavierleune

xavierleune commented Nov 12, 2026

Copy link
Copy Markdown
Author

Thanks @nicolas-grekas, I've taken the test from #25410 (and co-authored the commit). Description has been updated too !

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@kocsismate kocsismate Awaiting requested review from kocsismate kocsismate is a code owner
@adoy adoy Awaiting requested review from adoy adoy is a code owner
1 more reviewer
@nicolas-grekas nicolas-grekas nicolas-grekas left review comments
Reviewers whose approvals may not affect merge requirements

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

Extension: curl

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

2 participants

@xavierleune @nicolas-grekas
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.