diff --git a/php-8.0.30-ldap.patch b/php-8.0.30-ldap.patch new file mode 100644 index 0000000..660cb54 --- /dev/null +++ b/php-8.0.30-ldap.patch @@ -0,0 +1,655 @@ +From 8f69248f7631cbd0515604c5d7e68fc6ee0c5b97 Mon Sep 17 00:00:00 2001 +From: Remi Collet +Date: Wed, 26 Feb 2025 17:26:08 +0100 +Subject: [PATCH 1/4] Fix #17776 LDAP_OPT_X_TLS_REQUIRE_CERT can't be + overridden + +(cherry picked from commit 389de7c6bf59e14145e932f4d3c0171803a3ba97) +--- + ext/ldap/ldap.c | 89 ++++++++++++++++++------ + ext/ldap/php_ldap.h | 1 + + ext/ldap/tests/ldap_start_tls_basic.phpt | 21 +++++- + ext/ldap/tests/ldaps_basic.phpt | 55 +++++++++++++++ + 4 files changed, 141 insertions(+), 25 deletions(-) + create mode 100644 ext/ldap/tests/ldaps_basic.phpt + +diff --git a/ext/ldap/ldap.c b/ext/ldap/ldap.c +index 6661310d055..299cd6c855b 100644 +--- a/ext/ldap/ldap.c ++++ b/ext/ldap/ldap.c +@@ -715,6 +715,21 @@ static PHP_GINIT_FUNCTION(ldap) + } + /* }}} */ + ++/* {{{ PHP_RINIT_FUNCTION */ ++static PHP_RINIT_FUNCTION(ldap) ++{ ++#if defined(COMPILE_DL_LDAP) && defined(ZTS) ++ ZEND_TSRMLS_CACHE_UPDATE(); ++#endif ++ ++ /* needed before first connect and after TLS option changes */ ++ LDAPG(tls_newctx) = true; ++ ++ return SUCCESS; ++} ++/* }}} */ ++ ++ + /* {{{ PHP_MINIT_FUNCTION */ + PHP_MINIT_FUNCTION(ldap) + { +@@ -1037,6 +1052,20 @@ PHP_FUNCTION(ldap_connect) + snprintf( url, urllen, "ldap://%s:" ZEND_LONG_FMT, host, port ); + } + ++#ifdef LDAP_OPT_X_TLS_NEWCTX ++ if (LDAPG(tls_newctx) && url && !strncmp(url, "ldaps:", 6)) { ++ int val = 0; ++ ++ /* ensure all pending TLS options are applied in a new context */ ++ if (ldap_set_option(NULL, LDAP_OPT_X_TLS_NEWCTX, &val) != LDAP_OPT_SUCCESS) { ++ zval_ptr_dtor(return_value); ++ php_error_docref(NULL, E_WARNING, "Could not create new security context"); ++ RETURN_FALSE; ++ } ++ LDAPG(tls_newctx) = false; ++ } ++#endif ++ + #ifdef LDAP_API_FEATURE_X_OPENLDAP + /* ldap_init() is deprecated, use ldap_initialize() instead. + */ +@@ -3135,15 +3164,7 @@ PHP_FUNCTION(ldap_set_option) + } + + switch (option) { +- /* options with int value */ +- case LDAP_OPT_DEREF: +- case LDAP_OPT_SIZELIMIT: +- case LDAP_OPT_TIMELIMIT: +- case LDAP_OPT_PROTOCOL_VERSION: +- case LDAP_OPT_ERROR_NUMBER: +-#ifdef LDAP_OPT_DEBUG_LEVEL +- case LDAP_OPT_DEBUG_LEVEL: +-#endif ++ /* TLS options with int value */ + #ifdef LDAP_OPT_X_TLS_REQUIRE_CERT + case LDAP_OPT_X_TLS_REQUIRE_CERT: + #endif +@@ -3152,6 +3173,18 @@ PHP_FUNCTION(ldap_set_option) + #endif + #ifdef LDAP_OPT_X_TLS_PROTOCOL_MIN + case LDAP_OPT_X_TLS_PROTOCOL_MIN: ++#endif ++ /* TLS option change requires resetting TLS context */ ++ LDAPG(tls_newctx) = true; ++ ZEND_FALLTHROUGH; ++ /* other options with int value */ ++ case LDAP_OPT_DEREF: ++ case LDAP_OPT_SIZELIMIT: ++ case LDAP_OPT_TIMELIMIT: ++ case LDAP_OPT_PROTOCOL_VERSION: ++ case LDAP_OPT_ERROR_NUMBER: ++#ifdef LDAP_OPT_DEBUG_LEVEL ++ case LDAP_OPT_DEBUG_LEVEL: + #endif + #ifdef LDAP_OPT_X_KEEPALIVE_IDLE + case LDAP_OPT_X_KEEPALIVE_IDLE: +@@ -3208,17 +3241,7 @@ PHP_FUNCTION(ldap_set_option) + } + } break; + #endif +- /* options with string value */ +- case LDAP_OPT_ERROR_STRING: +-#ifdef LDAP_OPT_HOST_NAME +- case LDAP_OPT_HOST_NAME: +-#endif +-#ifdef HAVE_LDAP_SASL +- case LDAP_OPT_X_SASL_MECH: +- case LDAP_OPT_X_SASL_REALM: +- case LDAP_OPT_X_SASL_AUTHCID: +- case LDAP_OPT_X_SASL_AUTHZID: +-#endif ++ /* TLS options with string value */ + #if (LDAP_API_VERSION > 2000) + case LDAP_OPT_X_TLS_CACERTDIR: + case LDAP_OPT_X_TLS_CACERTFILE: +@@ -3232,6 +3255,20 @@ PHP_FUNCTION(ldap_set_option) + #endif + #ifdef LDAP_OPT_X_TLS_DHFILE + case LDAP_OPT_X_TLS_DHFILE: ++#endif ++ /* TLS option change requires resetting TLS context */ ++ LDAPG(tls_newctx) = true; ++ ZEND_FALLTHROUGH; ++ /* other options with string value */ ++ case LDAP_OPT_ERROR_STRING: ++#ifdef LDAP_OPT_HOST_NAME ++ case LDAP_OPT_HOST_NAME: ++#endif ++#ifdef HAVE_LDAP_SASL ++ case LDAP_OPT_X_SASL_MECH: ++ case LDAP_OPT_X_SASL_REALM: ++ case LDAP_OPT_X_SASL_AUTHCID: ++ case LDAP_OPT_X_SASL_AUTHZID: + #endif + #ifdef LDAP_OPT_MATCHED_DN + case LDAP_OPT_MATCHED_DN: +@@ -3658,6 +3695,9 @@ PHP_FUNCTION(ldap_start_tls) + zval *link; + ldap_linkdata *ld; + int rc, protocol = LDAP_VERSION3; ++#ifdef LDAP_OPT_X_TLS_NEWCTX ++ int val = 0; ++#endif + + if (zend_parse_parameters(ZEND_NUM_ARGS(), "r", &link) != SUCCESS) { + RETURN_THROWS(); +@@ -3668,13 +3708,16 @@ PHP_FUNCTION(ldap_start_tls) + } + + if (((rc = ldap_set_option(ld->link, LDAP_OPT_PROTOCOL_VERSION, &protocol)) != LDAP_SUCCESS) || ++#ifdef LDAP_OPT_X_TLS_NEWCTX ++ (LDAPG(tls_newctx) && (rc = ldap_set_option(ld->link, LDAP_OPT_X_TLS_NEWCTX, &val)) != LDAP_OPT_SUCCESS) || ++#endif + ((rc = ldap_start_tls_s(ld->link, NULL, NULL)) != LDAP_SUCCESS) + ) { + php_error_docref(NULL, E_WARNING,"Unable to start TLS: %s", ldap_err2string(rc)); + RETURN_FALSE; +- } else { +- RETURN_TRUE; + } ++ LDAPG(tls_newctx) = false; ++ RETURN_TRUE; + } + /* }}} */ + #endif +@@ -4178,7 +4221,7 @@ zend_module_entry ldap_module_entry = { /* {{{ */ + ext_functions, + PHP_MINIT(ldap), + PHP_MSHUTDOWN(ldap), +- NULL, ++ PHP_RINIT(ldap), + NULL, + PHP_MINFO(ldap), + PHP_LDAP_VERSION, +diff --git a/ext/ldap/php_ldap.h b/ext/ldap/php_ldap.h +index fcda55e92fb..36941d952bf 100644 +--- a/ext/ldap/php_ldap.h ++++ b/ext/ldap/php_ldap.h +@@ -39,6 +39,7 @@ PHP_MINFO_FUNCTION(ldap); + ZEND_BEGIN_MODULE_GLOBALS(ldap) + zend_long num_links; + zend_long max_links; ++ bool tls_newctx; /* create new TLS context before connect */ + ZEND_END_MODULE_GLOBALS(ldap) + + #if defined(ZTS) && defined(COMPILE_DL_LDAP) +diff --git a/ext/ldap/tests/ldap_start_tls_basic.phpt b/ext/ldap/tests/ldap_start_tls_basic.phpt +index a9e3c6c97ce..94d9e8e9552 100644 +--- a/ext/ldap/tests/ldap_start_tls_basic.phpt ++++ b/ext/ldap/tests/ldap_start_tls_basic.phpt +@@ -8,11 +8,28 @@ Patrick Allaert + + --FILE-- + + --EXPECT-- ++bool(false) + bool(true) ++bool(false) +diff --git a/ext/ldap/tests/ldaps_basic.phpt b/ext/ldap/tests/ldaps_basic.phpt +new file mode 100644 +index 00000000000..7a1a1383436 +--- /dev/null ++++ b/ext/ldap/tests/ldaps_basic.phpt +@@ -0,0 +1,55 @@ ++--TEST-- ++ldap_connect() - Basic ldaps test ++--EXTENSIONS-- ++ldap ++--XFAIL-- ++Passes locally but fails on CI - need investigation (configuration ?) ++--SKIPIF-- ++ ++--FILE-- ++ ++--EXPECT-- ++bool(false) ++bool(true) ++bool(true) ++bool(false) ++bool(false) +-- +2.43.7 + +From 398430a2ee29c30aad76fea54358e3d7c53c3357 Mon Sep 17 00:00:00 2001 +From: Remi Collet +Date: Tue, 13 May 2025 16:00:32 +0200 +Subject: [PATCH 2/4] Fix GH-18529: ldap no longer respects TLS_CACERT from + ldaprc in ldap_start_tls() Regresion introduced in fix for GH-17776 + +- ensure TLS string options are properly inherited + workaround to openldap issue https://bugs.openldap.org/show_bug.cgi?id=10337 + +- fix ldaps/start_tls tests using LDAPNOINIT in ldaps/tls tests + +(cherry picked from commit 2760a3ef9719dac2e53baf3dc2d8a3dd1227d88b) +--- + ext/ldap/ldap.c | 49 ++++++++++++++++++++++-- + ext/ldap/tests/ldap_start_tls_basic.phpt | 2 + + ext/ldap/tests/ldaps_basic.phpt | 4 +- + 3 files changed, 49 insertions(+), 6 deletions(-) + +diff --git a/ext/ldap/ldap.c b/ext/ldap/ldap.c +index 299cd6c855b..6d576b84bd4 100644 +--- a/ext/ldap/ldap.c ++++ b/ext/ldap/ldap.c +@@ -3689,15 +3689,56 @@ PHP_FUNCTION(ldap_rename_ext) + /* }}} */ + + #ifdef HAVE_LDAP_START_TLS_S ++/* ++ Force new tls context creation with string options inherited from global ++ Workaround to https://bugs.openldap.org/show_bug.cgi?id=10337 ++ */ ++static int _php_ldap_tls_newctx(LDAP *ld) ++{ ++ int val = 0, i, opts[] = { ++#if (LDAP_API_VERSION > 2000) ++ LDAP_OPT_X_TLS_CACERTDIR, ++ LDAP_OPT_X_TLS_CACERTFILE, ++ LDAP_OPT_X_TLS_CERTFILE, ++ LDAP_OPT_X_TLS_CIPHER_SUITE, ++ LDAP_OPT_X_TLS_KEYFILE, ++ LDAP_OPT_X_TLS_RANDOM_FILE, ++#endif ++#ifdef LDAP_OPT_X_TLS_CRLFILE ++ LDAP_OPT_X_TLS_CRLFILE, ++#endif ++#ifdef LDAP_OPT_X_TLS_DHFILE ++ LDAP_OPT_X_TLS_DHFILE, ++#endif ++#ifdef LDAP_OPT_X_TLS_ECNAME ++ LDAP_OPT_X_TLS_ECNAME, ++#endif ++ 0}; ++ ++ for (i=0 ; opts[i] ; i++) { ++ char *path = NULL; ++ ++ ldap_get_option(ld, opts[i], &path); ++ if (path) { /* already set locally */ ++ ldap_memfree(path); ++ } else { ++ ldap_get_option(NULL, opts[i], &path); ++ if (path) { /* set globally, inherit */ ++ ldap_set_option(ld, opts[i], path); ++ ldap_memfree(path); ++ } ++ } ++ } ++ ++ return ldap_set_option(ld, LDAP_OPT_X_TLS_NEWCTX, &val); ++} ++ + /* {{{ Start TLS */ + PHP_FUNCTION(ldap_start_tls) + { + zval *link; + ldap_linkdata *ld; + int rc, protocol = LDAP_VERSION3; +-#ifdef LDAP_OPT_X_TLS_NEWCTX +- int val = 0; +-#endif + + if (zend_parse_parameters(ZEND_NUM_ARGS(), "r", &link) != SUCCESS) { + RETURN_THROWS(); +@@ -3709,7 +3750,7 @@ PHP_FUNCTION(ldap_start_tls) + + if (((rc = ldap_set_option(ld->link, LDAP_OPT_PROTOCOL_VERSION, &protocol)) != LDAP_SUCCESS) || + #ifdef LDAP_OPT_X_TLS_NEWCTX +- (LDAPG(tls_newctx) && (rc = ldap_set_option(ld->link, LDAP_OPT_X_TLS_NEWCTX, &val)) != LDAP_OPT_SUCCESS) || ++ (LDAPG(tls_newctx) && (rc = _php_ldap_tls_newctx(ld->link)) != LDAP_OPT_SUCCESS) || + #endif + ((rc = ldap_start_tls_s(ld->link, NULL, NULL)) != LDAP_SUCCESS) + ) { +diff --git a/ext/ldap/tests/ldap_start_tls_basic.phpt b/ext/ldap/tests/ldap_start_tls_basic.phpt +index 94d9e8e9552..14720b57286 100644 +--- a/ext/ldap/tests/ldap_start_tls_basic.phpt ++++ b/ext/ldap/tests/ldap_start_tls_basic.phpt +@@ -3,6 +3,8 @@ ldap_start_tls() - Basic ldap_start_tls test + --CREDITS-- + Patrick Allaert + # Belgian PHP Testfest 2009 ++--ENV-- ++LDAPNOINIT=1 + --SKIPIF-- + + +diff --git a/ext/ldap/tests/ldaps_basic.phpt b/ext/ldap/tests/ldaps_basic.phpt +index 7a1a1383436..9fa49a6ce79 100644 +--- a/ext/ldap/tests/ldaps_basic.phpt ++++ b/ext/ldap/tests/ldaps_basic.phpt +@@ -2,8 +2,8 @@ + ldap_connect() - Basic ldaps test + --EXTENSIONS-- + ldap +---XFAIL-- +-Passes locally but fails on CI - need investigation (configuration ?) ++--ENV-- ++LDAPNOINIT=1 + --SKIPIF-- + + --FILE-- +-- +2.43.7 + +From d79c713b18d67495dad3d8199ec093dd93404eac Mon Sep 17 00:00:00 2001 +From: Jakub Zelenka +Date: Tue, 27 May 2025 19:03:56 +0200 +Subject: [PATCH 3/4] Fix GH-18529: additional inheriting of TLS int options + +This is for LDAP_OPT_X_TLS_PROTOCOL_MIN and LDAP_OPT_X_TLS_PROTOCOL_MAX + +It also adds a test that uses LDAPCONF with TLS max version lower than +the minimum TLS server version so it should always fail. However it +does not fial for the second case without this change which confirms +that the change works as expected. + +Closes GH-18676 + +(cherry picked from commit eade5c17ead650b0735295214bbec84872465e87) +--- + .github/scripts/setup-slapd.sh | 3 ++ + ext/ldap/ldap.c | 32 ++++++++++++--- + .../tests/ldap_start_tls_rc_max_version.conf | 1 + + .../tests/ldap_start_tls_rc_max_version.phpt | 41 +++++++++++++++++++ + ext/ldap/tests/skipifbindfailure.inc | 33 +++++++++++++++ + 5 files changed, 105 insertions(+), 5 deletions(-) + create mode 100644 ext/ldap/tests/ldap_start_tls_rc_max_version.conf + create mode 100644 ext/ldap/tests/ldap_start_tls_rc_max_version.phpt + +diff --git a/.github/scripts/setup-slapd.sh b/.github/scripts/setup-slapd.sh +index b9cb1a4ff7a..f2eb1269d1f 100755 +--- a/.github/scripts/setup-slapd.sh ++++ b/.github/scripts/setup-slapd.sh +@@ -72,6 +72,9 @@ olcTLSCertificateKeyFile: /etc/ldap/ssl/server.key + add: olcTLSVerifyClient + olcTLSVerifyClient: never + - ++add: olcTLSProtocolMin ++olcTLSProtocolMin: 3.3 ++- + add: olcAuthzRegexp + olcAuthzRegexp: uid=usera,cn=digest-md5,cn=auth cn=usera,dc=my-domain,dc=com + - +diff --git a/ext/ldap/ldap.c b/ext/ldap/ldap.c +index 6d576b84bd4..8da103fd632 100644 +--- a/ext/ldap/ldap.c ++++ b/ext/ldap/ldap.c +@@ -3695,7 +3695,8 @@ PHP_FUNCTION(ldap_rename_ext) + */ + static int _php_ldap_tls_newctx(LDAP *ld) + { +- int val = 0, i, opts[] = { ++ int val = 0, i; ++ int str_opts[] = { + #if (LDAP_API_VERSION > 2000) + LDAP_OPT_X_TLS_CACERTDIR, + LDAP_OPT_X_TLS_CACERTFILE, +@@ -3715,21 +3716,42 @@ static int _php_ldap_tls_newctx(LDAP *ld) + #endif + 0}; + +- for (i=0 ; opts[i] ; i++) { ++ for (i=0 ; str_opts[i] ; i++) { + char *path = NULL; + +- ldap_get_option(ld, opts[i], &path); ++ ldap_get_option(ld, str_opts[i], &path); + if (path) { /* already set locally */ + ldap_memfree(path); + } else { +- ldap_get_option(NULL, opts[i], &path); ++ ldap_get_option(NULL, str_opts[i], &path); + if (path) { /* set globally, inherit */ +- ldap_set_option(ld, opts[i], path); ++ ldap_set_option(ld, str_opts[i], path); + ldap_memfree(path); + } + } + } + ++#ifdef LDAP_OPT_X_TLS_PROTOCOL_MIN ++ int int_opts[] = { ++ LDAP_OPT_X_TLS_PROTOCOL_MIN, ++#ifdef LDAP_OPT_X_TLS_PROTOCOL_MAX ++ LDAP_OPT_X_TLS_PROTOCOL_MAX, ++#endif ++ 0 ++ }; ++ for (i=0 ; int_opts[i] ; i++) { ++ int value = 0; ++ ++ ldap_get_option(ld, int_opts[i], &value); ++ if (value <= 0) { /* if value is not set already */ ++ ldap_get_option(NULL, int_opts[i], &value); ++ if (value > 0) { /* set globally, inherit */ ++ ldap_set_option(ld, int_opts[i], &value); ++ } ++ } ++ } ++#endif ++ + return ldap_set_option(ld, LDAP_OPT_X_TLS_NEWCTX, &val); + } + +diff --git a/ext/ldap/tests/ldap_start_tls_rc_max_version.conf b/ext/ldap/tests/ldap_start_tls_rc_max_version.conf +new file mode 100644 +index 00000000000..0cd03f8b8e1 +--- /dev/null ++++ b/ext/ldap/tests/ldap_start_tls_rc_max_version.conf +@@ -0,0 +1 @@ ++TLS_PROTOCOL_MAX 3.2 +\ No newline at end of file +diff --git a/ext/ldap/tests/ldap_start_tls_rc_max_version.phpt b/ext/ldap/tests/ldap_start_tls_rc_max_version.phpt +new file mode 100644 +index 00000000000..359785f8b5a +--- /dev/null ++++ b/ext/ldap/tests/ldap_start_tls_rc_max_version.phpt +@@ -0,0 +1,41 @@ ++--TEST-- ++ldap_start_tls() - Basic ldap_start_tls test ++--EXTENSIONS-- ++ldap ++--ENV-- ++LDAPCONF={PWD}/ldap_start_tls_rc_max_version.conf ++--SKIPIF-- ++ "OpenLDAP", ++ "min_version" => 20600, ++]; ++require_once __DIR__ .'/skipifbindfailure.inc'; ++?> ++--FILE-- ++ ++--EXPECT-- ++bool(false) ++bool(false) ++bool(false) +diff --git a/ext/ldap/tests/skipifbindfailure.inc b/ext/ldap/tests/skipifbindfailure.inc +index 8f66c6cb968..ce24cd78f1e 100644 +--- a/ext/ldap/tests/skipifbindfailure.inc ++++ b/ext/ldap/tests/skipifbindfailure.inc +@@ -10,4 +10,37 @@ if ($skip_on_bind_failure) { + + ldap_unbind($link); + } ++ ++if (isset($require_vendor)) { ++ ob_start(); ++ phpinfo(INFO_MODULES); ++ $phpinfo = ob_get_clean(); ++ ++ // Extract the LDAP section specifically ++ if (preg_match('/^ldap\s*$(.*?)^[a-z_]+\s*$/ims', $phpinfo, $ldap_section_match)) { ++ $ldap_section = $ldap_section_match[1]; ++ ++ // Extract vendor info from the LDAP section only ++ if (preg_match('/Vendor Name\s*=>\s*(.+)/i', $ldap_section, $name_match) && ++ preg_match('/Vendor Version\s*=>\s*(\d+)/i', $ldap_section, $version_match)) { ++ ++ $vendor_name = trim($name_match[1]); ++ $vendor_version = (int)$version_match[1]; ++ ++ // Check vendor name if specified ++ if (isset($require_vendor['name']) && $vendor_name !== $require_vendor['name']) { ++ die("skip Requires {$require_vendor['name']} (detected: $vendor_name)"); ++ } ++ ++ // Check minimum version if specified ++ if (isset($require_vendor['min_version']) && $vendor_version < $require_vendor['min_version']) { ++ die("skip Requires minimum version {$require_vendor['min_version']} (detected: $vendor_version)"); ++ } ++ } else { ++ die("skip Cannot determine LDAP vendor information"); ++ } ++ } else { ++ die("skip LDAP extension information not found"); ++ } ++} + ?> +-- +2.43.7 + +From bb559b2ecaa9359c79e336e6ff20bbe61bec9f6c Mon Sep 17 00:00:00 2001 +From: Remi Collet +Date: Fri, 3 Oct 2025 08:31:29 +0200 +Subject: [PATCH 4/4] adapt for 8.0 + +--- + ext/ldap/ldap.c | 2 -- + ext/ldap/tests/ldap_start_tls_rc_max_version.phpt | 2 ++ + 2 files changed, 2 insertions(+), 2 deletions(-) + +diff --git a/ext/ldap/ldap.c b/ext/ldap/ldap.c +index 8da103fd632..9a4589565cb 100644 +--- a/ext/ldap/ldap.c ++++ b/ext/ldap/ldap.c +@@ -3176,7 +3176,6 @@ PHP_FUNCTION(ldap_set_option) + #endif + /* TLS option change requires resetting TLS context */ + LDAPG(tls_newctx) = true; +- ZEND_FALLTHROUGH; + /* other options with int value */ + case LDAP_OPT_DEREF: + case LDAP_OPT_SIZELIMIT: +@@ -3258,7 +3257,6 @@ PHP_FUNCTION(ldap_set_option) + #endif + /* TLS option change requires resetting TLS context */ + LDAPG(tls_newctx) = true; +- ZEND_FALLTHROUGH; + /* other options with string value */ + case LDAP_OPT_ERROR_STRING: + #ifdef LDAP_OPT_HOST_NAME +diff --git a/ext/ldap/tests/ldap_start_tls_rc_max_version.phpt b/ext/ldap/tests/ldap_start_tls_rc_max_version.phpt +index 359785f8b5a..fcdf276af9c 100644 +--- a/ext/ldap/tests/ldap_start_tls_rc_max_version.phpt ++++ b/ext/ldap/tests/ldap_start_tls_rc_max_version.phpt +@@ -16,6 +16,8 @@ require_once __DIR__ .'/skipifbindfailure.inc'; + +Date: Thu, 20 Mar 2025 09:26:26 +0100 +Subject: [PATCH] Fix #66049 Typemap can break parsing in parse_packet_soap + leading to a segfault + +(cherry picked from commit 209f4c296ec6a08c721afdf17d787db4b5fd37d0) +--- + ext/soap/php_packet_soap.c | 3 ++ + ext/soap/tests/bugs/bug66049.phpt | 48 +++++++++++++++++++++++++++++++ + 2 files changed, 51 insertions(+) + create mode 100644 ext/soap/tests/bugs/bug66049.phpt + +diff --git a/ext/soap/php_packet_soap.c b/ext/soap/php_packet_soap.c +index 0e3a5197373..ab3dbf6b1e4 100644 +--- a/ext/soap/php_packet_soap.c ++++ b/ext/soap/php_packet_soap.c +@@ -192,6 +192,7 @@ int parse_packet_soap(zval *this_ptr, char *buffer, int buffer_size, sdlFunction + if (tmp != NULL && tmp->children != NULL) { + zval zv; + master_to_zval(&zv, get_conversion(IS_STRING), tmp); ++ convert_to_string(&zv) + faultstring = Z_STR(zv); + } + +@@ -199,6 +200,7 @@ int parse_packet_soap(zval *this_ptr, char *buffer, int buffer_size, sdlFunction + if (tmp != NULL && tmp->children != NULL) { + zval zv; + master_to_zval(&zv, get_conversion(IS_STRING), tmp); ++ convert_to_string(&zv) + faultactor = Z_STR(zv); + } + +@@ -222,6 +224,7 @@ int parse_packet_soap(zval *this_ptr, char *buffer, int buffer_size, sdlFunction + if (tmp != NULL && tmp->children != NULL) { + zval zv; + master_to_zval(&zv, get_conversion(IS_STRING), tmp); ++ convert_to_string(&zv) + faultstring = Z_STR(zv); + } + } +diff --git a/ext/soap/tests/bugs/bug66049.phpt b/ext/soap/tests/bugs/bug66049.phpt +new file mode 100644 +index 00000000000..e48845a8a14 +--- /dev/null ++++ b/ext/soap/tests/bugs/bug66049.phpt +@@ -0,0 +1,48 @@ ++--TEST-- ++Fix #66049 Typemap can break parsing in parse_packet_soap leading to a segfault ++--EXTENSIONS-- ++soap ++--INI-- ++soap.wsdl_cache_enabled=0 ++--FILE-- ++ ++ ++ ++ SOAP-ENV:Servernot present ++ '; ++ return $res; ++ } ++} ++ ++try { ++ $client=new TestSoapClient(null, [ ++ 'uri' => 'test://', ++ 'location' => 'test://', ++ 'typemap' => [[ ++ "type_ns" => "http://www.w3.org/2001/XMLSchema", ++ "type_name" => "string", ++ "from_xml" => "soap_string_from_xml" ++ ]]]); ++ $client->Mist(""); ++} catch (SoapFault $e) { ++ var_dump($e->faultstring); ++ var_dump($e->faultcode); ++} ++?> ++Done ++--EXPECT-- ++soap_string_from_xml ++string(3) "2.3" ++string(15) "SOAP-ENV:Server" ++Done +-- +2.43.7 + diff --git a/php-cve-2025-1220.patch b/php-cve-2025-1220.patch new file mode 100644 index 0000000..dad45e0 --- /dev/null +++ b/php-cve-2025-1220.patch @@ -0,0 +1,153 @@ +From 36150278addd8686a9899559241296094bd57282 Mon Sep 17 00:00:00 2001 +From: Jakub Zelenka +Date: Thu, 10 Apr 2025 15:15:36 +0200 +Subject: [PATCH 2/4] Fix GHSA-3cr5-j632-f35r: Null byte in hostnames + +This fixes stream_socket_client() and fsockopen(). + +Specifically it adds a check to parse_ip_address_ex and it also makes +sure that the \0 is not ignored in fsockopen() hostname formatting. + +(cherry picked from commit cac8f7f1cf4939f55f06b68120040f057682d89c) +--- + ext/standard/fsock.c | 27 +++++++++++++++++-- + .../tests/network/ghsa-3cr5-j632-f35r.phpt | 21 +++++++++++++++ + .../tests/streams/ghsa-3cr5-j632-f35r.phpt | 26 ++++++++++++++++++ + main/streams/xp_socket.c | 9 ++++--- + 4 files changed, 78 insertions(+), 5 deletions(-) + create mode 100644 ext/standard/tests/network/ghsa-3cr5-j632-f35r.phpt + create mode 100644 ext/standard/tests/streams/ghsa-3cr5-j632-f35r.phpt + +diff --git a/ext/standard/fsock.c b/ext/standard/fsock.c +index a9c3cb0bf5d..636dbb6e359 100644 +--- a/ext/standard/fsock.c ++++ b/ext/standard/fsock.c +@@ -23,6 +23,28 @@ + #include "php_network.h" + #include "file.h" + ++static size_t php_fsockopen_format_host_port(char **message, const char *prefix, size_t prefix_len, ++ const char *host, size_t host_len, zend_long port) ++{ ++ char portbuf[32]; ++ int portlen = snprintf(portbuf, sizeof(portbuf), ":" ZEND_LONG_FMT, port); ++ size_t total_len = prefix_len + host_len + portlen; ++ ++ char *result = emalloc(total_len + 1); ++ ++ if (prefix_len > 0) { ++ memcpy(result, prefix, prefix_len); ++ } ++ memcpy(result + prefix_len, host, host_len); ++ memcpy(result + prefix_len + host_len, portbuf, portlen); ++ ++ result[total_len] = '\0'; ++ ++ *message = result; ++ ++ return total_len; ++} ++ + /* {{{ php_fsockopen() */ + + static void php_fsockopen_stream(INTERNAL_FUNCTION_PARAMETERS, int persistent) +@@ -62,11 +84,12 @@ static void php_fsockopen_stream(INTERNAL_FUNCTION_PARAMETERS, int persistent) + } + + if (persistent) { +- spprintf(&hashkey, 0, "pfsockopen__%s:" ZEND_LONG_FMT, host, port); ++ php_fsockopen_format_host_port(&hashkey, "pfsockopen__", strlen("pfsockopen__"), host, ++ host_len, port); + } + + if (port > 0) { +- hostname_len = spprintf(&hostname, 0, "%s:" ZEND_LONG_FMT, host, port); ++ hostname_len = php_fsockopen_format_host_port(&hostname, "", 0, host, host_len, port); + } else { + hostname_len = host_len; + hostname = host; +diff --git a/ext/standard/tests/network/ghsa-3cr5-j632-f35r.phpt b/ext/standard/tests/network/ghsa-3cr5-j632-f35r.phpt +new file mode 100644 +index 00000000000..7556c3be94c +--- /dev/null ++++ b/ext/standard/tests/network/ghsa-3cr5-j632-f35r.phpt +@@ -0,0 +1,21 @@ ++--TEST-- ++GHSA-3cr5-j632-f35r: Null byte termination in fsockopen() ++--FILE-- ++ ++--EXPECTF-- ++ ++Warning: fsockopen(): Unable to connect to localhost:%d (The hostname must not contain null bytes) in %s ++bool(false) +diff --git a/ext/standard/tests/streams/ghsa-3cr5-j632-f35r.phpt b/ext/standard/tests/streams/ghsa-3cr5-j632-f35r.phpt +new file mode 100644 +index 00000000000..52f9263c99a +--- /dev/null ++++ b/ext/standard/tests/streams/ghsa-3cr5-j632-f35r.phpt +@@ -0,0 +1,26 @@ ++--TEST-- ++GHSA-3cr5-j632-f35r: Null byte termination in stream_socket_client() ++--FILE-- ++ ++--EXPECTF-- ++ ++Warning: stream_socket_client(): Unable to connect to tcp://localhost\0.example.com:%d (The hostname must not contain null bytes) in %s ++bool(false) +diff --git a/main/streams/xp_socket.c b/main/streams/xp_socket.c +index 68df3366340..47f1bdd4b1d 100644 +--- a/main/streams/xp_socket.c ++++ b/main/streams/xp_socket.c +@@ -580,12 +580,15 @@ static inline char *parse_ip_address_ex(const char *str, size_t str_len, int *po + char *colon; + char *host = NULL; + +-#ifdef HAVE_IPV6 +- char *p; ++ if (memchr(str, '\0', str_len)) { ++ *err = strpprintf(0, "The hostname must not contain null bytes"); ++ return NULL; ++ } + ++#ifdef HAVE_IPV6 + if (*(str) == '[' && str_len > 1) { + /* IPV6 notation to specify raw address with port (i.e. [fe80::1]:80) */ +- p = memchr(str + 1, ']', str_len - 2); ++ char *p = memchr(str + 1, ']', str_len - 2); + if (!p || *(p + 1) != ':') { + if (get_err) { + *err = strpprintf(0, "Failed to parse IPv6 address \"%s\"", str); +-- +2.50.0 + diff --git a/php-cve-2025-1735.patch b/php-cve-2025-1735.patch new file mode 100644 index 0000000..60e7e2e --- /dev/null +++ b/php-cve-2025-1735.patch @@ -0,0 +1,490 @@ +From 7633d987cc11ee2601223e73cfdb8b31fed5980f Mon Sep 17 00:00:00 2001 +From: Jakub Zelenka +Date: Tue, 4 Mar 2025 17:23:01 +0100 +Subject: [PATCH 3/4] Fix GHSA-hrwm-9436-5mv3: pgsql escaping no error checks + +This adds error checks for escape function is pgsql and pdo_pgsql +extensions. It prevents possibility of storing not properly escaped +data which could potentially lead to some security issues. + +(cherry picked from commit 9376aeef9f8ff81f2705b8016237ec3e30bdee44) +--- + ext/pdo_pgsql/pgsql_driver.c | 10 +- + ext/pdo_pgsql/tests/ghsa-hrwm-9436-5mv3.phpt | 22 ++++ + ext/pgsql/pgsql.c | 129 +++++++++++++++---- + ext/pgsql/tests/ghsa-hrwm-9436-5mv3.phpt | 64 +++++++++ + 4 files changed, 202 insertions(+), 23 deletions(-) + create mode 100644 ext/pdo_pgsql/tests/ghsa-hrwm-9436-5mv3.phpt + create mode 100644 ext/pgsql/tests/ghsa-hrwm-9436-5mv3.phpt + +diff --git a/ext/pdo_pgsql/pgsql_driver.c b/ext/pdo_pgsql/pgsql_driver.c +index c90ef468907..218a306fa3c 100644 +--- a/ext/pdo_pgsql/pgsql_driver.c ++++ b/ext/pdo_pgsql/pgsql_driver.c +@@ -354,11 +354,15 @@ static int pgsql_handle_quoter(pdo_dbh_t *dbh, const char *unquoted, size_t unqu + unsigned char *escaped; + pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data; + size_t tmp_len; ++ int err; + + switch (paramtype) { + case PDO_PARAM_LOB: + /* escapedlen returned by PQescapeBytea() accounts for trailing 0 */ + escaped = PQescapeByteaConn(H->server, (unsigned char *)unquoted, unquotedlen, &tmp_len); ++ if (escaped == NULL) { ++ return 0; ++ } + *quotedlen = tmp_len + 1; + *quoted = emalloc(*quotedlen + 1); + memcpy((*quoted)+1, escaped, *quotedlen-2); +@@ -370,7 +374,11 @@ static int pgsql_handle_quoter(pdo_dbh_t *dbh, const char *unquoted, size_t unqu + default: + *quoted = safe_emalloc(2, unquotedlen, 3); + (*quoted)[0] = '\''; +- *quotedlen = PQescapeStringConn(H->server, *quoted + 1, unquoted, unquotedlen, NULL); ++ *quotedlen = PQescapeStringConn(H->server, *quoted + 1, unquoted, unquotedlen, &err); ++ if (err) { ++ efree(*quoted); ++ return 0; ++ } + (*quoted)[*quotedlen + 1] = '\''; + (*quoted)[*quotedlen + 2] = '\0'; + *quotedlen += 2; +diff --git a/ext/pdo_pgsql/tests/ghsa-hrwm-9436-5mv3.phpt b/ext/pdo_pgsql/tests/ghsa-hrwm-9436-5mv3.phpt +new file mode 100644 +index 00000000000..60e13613d04 +--- /dev/null ++++ b/ext/pdo_pgsql/tests/ghsa-hrwm-9436-5mv3.phpt +@@ -0,0 +1,22 @@ ++--TEST-- ++#GHSA-hrwm-9436-5mv3: pdo_pgsql extension does not check for errors during escaping ++--SKIPIF-- ++ ++--FILE-- ++setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); ++ ++$invalid = "ABC\xff\x30';"; ++var_dump($db->quote($invalid)); ++ ++?> ++--EXPECT-- ++bool(false) +diff --git a/ext/pgsql/pgsql.c b/ext/pgsql/pgsql.c +index 588f481a498..e9a68a8555f 100644 +--- a/ext/pgsql/pgsql.c ++++ b/ext/pgsql/pgsql.c +@@ -3298,10 +3298,16 @@ PHP_FUNCTION(pg_escape_string) + + to = zend_string_safe_alloc(ZSTR_LEN(from), 2, 0, 0); + if (link) { ++ int err; + if ((pgsql = (PGconn *)zend_fetch_resource2(link, "PostgreSQL link", le_link, le_plink)) == NULL) { + RETURN_THROWS(); + } +- ZSTR_LEN(to) = PQescapeStringConn(pgsql, ZSTR_VAL(to), ZSTR_VAL(from), ZSTR_LEN(from), NULL); ++ ZSTR_LEN(to) = PQescapeStringConn(pgsql, ZSTR_VAL(to), ZSTR_VAL(from), ZSTR_LEN(from), &err); ++ if (err) { ++ zend_argument_value_error(ZEND_NUM_ARGS(), "Escaping string failed"); ++ zend_string_efree(to); ++ RETURN_THROWS(); ++ } + } else + { + ZSTR_LEN(to) = PQescapeString(ZSTR_VAL(to), ZSTR_VAL(from), ZSTR_LEN(from)); +@@ -3344,6 +3350,10 @@ PHP_FUNCTION(pg_escape_bytea) + to = (char *)PQescapeByteaConn(pgsql, (unsigned char *)from, (size_t)from_len, &to_len); + } else + to = (char *)PQescapeBytea((unsigned char*)from, from_len, &to_len); ++ if (to == NULL) { ++ zend_argument_value_error(ZEND_NUM_ARGS(), "Escape failure"); ++ RETURN_THROWS(); ++ } + + RETVAL_STRINGL(to, to_len-1); /* to_len includes additional '\0' */ + PQfreemem(to); +@@ -4251,7 +4261,7 @@ PHP_PGSQL_API int php_pgsql_meta_data(PGconn *pg_link, const char *table_name, z + char *escaped; + smart_str querystr = {0}; + size_t new_len; +- int i, num_rows; ++ int i, num_rows, err; + zval elem; + + ZEND_ASSERT(*table_name); +@@ -4290,7 +4300,14 @@ PHP_PGSQL_API int php_pgsql_meta_data(PGconn *pg_link, const char *table_name, z + "WHERE a.attnum > 0 AND c.relname = '"); + } + escaped = (char *)safe_emalloc(strlen(tmp_name2), 2, 1); +- new_len = PQescapeStringConn(pg_link, escaped, tmp_name2, strlen(tmp_name2), NULL); ++ new_len = PQescapeStringConn(pg_link, escaped, tmp_name2, strlen(tmp_name2), &err); ++ if (err) { ++ php_error_docref(NULL, E_WARNING, "Escaping table name '%s' failed", table_name); ++ efree(src); ++ efree(escaped); ++ smart_str_free(&querystr); ++ return FAILURE; ++ } + if (new_len) { + smart_str_appendl(&querystr, escaped, new_len); + } +@@ -4298,7 +4315,14 @@ PHP_PGSQL_API int php_pgsql_meta_data(PGconn *pg_link, const char *table_name, z + + smart_str_appends(&querystr, "' AND n.nspname = '"); + escaped = (char *)safe_emalloc(strlen(tmp_name), 2, 1); +- new_len = PQescapeStringConn(pg_link, escaped, tmp_name, strlen(tmp_name), NULL); ++ new_len = PQescapeStringConn(pg_link, escaped, tmp_name, strlen(tmp_name), &err); ++ if (err) { ++ php_error_docref(NULL, E_WARNING, "Escaping table namespace '%s' failed", table_name); ++ efree(src); ++ efree(escaped); ++ smart_str_free(&querystr); ++ return FAILURE; ++ } + if (new_len) { + smart_str_appendl(&querystr, escaped, new_len); + } +@@ -4575,7 +4599,7 @@ PHP_PGSQL_API int php_pgsql_convert(PGconn *pg_link, const char *table_name, con + { + zend_string *field = NULL; + zval meta, *def, *type, *not_null, *has_default, *is_enum, *val, new_val; +- int err = 0, skip_field; ++ int err = 0, escape_err = 0, skip_field; + php_pgsql_data_type data_type; + + ZEND_ASSERT(pg_link != NULL); +@@ -4829,10 +4853,14 @@ PHP_PGSQL_API int php_pgsql_convert(PGconn *pg_link, const char *table_name, con + /* PostgreSQL ignores \0 */ + str = zend_string_alloc(Z_STRLEN_P(val) * 2, 0); + /* better to use PGSQLescapeLiteral since PGescapeStringConn does not handle special \ */ +- ZSTR_LEN(str) = PQescapeStringConn(pg_link, ZSTR_VAL(str), Z_STRVAL_P(val), Z_STRLEN_P(val), NULL); +- str = zend_string_truncate(str, ZSTR_LEN(str), 0); +- ZVAL_NEW_STR(&new_val, str); +- php_pgsql_add_quotes(&new_val, 1); ++ ZSTR_LEN(str) = PQescapeStringConn(pg_link, ZSTR_VAL(str), Z_STRVAL_P(val), Z_STRLEN_P(val), &escape_err); ++ if (escape_err) { ++ err = 1; ++ } else { ++ str = zend_string_truncate(str, ZSTR_LEN(str), 0); ++ ZVAL_NEW_STR(&new_val, str); ++ php_pgsql_add_quotes(&new_val, 1); ++ } + } + break; + +@@ -4854,7 +4882,15 @@ PHP_PGSQL_API int php_pgsql_convert(PGconn *pg_link, const char *table_name, con + } + PGSQL_CONV_CHECK_IGNORE(); + if (err) { +- php_error_docref(NULL, E_NOTICE, "Expects NULL, string, long or double value for PostgreSQL '%s' (%s)", Z_STRVAL_P(type), ZSTR_VAL(field)); ++ if (escape_err) { ++ php_error_docref(NULL, E_NOTICE, ++ "String value escaping failed for PostgreSQL '%s' (%s)", ++ Z_STRVAL_P(type), ZSTR_VAL(field)); ++ } else { ++ php_error_docref(NULL, E_NOTICE, ++ "Expects NULL, string, long or double value for PostgreSQL '%s' (%s)", ++ Z_STRVAL_P(type), ZSTR_VAL(field)); ++ } + } + break; + +@@ -5129,6 +5165,11 @@ PHP_PGSQL_API int php_pgsql_convert(PGconn *pg_link, const char *table_name, con + size_t to_len; + smart_str s = {0}; + tmp = PQescapeByteaConn(pg_link, (unsigned char *)Z_STRVAL_P(val), Z_STRLEN_P(val), &to_len); ++ if (tmp == NULL) { ++ php_error_docref(NULL, E_NOTICE, "Escaping value failed for %s field (%s)", Z_STRVAL_P(type), ZSTR_VAL(field)); ++ err = 1; ++ break; ++ } + ZVAL_STRINGL(&new_val, (char *)tmp, to_len - 1); /* PQescapeBytea's to_len includes additional '\0' */ + PQfreemem(tmp); + php_pgsql_add_quotes(&new_val, 1); +@@ -5210,6 +5251,12 @@ PHP_PGSQL_API int php_pgsql_convert(PGconn *pg_link, const char *table_name, con + zend_hash_update(Z_ARRVAL_P(result), field, &new_val); + } else { + char *escaped = PQescapeIdentifier(pg_link, ZSTR_VAL(field), ZSTR_LEN(field)); ++ if (escaped == NULL) { ++ /* This cannot fail because of invalid string but only due to failed memory allocation */ ++ php_error_docref(NULL, E_NOTICE, "Escaping field '%s' failed", ZSTR_VAL(field)); ++ err = 1; ++ break; ++ } + add_assoc_zval(result, escaped, &new_val); + PQfreemem(escaped); + } +@@ -5290,7 +5337,7 @@ static int do_exec(smart_str *querystr, ExecStatusType expect, PGconn *pg_link, + } + /* }}} */ + +-static inline void build_tablename(smart_str *querystr, PGconn *pg_link, const char *table) /* {{{ */ ++static inline zend_result build_tablename(smart_str *querystr, PGconn *pg_link, const char *table) /* {{{ */ + { + size_t table_len = strlen(table); + +@@ -5301,6 +5348,10 @@ static inline void build_tablename(smart_str *querystr, PGconn *pg_link, const c + smart_str_appendl(querystr, table, len); + } else { + char *escaped = PQescapeIdentifier(pg_link, table, len); ++ if (escaped == NULL) { ++ php_error_docref(NULL, E_NOTICE, "Failed to escape table name '%s'", table); ++ return FAILURE; ++ } + smart_str_appends(querystr, escaped); + PQfreemem(escaped); + } +@@ -5313,11 +5364,17 @@ static inline void build_tablename(smart_str *querystr, PGconn *pg_link, const c + smart_str_appendl(querystr, after_dot, len); + } else { + char *escaped = PQescapeIdentifier(pg_link, after_dot, len); ++ if (escaped == NULL) { ++ php_error_docref(NULL, E_NOTICE, "Failed to escape table name '%s'", table); ++ return FAILURE; ++ } + smart_str_appendc(querystr, '.'); + smart_str_appends(querystr, escaped); + PQfreemem(escaped); + } + } ++ ++ return SUCCESS; + } + /* }}} */ + +@@ -5338,7 +5395,9 @@ PHP_PGSQL_API int php_pgsql_insert(PGconn *pg_link, const char *table, zval *var + ZVAL_UNDEF(&converted); + if (zend_hash_num_elements(Z_ARRVAL_P(var_array)) == 0) { + smart_str_appends(&querystr, "INSERT INTO "); +- build_tablename(&querystr, pg_link, table); ++ if (build_tablename(&querystr, pg_link, table) == FAILURE) { ++ goto cleanup; ++ } + smart_str_appends(&querystr, " DEFAULT VALUES"); + + goto no_values; +@@ -5354,7 +5413,9 @@ PHP_PGSQL_API int php_pgsql_insert(PGconn *pg_link, const char *table, zval *var + } + + smart_str_appends(&querystr, "INSERT INTO "); +- build_tablename(&querystr, pg_link, table); ++ if (build_tablename(&querystr, pg_link, table) == FAILURE) { ++ goto cleanup; ++ } + smart_str_appends(&querystr, " ("); + + ZEND_HASH_FOREACH_STR_KEY(Z_ARRVAL_P(var_array), fld) { +@@ -5364,6 +5425,10 @@ PHP_PGSQL_API int php_pgsql_insert(PGconn *pg_link, const char *table, zval *var + } + if (opt & PGSQL_DML_ESCAPE) { + tmp = PQescapeIdentifier(pg_link, ZSTR_VAL(fld), ZSTR_LEN(fld) + 1); ++ if (tmp == NULL) { ++ php_error_docref(NULL, E_NOTICE, "Failed to escape field '%s'", ZSTR_VAL(fld)); ++ goto cleanup; ++ } + smart_str_appends(&querystr, tmp); + PQfreemem(tmp); + } else { +@@ -5375,15 +5440,19 @@ PHP_PGSQL_API int php_pgsql_insert(PGconn *pg_link, const char *table, zval *var + smart_str_appends(&querystr, ") VALUES ("); + + /* make values string */ +- ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(var_array), val) { ++ ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(var_array), fld, val) { + /* we can avoid the key_type check here, because we tested it in the other loop */ + switch (Z_TYPE_P(val)) { + case IS_STRING: + if (opt & PGSQL_DML_ESCAPE) { +- size_t new_len; +- char *tmp; +- tmp = (char *)safe_emalloc(Z_STRLEN_P(val), 2, 1); +- new_len = PQescapeStringConn(pg_link, tmp, Z_STRVAL_P(val), Z_STRLEN_P(val), NULL); ++ int error; ++ char *tmp = safe_emalloc(Z_STRLEN_P(val), 2, 1); ++ size_t new_len = PQescapeStringConn(pg_link, tmp, Z_STRVAL_P(val), Z_STRLEN_P(val), &error); ++ if (error) { ++ php_error_docref(NULL, E_NOTICE, "Failed to escape field '%s' value", ZSTR_VAL(fld)); ++ efree(tmp); ++ goto cleanup; ++ } + smart_str_appendc(&querystr, '\''); + smart_str_appendl(&querystr, tmp, new_len); + smart_str_appendc(&querystr, '\''); +@@ -5537,6 +5606,10 @@ static inline int build_assignment_string(PGconn *pg_link, smart_str *querystr, + } + if (opt & PGSQL_DML_ESCAPE) { + char *tmp = PQescapeIdentifier(pg_link, ZSTR_VAL(fld), ZSTR_LEN(fld) + 1); ++ if (tmp == NULL) { ++ php_error_docref(NULL, E_NOTICE, "Failed to escape field '%s'", ZSTR_VAL(fld)); ++ return -1; ++ } + smart_str_appends(querystr, tmp); + PQfreemem(tmp); + } else { +@@ -5551,8 +5624,14 @@ static inline int build_assignment_string(PGconn *pg_link, smart_str *querystr, + switch (Z_TYPE_P(val)) { + case IS_STRING: + if (opt & PGSQL_DML_ESCAPE) { ++ int error; + char *tmp = (char *)safe_emalloc(Z_STRLEN_P(val), 2, 1); +- size_t new_len = PQescapeStringConn(pg_link, tmp, Z_STRVAL_P(val), Z_STRLEN_P(val), NULL); ++ size_t new_len = PQescapeStringConn(pg_link, tmp, Z_STRVAL_P(val), Z_STRLEN_P(val), &error); ++ if (error) { ++ php_error_docref(NULL, E_NOTICE, "Failed to escape field '%s' value", ZSTR_VAL(fld)); ++ efree(tmp); ++ return -1; ++ } + smart_str_appendc(querystr, '\''); + smart_str_appendl(querystr, tmp, new_len); + smart_str_appendc(querystr, '\''); +@@ -5620,7 +5699,9 @@ PHP_PGSQL_API int php_pgsql_update(PGconn *pg_link, const char *table, zval *var + } + + smart_str_appends(&querystr, "UPDATE "); +- build_tablename(&querystr, pg_link, table); ++ if (build_tablename(&querystr, pg_link, table) == FAILURE) { ++ goto cleanup; ++ } + smart_str_appends(&querystr, " SET "); + + if (build_assignment_string(pg_link, &querystr, Z_ARRVAL_P(var_array), 0, ",", 1, opt)) +@@ -5722,7 +5803,9 @@ PHP_PGSQL_API int php_pgsql_delete(PGconn *pg_link, const char *table, zval *ids + } + + smart_str_appends(&querystr, "DELETE FROM "); +- build_tablename(&querystr, pg_link, table); ++ if (build_tablename(&querystr, pg_link, table) == FAILURE) { ++ goto cleanup; ++ } + smart_str_appends(&querystr, " WHERE "); + + if (build_assignment_string(pg_link, &querystr, Z_ARRVAL_P(ids_array), 1, " AND ", sizeof(" AND ")-1, opt)) +@@ -5860,7 +5943,9 @@ PHP_PGSQL_API void php_pgsql_result2array(PGresult *pg_result, zval *ret_array, + } + + smart_str_appends(&querystr, "SELECT * FROM "); +- build_tablename(&querystr, pg_link, table); ++ if (build_tablename(&querystr, pg_link, table) == FAILURE) { ++ goto cleanup; ++ } + smart_str_appends(&querystr, " WHERE "); + + if (build_assignment_string(pg_link, &querystr, Z_ARRVAL_P(ids_array), 1, " AND ", sizeof(" AND ")-1, opt)) +diff --git a/ext/pgsql/tests/ghsa-hrwm-9436-5mv3.phpt b/ext/pgsql/tests/ghsa-hrwm-9436-5mv3.phpt +new file mode 100644 +index 00000000000..c1c5e05dce6 +--- /dev/null ++++ b/ext/pgsql/tests/ghsa-hrwm-9436-5mv3.phpt +@@ -0,0 +1,64 @@ ++--TEST-- ++#GHSA-hrwm-9436-5mv3: pgsql extension does not check for errors during escaping ++--EXTENSIONS-- ++pgsql ++--SKIPIF-- ++ ++--FILE-- ++ 'test'])); // table name str escape in php_pgsql_meta_data ++var_dump(pg_insert($db, "$invalid.tbl", ['bar' => 'test'])); // schema name str escape in php_pgsql_meta_data ++var_dump(pg_insert($db, 'ghsa_hrmw_9436_5mv3', ['bar' => $invalid])); // converted value str escape in php_pgsql_convert ++var_dump(pg_insert($db, $invalid, [])); // ident escape in build_tablename ++var_dump(pg_insert($db, 'ghsa_hrmw_9436_5mv3', [$invalid => 'foo'], $flags)); // ident escape for field php_pgsql_insert ++var_dump(pg_insert($db, 'ghsa_hrmw_9436_5mv3', ['bar' => $invalid], $flags)); // str escape for field value in php_pgsql_insert ++var_dump(pg_update($db, 'ghsa_hrmw_9436_5mv3', ['bar' => 'val'], [$invalid => 'test'], $flags)); // ident escape in build_assignment_string ++var_dump(pg_update($db, 'ghsa_hrmw_9436_5mv3', ['bar' => 'val'], ['bar' => $invalid], $flags)); // invalid str escape in build_assignment_string ++var_dump(pg_escape_literal($db, $invalid)); // pg_escape_literal escape ++var_dump(pg_escape_identifier($db, $invalid)); // pg_escape_identifier escape ++ ++?> ++--EXPECTF-- ++ ++Warning: pg_insert(): Escaping table name 'ABC%s';' failed in %s on line %d ++bool(false) ++ ++Warning: pg_insert(): Escaping table namespace 'ABC%s';.tbl' failed in %s on line %d ++bool(false) ++ ++Notice: pg_insert(): String value escaping failed for PostgreSQL 'text' (bar) in %s on line %d ++bool(false) ++ ++Notice: pg_insert(): Failed to escape table name 'ABC%s';' in %s on line %d ++bool(false) ++ ++Notice: pg_insert(): Failed to escape field 'ABC%s';' in %s on line %d ++bool(false) ++ ++Notice: pg_insert(): Failed to escape field 'bar' value in %s on line %d ++bool(false) ++ ++Notice: pg_update(): Failed to escape field 'ABC%s';' in %s on line %d ++bool(false) ++ ++Notice: pg_update(): Failed to escape field 'bar' value in %s on line %d ++bool(false) ++ ++Warning: pg_escape_literal(): Failed to escape in %s on line %d ++bool(false) ++ ++Warning: pg_escape_identifier(): Failed to escape in %s on line %d ++bool(false) +-- +2.50.0 + +From 970548b94b7f23be32154d05a9545b10c98bfd62 Mon Sep 17 00:00:00 2001 +From: Remi Collet +Date: Thu, 3 Jul 2025 09:32:25 +0200 +Subject: [PATCH 4/4] NEWS + +--- + NEWS | 14 ++++++++++++++ + 1 file changed, 14 insertions(+) + +diff --git a/NEWS b/NEWS +index 7db6f2660d2..c813f4f357a 100644 +--- a/NEWS ++++ b/NEWS +@@ -1,6 +1,20 @@ + PHP NEWS + ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| + ++Backported from 8.1.33 ++ ++- PGSQL: ++ . Fixed GHSA-hrwm-9436-5mv3 (pgsql extension does not check for errors during ++ escaping). (CVE-2025-1735) (Jakub Zelenka) ++ ++- SOAP: ++ . Fixed GHSA-453j-q27h-5p8x (NULL Pointer Dereference in PHP SOAP Extension ++ via Large XML Namespace Prefix). (CVE-2025-6491) (Lekssays, nielsdos) ++ ++- Standard: ++ . Fixed GHSA-3cr5-j632-f35r (Null byte termination in hostnames). ++ (CVE-2025-1220) (Jakub Zelenka) ++ + Backported from 8.1.32 + + - LibXML: +-- +2.50.0 + diff --git a/php-cve-2025-6491.patch b/php-cve-2025-6491.patch new file mode 100644 index 0000000..d4e4f36 --- /dev/null +++ b/php-cve-2025-6491.patch @@ -0,0 +1,102 @@ +From 1b7410a57f8a5fd1dd43854bcf7b9200517c9fd2 Mon Sep 17 00:00:00 2001 +From: Ahmed Lekssays +Date: Tue, 3 Jun 2025 09:00:55 +0000 +Subject: [PATCH 1/4] Fix GHSA-453j-q27h-5p8x + +Libxml versions prior to 2.13 cannot correctly handle a call to +xmlNodeSetName() with a name longer than 2G. It will leave the node +object in an invalid state with a NULL name. This later causes a NULL +pointer dereference when using the name during message serialization. + +To solve this, implement a workaround that resets the name to the +sentinel name if this situation arises. + +Versions of libxml of 2.13 and higher are not affected. + +This can be exploited if a SoapVar is created with a fully qualified +name that is longer than 2G. This would be possible if some application +code uses a namespace prefix from an untrusted source like from a remote +SOAP service. + +Co-authored-by: Niels Dossche <7771979+nielsdos@users.noreply.github.com> +(cherry picked from commit 9cb3d8d200f0c822b17bda35a2a67a97b039d3e1) +--- + ext/soap/soap.c | 6 ++-- + ext/soap/tests/soap_qname_crash.phpt | 48 ++++++++++++++++++++++++++++ + 2 files changed, 52 insertions(+), 2 deletions(-) + create mode 100644 ext/soap/tests/soap_qname_crash.phpt + +diff --git a/ext/soap/soap.c b/ext/soap/soap.c +index a8df136d665..08d6f285d28 100644 +--- a/ext/soap/soap.c ++++ b/ext/soap/soap.c +@@ -4143,8 +4143,10 @@ static xmlNodePtr serialize_zval(zval *val, sdlParamPtr param, char *paramName, + } + xmlParam = master_to_xml(enc, val, style, parent); + zval_ptr_dtor(&defval); +- if (!strcmp((char*)xmlParam->name, "BOGUS")) { +- xmlNodeSetName(xmlParam, BAD_CAST(paramName)); ++ if (xmlParam != NULL) { ++ if (xmlParam->name == NULL || strcmp((char*)xmlParam->name, "BOGUS") == 0) { ++ xmlNodeSetName(xmlParam, BAD_CAST(paramName)); ++ } + } + return xmlParam; + } +diff --git a/ext/soap/tests/soap_qname_crash.phpt b/ext/soap/tests/soap_qname_crash.phpt +new file mode 100644 +index 00000000000..52177577788 +--- /dev/null ++++ b/ext/soap/tests/soap_qname_crash.phpt +@@ -0,0 +1,48 @@ ++--TEST-- ++Test SoapClient with excessively large QName prefix in SoapVar ++--EXTENSIONS-- ++soap ++--SKIPIF-- ++ ++--INI-- ++memory_limit=8G ++--FILE-- ++ 'http://127.0.0.1/', ++ 'uri' => 'urn:dummy', ++ 'trace' => 1, ++ 'exceptions' => true, ++]; ++$client = new TestSoapClient(null, $options); ++$client->__soapCall("DummyFunction", [$var]); ++?> ++--EXPECT-- ++Attempting to create SoapVar with very large QName ++Attempting encoding ++ ++value +-- +2.50.0 + diff --git a/php.spec b/php.spec index 1762575..1ed9e04 100644 --- a/php.spec +++ b/php.spec @@ -62,7 +62,7 @@ Summary: PHP scripting language for creating dynamic web sites Name: php Version: %{upver}%{?rcver:~%{rcver}} -Release: 3%{?dist} +Release: 4%{?dist} # All files licensed under PHP version 3.01, except # Zend is licensed under Zend # TSRM is licensed under BSD @@ -121,6 +121,10 @@ Patch49: php-8.0.10-phar-sha.patch Patch50: php-8.0.21-openssl3.patch # use system libxcrypt Patch51: php-8.0.13-crypt.patch +# soap fixes from 8.3 +Patch52: php-8.0.30-soap.patch +# ldap fixes from 8.3 +Patch53: php-8.0.30-ldap.patch # Upstream fixes (100+) @@ -144,6 +148,9 @@ Patch214: php-cve-2025-1734.patch Patch215: php-cve-2025-1861.patch Patch216: php-cve-2025-1736.patch Patch217: php-cve-2025-1219.patch +Patch218: php-cve-2025-6491.patch +Patch219: php-cve-2025-1220.patch +Patch220: php-cve-2025-1735.patch # Fixes for tests (300+) # Factory is droped from system tzdata @@ -742,6 +749,8 @@ in pure PHP. %patch -P50 -p1 -b .openssl3 rm ext/openssl/tests/p12_with_extra_certs.p12 %patch -P51 -p1 -b .libxcrypt +%patch -P52 -p1 -b .soapbackports +%patch -P53 -p1 -b .ldapbackports # upstream patches @@ -764,6 +773,9 @@ rm ext/openssl/tests/p12_with_extra_certs.p12 %patch -P215 -p1 -b .cve1861 %patch -P216 -p1 -b .cve1736 %patch -P217 -p1 -b .cve1219 +%patch -P218 -p1 -b .cve6491 +%patch -P219 -p1 -b .cve1220 +%patch -P220 -p1 -b .cve1735 # Fixes for tests %patch -P300 -p1 -b .datetests @@ -1572,6 +1584,16 @@ systemctl try-restart php-fpm.service >/dev/null 2>&1 || : %changelog +* Fri Oct 3 2025 Remi Collet - 8.0.30-4 +- Fix pgsql extension does not check for errors during escaping + CVE-2025-1735 +- Fix NULL Pointer Dereference in PHP SOAP Extension via Large XML Namespace Prefix + CVE-2025-6491 +- Fix Null byte termination in hostnames + CVE-2025-1220 +- Fix soap memory corruption +- Fix ldap_set_option() not applied on different ldap connections + * Thu Mar 13 2025 Remi Collet - 8.0.30-3 - Fix libxml streams use wrong `content-type` header when requesting a redirected resource CVE-2025-1219