Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ PHP NEWS
. Set content length using CURLOPT_POSTFIELDSIZE_LARGE instead of
CURLOPT_POSTFIELDSIZE. This makes it possible to post strings larger than
2GB on some platforms, e.g. Windows. (Sjoerd Langkemper)
. Added CURLOPT_PRECONNECTFUNCTION to allow or refuse each connection libcurl
is about to make. (Xavier Leune)

- DOM:
. Fixed a typo in the DOMException message for INUSE_ATTRIBUTE_ERR.
Expand Down
22 changes: 22 additions & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,24 @@ PHP 8.6 UPGRADE NOTES
failing with CURLE_SEND_FAIL_REWIND. The callback receives the CurlHandle,
offset and origin, and must return one of CURL_SEEKFUNC_OK,
CURL_SEEKFUNC_FAIL or CURL_SEEKFUNC_CANTSEEK.
. Added CURLOPT_PRECONNECTFUNCTION to register a callback that allows or
refuses each socket libcurl is about to create, after DNS resolution and
before connect():

function (CurlHandle $handle, ?string $ip, int $port,
CurlAddressFamily $family): bool

Returning false refuses the connection, and the transfer fails with
CURLE_COULDNT_CONNECT; any other return type raises a TypeError. $ip is
null for a UNIX domain socket. null as the option value restores libcurl's
own socket creation.

It is not on its own a complete SSRF defence: the callback is invoked per
socket created, so a connection reused from the pool is not seen by it;
with a proxy it sees the proxy, not the target; CURLOPT_DOH_URL connections
and schemes that open no socket (file:// in particular) bypass it entirely.
Combine it with CURLOPT_PROTOCOLS_STR and CURLOPT_REDIR_PROTOCOLS_STR, and
do not share CURL_LOCK_DATA_CONNECT between handles vetted differently.

- Date:
. Added a new Time\Duration class.
Expand Down Expand Up @@ -810,6 +828,9 @@ PHP 8.6 UPGRADE NOTES
7. New Classes and Interfaces
========================================

- Curl:
. enum CurlAddressFamily

- Date:
. Time\Duration
RFC: https://wiki.php.net/rfc/duration_class
Expand Down Expand Up @@ -886,6 +907,7 @@ PHP 8.6 UPGRADE NOTES
- Curl:
. CURLINFO_SIZE_DELIVERED (libcurl >= 8.20.0).
. CURLOPT_SEEKFUNCTION.
. CURLOPT_PRECONNECTFUNCTION.
. CURL_SEEKFUNC_OK.
. CURL_SEEKFUNC_FAIL.
. CURL_SEEKFUNC_CANTSEEK.
Expand Down
17 changes: 17 additions & 0 deletions ext/curl/curl.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -3739,6 +3739,23 @@
*/
const CURLOPT_SAFE_UPLOAD = UNKNOWN;

/**
* @var int
* @cvalue CURLOPT_PRECONNECTFUNCTION
*/
const CURLOPT_PRECONNECTFUNCTION = UNKNOWN;

/**
* The address family of the endpoint libcurl is about to connect to, as reported
* to the CURLOPT_PRECONNECTFUNCTION callback.
*/
enum CurlAddressFamily
{
case Inet;
case Inet6;
case Unix;
}

/**
* @strict-properties
* @not-serializable
Expand Down
17 changes: 16 additions & 1 deletion ext/curl/curl_arginfo.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions ext/curl/curl_private.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@

#define CURLOPT_RETURNTRANSFER 19913
#define CURLOPT_BINARYTRANSFER 19914 /* For Backward compatibility */
/* PHP-specific option: the callback returns a bool rather than libcurl's socket,
* so it does not share CURLOPT_OPENSOCKETFUNCTION's name or number. */
#define CURLOPT_PRECONNECTFUNCTION 19915
#define PHP_CURL_STDOUT 0
#define PHP_CURL_FILE 1
#define PHP_CURL_USER 2
Expand Down Expand Up @@ -79,6 +82,7 @@ typedef struct {
zend_fcall_info_cache xferinfo;
zend_fcall_info_cache fnmatch;
zend_fcall_info_cache debug;
zend_fcall_info_cache preconnect;
#if LIBCURL_VERSION_NUM >= 0x075000 /* Available since 7.80.0 */
zend_fcall_info_cache prereq;
#endif
Expand Down
162 changes: 162 additions & 0 deletions ext/curl/interface.c
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@

#ifdef PHP_WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#include <sys/types.h>
#else
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#endif

#include <curl/curl.h>
Expand All @@ -36,6 +41,8 @@
#include "ext/standard/info.h"
#include "ext/standard/file.h"
#include "ext/standard/url.h"
#include "ext/standard/php_net.h"
#include "Zend/zend_enum.h"
#include "curl_private.h"

#ifdef __GNUC__
Expand Down Expand Up @@ -232,6 +239,7 @@ PHP_GSHUTDOWN_FUNCTION(curl)
zend_class_entry *curl_ce;
zend_class_entry *curl_share_ce;
zend_class_entry *curl_share_persistent_ce;
static zend_class_entry *curl_address_family_ce;
static zend_object_handlers curl_object_handlers;

static zend_object *curl_create_object(zend_class_entry *class_type);
Expand Down Expand Up @@ -397,6 +405,8 @@ PHP_MINIT_FUNCTION(curl)
curl_share_persistent_ce = register_class_CurlSharePersistentHandle();
curl_share_persistent_register_handlers();

curl_address_family_ce = register_class_CurlAddressFamily();

curlfile_register_class();

return SUCCESS;
Expand Down Expand Up @@ -499,6 +509,10 @@ static HashTable *curl_get_gc(zend_object *object, zval **table, int *n)
zend_get_gc_buffer_add_fcc(gc_buffer, &curl->handlers.debug);
}

if (ZEND_FCC_INITIALIZED(curl->handlers.preconnect)) {
zend_get_gc_buffer_add_fcc(gc_buffer, &curl->handlers.preconnect);
}

#if LIBCURL_VERSION_NUM >= 0x075000 /* Available since 7.80.0 */
if (ZEND_FCC_INITIALIZED(curl->handlers.prereq)) {
zend_get_gc_buffer_add_fcc(gc_buffer, &curl->handlers.prereq);
Expand Down Expand Up @@ -811,6 +825,124 @@ static int curl_ssh_hostkeyfunction(void *clientp, int keytype, const char *key,
}
#endif

/* {{{ curl_sockaddr_port
The port libcurl is about to connect to, in host byte order. */
static zend_long curl_sockaddr_port(const struct sockaddr *sa)
{
switch (sa->sa_family) {
case AF_INET:
return ntohs(((const struct sockaddr_in *) sa)->sin_port);
#ifdef AF_INET6
case AF_INET6:
return ntohs(((const struct sockaddr_in6 *) sa)->sin6_port);
#endif
default:
return 0;
}
}
/* }}} */

/* {{{ curl_preconnectfunction
Registered as libcurl's CURLOPT_OPENSOCKETFUNCTION, but the PHP callback only
allows or refuses the connection: no descriptor is ever exposed to userland. */
static curl_socket_t curl_preconnectfunction(void *clientp, curlsocktype purpose, struct curl_sockaddr *address)
{
php_curl *ch = (php_curl *) clientp;

/* Unreachable in normal operation: libcurl's own socket creation is restored
* whenever no callback is installed, so the trampoline is only registered
* while the FCC is initialized. Refuse rather than connect, so that an
* unexpected state can never silently bypass the filter. */
if (UNEXPECTED(!ZEND_FCC_INITIALIZED(ch->handlers.preconnect))) {
return CURL_SOCKET_BAD;
}

#if PHP_CURL_DEBUG
fprintf(stderr, "curl_preconnectfunction() called\n");
fprintf(stderr, "purpose = %d, family = %d, socktype = %d, protocol = %d\n", purpose, address->family, address->socktype, address->protocol);
#endif

/* Whatever cannot be described to the callback is refused rather than
* connected: a policy must never be bypassed by an endpoint it was not
* shown. CURLSOCKTYPE_IPCXN is the only purpose libcurl currently uses; a
* future one may well not be a destination address at all. */
if (UNEXPECTED(purpose != CURLSOCKTYPE_IPCXN)) {
return CURL_SOCKET_BAD;
}

const char *address_family;
bool numeric_address;

switch (address->family) {
case AF_INET:
address_family = "Inet";
numeric_address = true;
break;
#ifdef AF_INET6
case AF_INET6:
address_family = "Inet6";
numeric_address = true;
break;
#endif
#ifdef AF_UNIX
case AF_UNIX:
address_family = "Unix";
numeric_address = false;
break;
#endif
default:
return CURL_SOCKET_BAD;
}

zval args[4];
zval retval;
curl_socket_t rval = CURL_SOCKET_BAD;

if (numeric_address) {
zend_string *ip = php_inet_ntop(&address->addr);
if (UNEXPECTED(ip == NULL)) {
return CURL_SOCKET_BAD;
}
ZVAL_STR(&args[1], ip);
ZVAL_LONG(&args[2], curl_sockaddr_port(&address->addr));
} else {
/* A UNIX domain socket (CURLOPT_UNIX_SOCKET_PATH or
* CURLOPT_ABSTRACT_UNIX_SOCKET) has no address to report: null rather
* than an empty string, so a policy has to handle the case. */
ZVAL_NULL(&args[1]);
ZVAL_LONG(&args[2], 0);
}

GC_ADDREF(&ch->std);
ZVAL_OBJ(&args[0], &ch->std);
ZVAL_OBJ_COPY(&args[3], zend_enum_get_case_cstr(curl_address_family_ce, address_family));

ch->in_callback = true;
zend_call_known_fcc(&ch->handlers.preconnect, &retval, /* param_count */ 4, args, /* named_params */ NULL);
ch->in_callback = false;

/* retval is undefined when the callback threw: the connection is refused and
* the exception propagates out of curl_exec(). */
if (!Z_ISUNDEF(retval)) {
_php_curl_verify_handlers(ch, /* reporterror */ true);
if (EXPECTED(Z_TYPE(retval) == IS_TRUE || Z_TYPE(retval) == IS_FALSE)) {
if (Z_TYPE(retval) == IS_TRUE) {
rval = socket(address->family, address->socktype, address->protocol);
}
} else {
zend_type_error("The CURLOPT_PRECONNECTFUNCTION callback must return a bool");
}
zval_ptr_dtor(&retval);
}

zval_ptr_dtor(&args[0]);
zval_ptr_dtor(&args[1]);
zval_ptr_dtor(&args[3]);

return rval;
}
/* }}} */

/* {{{ curl_read */
static size_t curl_read(char *data, size_t size, size_t nmemb, void *ctx)
{
Expand Down Expand Up @@ -1134,6 +1266,7 @@ void init_curl_handle(php_curl *ch)
ch->handlers.progress = empty_fcall_info_cache;
ch->handlers.xferinfo = empty_fcall_info_cache;
ch->handlers.fnmatch = empty_fcall_info_cache;
ch->handlers.preconnect = empty_fcall_info_cache;
ch->handlers.debug = empty_fcall_info_cache;
#if LIBCURL_VERSION_NUM >= 0x075000 /* Available since 7.80.0 */
ch->handlers.prereq = empty_fcall_info_cache;
Expand Down Expand Up @@ -1306,6 +1439,7 @@ void _php_setup_easy_copy_handlers(php_curl *ch, php_curl *source)
php_curl_copy_fcc_with_option(ch, CURLOPT_XFERINFODATA, &ch->handlers.xferinfo, &source->handlers.xferinfo);
php_curl_copy_fcc_with_option(ch, CURLOPT_FNMATCH_DATA, &ch->handlers.fnmatch, &source->handlers.fnmatch);
php_curl_copy_fcc_with_option(ch, CURLOPT_DEBUGDATA, &ch->handlers.debug, &source->handlers.debug);
php_curl_copy_fcc_with_option(ch, CURLOPT_OPENSOCKETDATA, &ch->handlers.preconnect, &source->handlers.preconnect);
#if LIBCURL_VERSION_NUM >= 0x075000 /* Available since 7.80.0 */
php_curl_copy_fcc_with_option(ch, CURLOPT_PREREQDATA, &ch->handlers.prereq, &source->handlers.prereq);
#endif
Expand Down Expand Up @@ -1677,6 +1811,27 @@ static zend_result _php_curl_setopt(php_curl *ch, zend_long option, zval *zvalue
HANDLE_CURL_OPTION_CALLABLE(ch, CURLOPT_FNMATCH_, handlers.fnmatch, curl_fnmatch);
HANDLE_CURL_OPTION_CALLABLE(ch, CURLOPT_DEBUG, handlers.debug, curl_debug);

case CURLOPT_PRECONNECTFUNCTION: {
bool installed = php_curl_set_callable_handler(&ch->handlers.preconnect, zvalue, is_array_config, "CURLOPT_PRECONNECTFUNCTION");
if (!installed || !ZEND_FCC_INITIALIZED(ch->handlers.preconnect)) {
/* Restore libcurl's own socket creation, so that the trampoline
* is registered if and only if the FCC is initialized. A
* rejected callable also releases the previous handler, so
* leaving the option armed would point CURLOPT_OPENSOCKETDATA
* at a handle whose FCC is gone, and curl_copy_handle() would
* then carry that pointer into the copy. */
curl_easy_setopt(ch->cp, CURLOPT_OPENSOCKETFUNCTION, NULL);
curl_easy_setopt(ch->cp, CURLOPT_OPENSOCKETDATA, NULL);
if (!installed) {
return FAILURE;
}
break;
}
curl_easy_setopt(ch->cp, CURLOPT_OPENSOCKETFUNCTION, curl_preconnectfunction);
curl_easy_setopt(ch->cp, CURLOPT_OPENSOCKETDATA, ch);
break;
}

#if LIBCURL_VERSION_NUM >= 0x075000 /* Available since 7.80.0 */
HANDLE_CURL_OPTION_CALLABLE(ch, CURLOPT_PREREQ, handlers.prereq, curl_prereqfunction);
#endif
Expand Down Expand Up @@ -2870,6 +3025,9 @@ static void curl_free_obj(zend_object *object)
if (ZEND_FCC_INITIALIZED(ch->handlers.debug)) {
zend_fcc_dtor(&ch->handlers.debug);
}
if (ZEND_FCC_INITIALIZED(ch->handlers.preconnect)) {
zend_fcc_dtor(&ch->handlers.preconnect);
}
#if LIBCURL_VERSION_NUM >= 0x075000 /* Available since 7.80.0 */
if (ZEND_FCC_INITIALIZED(ch->handlers.prereq)) {
zend_fcc_dtor(&ch->handlers.prereq);
Expand Down Expand Up @@ -2962,6 +3120,10 @@ static void _php_curl_reset_handlers(php_curl *ch)
zend_fcc_dtor(&ch->handlers.debug);
}

if (ZEND_FCC_INITIALIZED(ch->handlers.preconnect)) {
zend_fcc_dtor(&ch->handlers.preconnect);
}

#if LIBCURL_VERSION_NUM >= 0x075000 /* Available since 7.80.0 */
if (ZEND_FCC_INITIALIZED(ch->handlers.prereq)) {
zend_fcc_dtor(&ch->handlers.prereq);
Expand Down
1 change: 1 addition & 0 deletions ext/curl/sync-constants.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@

const IGNORED_PHP_CONSTANTS = [
'CURLOPT_BINARYTRANSFER',
'CURLOPT_PRECONNECTFUNCTION',
'CURLOPT_RETURNTRANSFER',
'CURLOPT_SAFE_UPLOAD',
];
Expand Down
Loading
Loading