diff options
-rw-r--r-- | failed.txt | 31 | ||||
-rw-r--r-- | php-7.0.7-curl.patch | 15 | ||||
-rw-r--r-- | php-7.4.33-icu.patch | 35 | ||||
-rw-r--r-- | php-7.4.33-pcretests.patch | 43 | ||||
-rw-r--r-- | php-7.4.33-proto.patch | 392 | ||||
-rw-r--r-- | php-cve-2024-11233.patch | 68 | ||||
-rw-r--r-- | php-cve-2024-11234.patch | 95 | ||||
-rw-r--r-- | php-cve-2024-11236.patch | 119 | ||||
-rw-r--r-- | php-cve-2024-8929.patch | 2714 | ||||
-rw-r--r-- | php-cve-2024-8932.patch | 139 | ||||
-rw-r--r-- | php-cve-2025-1217.patch | 917 | ||||
-rw-r--r-- | php-cve-2025-1219.patch | 1906 | ||||
-rw-r--r-- | php-cve-2025-1220.patch | 154 | ||||
-rw-r--r-- | php-cve-2025-1734.patch | 314 | ||||
-rw-r--r-- | php-cve-2025-1735.patch | 492 | ||||
-rw-r--r-- | php-cve-2025-1736.patch | 242 | ||||
-rw-r--r-- | php-cve-2025-1861.patch | 349 | ||||
-rw-r--r-- | php-cve-2025-6491.patch | 103 | ||||
-rw-r--r-- | php-fpm.service | 2 | ||||
-rw-r--r-- | php-ghsa-4w77-75f9-2c8w.patch | 135 | ||||
-rw-r--r-- | php.spec | 129 |
21 files changed, 8332 insertions, 62 deletions
@@ -1,26 +1,21 @@ -===== 7.4.33-18 (2024-09-26) +===== 7.4.33-24 (2025-07-03) $ grep -ar 'Tests failed' /var/lib/mock/*/build.log -/var/lib/mock/scl74el8a/build.log:Tests failed : 3 -/var/lib/mock/scl74el8x/build.log:Tests failed : 3 -/var/lib/mock/scl74el9a/build.log:Tests failed : 1 -/var/lib/mock/scl74el9x/build.log:Tests failed : 1 -/var/lib/mock/scl74fc39a/build.log:Tests failed : 1 -/var/lib/mock/scl74fc39x/build.log:Tests failed : 1 -/var/lib/mock/scl80fc40a/build.log:Tests failed : 2 -/var/lib/mock/scl80fc40x/build.log:Tests failed : 2 -/var/lib/mock/scl80fc41a/build.log:Tests failed : 2 -/var/lib/mock/scl80fc41x/build.log:Tests failed : 2 +/var/lib/mock/scl74el8a/build.log:Tests failed : 0 +/var/lib/mock/scl74el8x/build.log:Tests failed : 0 +/var/lib/mock/scl74el9a/build.log:Tests failed : 0 +/var/lib/mock/scl74el9x/build.log:Tests failed : 0 +/var/lib/mock/scl74el10a/build.log:Tests failed : 0 +/var/lib/mock/scl74el10x/build.log:Tests failed : 0 +/var/lib/mock/scl80fc40a/build.log:Tests failed : 0 +/var/lib/mock/scl80fc40x/build.log:Tests failed : 0 +/var/lib/mock/scl80fc41a/build.log:Tests failed : 0 +/var/lib/mock/scl80fc41x/build.log:Tests failed : 0 +/var/lib/mock/scl74fc42a/build.log:Tests failed : 0 +/var/lib/mock/scl74fc42x/build.log:Tests failed : 0 -el8: - 3 openssl_error_string() tests [ext/openssl/tests/openssl_error_string_basic.phpt] - 3 openssl_open() tests [ext/openssl/tests/openssl_open_basic.phpt] -all: - 3 openssl_private_decrypt() tests [ext/openssl/tests/openssl_private_decrypt_basic.phpt] -fc40, fc41: - 3 openssl_x509_parse() tests [ext/openssl/tests/openssl_x509_parse_basic.phpt] (1) proc_open give erratic test results :( diff --git a/php-7.0.7-curl.patch b/php-7.0.7-curl.patch deleted file mode 100644 index 218db98..0000000 --- a/php-7.0.7-curl.patch +++ /dev/null @@ -1,15 +0,0 @@ -diff -up php-7.0.7RC1/ext/curl/interface.c.curltls php-7.0.7RC1/ext/curl/interface.c ---- php-7.0.7RC1/ext/curl/interface.c.curltls 2016-05-10 17:28:33.000000000 +0200 -+++ php-7.0.7RC1/ext/curl/interface.c 2016-05-12 07:43:00.900419946 +0200 -@@ -1257,7 +1257,11 @@ PHP_MINIT_FUNCTION(curl) - - #if LIBCURL_VERSION_NUM >= 0x072200 /* Available since 7.34.0 */ - REGISTER_CURL_CONSTANT(CURLOPT_LOGIN_OPTIONS); -+#endif - -+#if LIBCURL_VERSION_NUM >= 0x071300 /* Available since 7.19.0 (in upstream curl 7.34) -+ backported in RHEL-7 curl-7.29.0-16.el7 rhbz#1012136 -+ backported in RHEL-6 curl-7.19.7-43.el6 rhbz#1036789 */ - REGISTER_CURL_CONSTANT(CURL_SSLVERSION_TLSv1_0); - REGISTER_CURL_CONSTANT(CURL_SSLVERSION_TLSv1_1); - REGISTER_CURL_CONSTANT(CURL_SSLVERSION_TLSv1_2); diff --git a/php-7.4.33-icu.patch b/php-7.4.33-icu.patch new file mode 100644 index 0000000..48940fd --- /dev/null +++ b/php-7.4.33-icu.patch @@ -0,0 +1,35 @@ +From cc46a4e6b5a413bab3e264c1dcaaf7052f54fbc4 Mon Sep 17 00:00:00 2001 +From: David Carlier <devnexen@gmail.com> +Date: Sat, 17 Feb 2024 21:38:21 +0000 +Subject: [PATCH] ext/intl: level up c++ runtime std for icu 74 and onwards. + +to align with what is required to build icu 74 itself. + +Close GH-14002 +--- + NEWS | 3 +++ + ext/intl/config.m4 | 11 ++++++++++- + 2 files changed, 13 insertions(+), 1 deletion(-) + +diff --git a/ext/intl/config.m4 b/ext/intl/config.m4 +index dd687bcd97de3..48f5147ca7bbf 100644 +--- a/ext/intl/config.m4 ++++ b/ext/intl/config.m4 +@@ -83,7 +83,16 @@ if test "$PHP_INTL" != "no"; then + breakiterator/codepointiterator_methods.cpp" + + PHP_REQUIRE_CXX() +- PHP_CXX_COMPILE_STDCXX(11, mandatory, PHP_INTL_STDCXX) ++ ++ AC_MSG_CHECKING([if intl requires -std=gnu++17]) ++ AS_IF([test "$PKG_CONFIG icu-uc --atleast-version=74"],[ ++ AC_MSG_RESULT([yes]) ++ PHP_CXX_COMPILE_STDCXX(17, mandatory, PHP_INTL_STDCXX) ++ ],[ ++ AC_MSG_RESULT([no]) ++ PHP_CXX_COMPILE_STDCXX(11, mandatory, PHP_INTL_STDCXX) ++ ]) ++ + PHP_INTL_CXX_FLAGS="$INTL_COMMON_FLAGS $PHP_INTL_STDCXX $ICU_CXXFLAGS" + if test "$ext_shared" = "no"; then + PHP_ADD_SOURCES(PHP_EXT_DIR(intl), $PHP_INTL_CXX_SOURCES, $PHP_INTL_CXX_FLAGS) diff --git a/php-7.4.33-pcretests.patch b/php-7.4.33-pcretests.patch new file mode 100644 index 0000000..c226661 --- /dev/null +++ b/php-7.4.33-pcretests.patch @@ -0,0 +1,43 @@ +From c3150fcc89825f50d476b1b1971870aeb71f167d Mon Sep 17 00:00:00 2001 +From: Remi Collet <remi@remirepo.net> +Date: Wed, 12 Mar 2025 07:48:05 +0100 +Subject: [PATCH 1/2] Relax test expectation for pcre2lib 10.45 Using + e92848789acd8aa5cf32fedb519ba9378ac64e02 + +--- + ext/pcre/tests/bug75457.phpt | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/ext/pcre/tests/bug75457.phpt b/ext/pcre/tests/bug75457.phpt +index ee5ab162f8a6c..87dc12a1ad056 100644 +--- a/ext/pcre/tests/bug75457.phpt ++++ b/ext/pcre/tests/bug75457.phpt +@@ -6,5 +6,5 @@ $pattern = "/(((?(?C)0?=))(?!()0|.(?0)0)())/"; + var_dump(preg_match($pattern, "hello")); + ?> + --EXPECTF-- +-Warning: preg_match(): Compilation failed: assertion expected after (?( or (?(?C) at offset 8 in %sbug75457.php on line %d ++Warning: preg_match(): Compilation failed: %r(atomic|)%r assertion expected after (?( or (?(?C) at offset 8 in %sbug75457.php on line %d + bool(false) + +From 126095700a02b9aa1f33764a63c93a70e8373ad8 Mon Sep 17 00:00:00 2001 +From: Remi Collet <remi@famillecollet.com> +Date: Wed, 12 Mar 2025 09:36:33 +0100 +Subject: [PATCH 2/2] Update ext/pcre/tests/bug75457.phpt + +Co-authored-by: Niels Dossche <7771979+nielsdos@users.noreply.github.com> +--- + ext/pcre/tests/bug75457.phpt | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/ext/pcre/tests/bug75457.phpt b/ext/pcre/tests/bug75457.phpt +index 87dc12a1ad056..1401b25ff6fb7 100644 +--- a/ext/pcre/tests/bug75457.phpt ++++ b/ext/pcre/tests/bug75457.phpt +@@ -6,5 +6,5 @@ $pattern = "/(((?(?C)0?=))(?!()0|.(?0)0)())/"; + var_dump(preg_match($pattern, "hello")); + ?> + --EXPECTF-- +-Warning: preg_match(): Compilation failed: %r(atomic|)%r assertion expected after (?( or (?(?C) at offset 8 in %sbug75457.php on line %d ++Warning: preg_match(): Compilation failed:%r( atomic|)%r assertion expected after (?( or (?(?C) at offset 8 in %sbug75457.php on line %d + bool(false) diff --git a/php-7.4.33-proto.patch b/php-7.4.33-proto.patch new file mode 100644 index 0000000..2e20717 --- /dev/null +++ b/php-7.4.33-proto.patch @@ -0,0 +1,392 @@ +From f566cba0bb6bd53b1d44d5097e68201412b00f7a Mon Sep 17 00:00:00 2001 +From: Remi Collet <remi@php.net> +Date: Thu, 25 Nov 2021 13:16:26 +0100 +Subject: [PATCH] fix [-Wstrict-prototypes] build warnings in ext/gd + +--- + ext/gd/config.m4 | 2 -- + ext/gd/gd.c | 58 ++++++++++++++++++++++++------------------------ + 2 files changed, 29 insertions(+), 31 deletions(-) + +diff -up a/ext/gd/gd.c.proto b/ext/gd/gd.c +--- a/ext/gd/gd.c.proto 2022-10-31 11:36:07.000000000 +0100 ++++ b/ext/gd/gd.c 2025-02-13 12:04:07.860118321 +0100 +@@ -138,9 +138,9 @@ static void php_image_filter_pixelate(IN + static void php_image_filter_scatter(INTERNAL_FUNCTION_PARAMETERS); + + /* End Section filters declarations */ +-static gdImagePtr _php_image_create_from_string (zval *Data, char *tn, gdImagePtr (*ioctx_func_p)()); +-static void _php_image_create_from(INTERNAL_FUNCTION_PARAMETERS, int image_type, char *tn, gdImagePtr (*func_p)(), gdImagePtr (*ioctx_func_p)()); +-static void _php_image_output(INTERNAL_FUNCTION_PARAMETERS, int image_type, char *tn, void (*func_p)()); ++static gdImagePtr _php_image_create_from_string (zval *Data, char *tn, gdImagePtr (*ioctx_func_p)(gdIOCtxPtr)); ++static void _php_image_create_from(INTERNAL_FUNCTION_PARAMETERS, int image_type, char *tn, gdImagePtr (*func_p)(FILE *), gdImagePtr (*ioctx_func_p)(gdIOCtxPtr)); ++static void _php_image_output(INTERNAL_FUNCTION_PARAMETERS, int image_type, char *tn); + static int _php_image_type(char data[12]); + static void _php_image_convert(INTERNAL_FUNCTION_PARAMETERS, int image_type); + +@@ -2330,7 +2330,7 @@ static int _php_image_type (char data[12 + + /* {{{ _php_image_create_from_string + */ +-gdImagePtr _php_image_create_from_string(zval *data, char *tn, gdImagePtr (*ioctx_func_p)()) ++gdImagePtr _php_image_create_from_string(zval *data, char *tn, gdImagePtr (*ioctx_func_p)(gdIOCtxPtr)) + { + gdImagePtr im; + gdIOCtx *io_ctx; +@@ -2440,7 +2440,7 @@ PHP_FUNCTION(imagecreatefromstring) + + /* {{{ _php_image_create_from + */ +-static void _php_image_create_from(INTERNAL_FUNCTION_PARAMETERS, int image_type, char *tn, gdImagePtr (*func_p)(), gdImagePtr (*ioctx_func_p)()) ++static void _php_image_create_from(INTERNAL_FUNCTION_PARAMETERS, int image_type, char *tn, gdImagePtr (*func_p)(FILE *), gdImagePtr (*ioctx_func_p)(gdIOCtxPtr)) + { + char *file; + size_t file_len; +@@ -2477,7 +2477,7 @@ static void _php_image_create_from(INTER + if (FAILURE == php_stream_cast(stream, PHP_STREAM_AS_STDIO, (void**)&fp, REPORT_ERRORS)) { + goto out_err; + } +- } else if (ioctx_func_p) { ++ } else if (ioctx_func_p || image_type == PHP_GDIMG_TYPE_GD2PART) { + /* we can create an io context */ + gdIOCtx* io_ctx; + zend_string *buff; +@@ -2501,7 +2501,7 @@ static void _php_image_create_from(INTER + } + + if (image_type == PHP_GDIMG_TYPE_GD2PART) { +- im = (*ioctx_func_p)(io_ctx, srcx, srcy, width, height); ++ im = gdImageCreateFromGd2PartCtx(io_ctx, srcx, srcy, width, height); + } else { + im = (*ioctx_func_p)(io_ctx); + } +@@ -2519,7 +2519,7 @@ static void _php_image_create_from(INTER + if (!im && fp) { + switch (image_type) { + case PHP_GDIMG_TYPE_GD2PART: +- im = (*func_p)(fp, srcx, srcy, width, height); ++ im = gdImageCreateFromGd2Part(fp, srcx, srcy, width, height); + break; + #if defined(HAVE_GD_XPM) + case PHP_GDIMG_TYPE_XPM: +@@ -2608,7 +2608,7 @@ PHP_FUNCTION(imagecreatefromxbm) + Create a new image from XPM file or URL */ + PHP_FUNCTION(imagecreatefromxpm) + { +- _php_image_create_from(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_GDIMG_TYPE_XPM, "XPM", gdImageCreateFromXpm, NULL); ++ _php_image_create_from(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_GDIMG_TYPE_XPM, "XPM", NULL, NULL); + } + /* }}} */ + #endif +@@ -2641,7 +2641,7 @@ PHP_FUNCTION(imagecreatefromgd2) + Create a new image from a given part of GD2 file or URL */ + PHP_FUNCTION(imagecreatefromgd2part) + { +- _php_image_create_from(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_GDIMG_TYPE_GD2PART, "GD2", gdImageCreateFromGd2Part, gdImageCreateFromGd2PartCtx); ++ _php_image_create_from(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_GDIMG_TYPE_GD2PART, "GD2", NULL, NULL); + } + /* }}} */ + +@@ -2667,7 +2667,7 @@ PHP_FUNCTION(imagecreatefromtga) + + /* {{{ _php_image_output + */ +-static void _php_image_output(INTERNAL_FUNCTION_PARAMETERS, int image_type, char *tn, void (*func_p)()) ++static void _php_image_output(INTERNAL_FUNCTION_PARAMETERS, int image_type, char *tn) + { + zval *imgind; + char *file = NULL; +@@ -2720,13 +2720,13 @@ static void _php_image_output(INTERNAL_F + gdImageWBMP(im, q, fp); + break; + case PHP_GDIMG_TYPE_GD: +- (*func_p)(im, fp); ++ gdImageGd(im, fp); + break; + case PHP_GDIMG_TYPE_GD2: + if (q == -1) { + q = 128; + } +- (*func_p)(im, fp, q, t); ++ gdImageGd2(im, fp, q, t); + break; + default: + ZEND_ASSERT(0); +@@ -2756,13 +2756,13 @@ static void _php_image_output(INTERNAL_F + gdImageWBMP(im, q, tmp); + break; + case PHP_GDIMG_TYPE_GD: +- (*func_p)(im, tmp); ++ gdImageGd(im, tmp); + break; + case PHP_GDIMG_TYPE_GD2: + if (q == -1) { + q = 128; + } +- (*func_p)(im, tmp, q, t); ++ gdImageGd2(im, tmp, q, t); + break; + default: + ZEND_ASSERT(0); +@@ -2786,7 +2786,7 @@ static void _php_image_output(INTERNAL_F + Output XBM image to browser or file */ + PHP_FUNCTION(imagexbm) + { +- _php_image_output_ctx(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_GDIMG_TYPE_XBM, "XBM", gdImageXbmCtx); ++ _php_image_output_ctx(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_GDIMG_TYPE_XBM, "XBM"); + } + /* }}} */ + +@@ -2794,7 +2794,7 @@ PHP_FUNCTION(imagexbm) + Output GIF image to browser or file */ + PHP_FUNCTION(imagegif) + { +- _php_image_output_ctx(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_GDIMG_TYPE_GIF, "GIF", gdImageGifCtx); ++ _php_image_output_ctx(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_GDIMG_TYPE_GIF, "GIF"); + } + /* }}} */ + +@@ -2803,7 +2803,7 @@ PHP_FUNCTION(imagegif) + Output PNG image to browser or file */ + PHP_FUNCTION(imagepng) + { +- _php_image_output_ctx(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_GDIMG_TYPE_PNG, "PNG", gdImagePngCtxEx); ++ _php_image_output_ctx(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_GDIMG_TYPE_PNG, "PNG"); + } + /* }}} */ + #endif /* HAVE_GD_PNG */ +@@ -2814,7 +2814,7 @@ PHP_FUNCTION(imagepng) + Output WEBP image to browser or file */ + PHP_FUNCTION(imagewebp) + { +- _php_image_output_ctx(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_GDIMG_TYPE_WEBP, "WEBP", gdImageWebpCtx); ++ _php_image_output_ctx(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_GDIMG_TYPE_WEBP, "WEBP"); + } + /* }}} */ + #endif /* HAVE_GD_WEBP */ +@@ -2825,7 +2825,7 @@ PHP_FUNCTION(imagewebp) + Output JPEG image to browser or file */ + PHP_FUNCTION(imagejpeg) + { +- _php_image_output_ctx(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_GDIMG_TYPE_JPG, "JPEG", gdImageJpegCtx); ++ _php_image_output_ctx(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_GDIMG_TYPE_JPG, "JPEG"); + } + /* }}} */ + #endif /* HAVE_GD_JPG */ +@@ -2834,7 +2834,7 @@ PHP_FUNCTION(imagejpeg) + Output WBMP image to browser or file */ + PHP_FUNCTION(imagewbmp) + { +- _php_image_output_ctx(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_GDIMG_TYPE_WBM, "WBMP", gdImageWBMPCtx); ++ _php_image_output_ctx(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_GDIMG_TYPE_WBM, "WBMP"); + } + /* }}} */ + +@@ -2842,7 +2842,7 @@ PHP_FUNCTION(imagewbmp) + Output GD image to browser or file */ + PHP_FUNCTION(imagegd) + { +- _php_image_output(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_GDIMG_TYPE_GD, "GD", gdImageGd); ++ _php_image_output(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_GDIMG_TYPE_GD, "GD"); + } + /* }}} */ + +@@ -2850,7 +2850,7 @@ PHP_FUNCTION(imagegd) + Output GD2 image to browser or file */ + PHP_FUNCTION(imagegd2) + { +- _php_image_output(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_GDIMG_TYPE_GD2, "GD2", gdImageGd2); ++ _php_image_output(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_GDIMG_TYPE_GD2, "GD2"); + } + /* }}} */ + +@@ -2859,7 +2859,7 @@ PHP_FUNCTION(imagegd2) + Output BMP image to browser or file */ + PHP_FUNCTION(imagebmp) + { +- _php_image_output_ctx(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_GDIMG_TYPE_BMP, "BMP", gdImageBmpCtx); ++ _php_image_output_ctx(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_GDIMG_TYPE_BMP, "BMP"); + } + /* }}} */ + #endif +@@ -4146,7 +4146,7 @@ static void php_imagettftext_common(INTE + Output WBMP image to browser or file */ + PHP_FUNCTION(image2wbmp) + { +- _php_image_output(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_GDIMG_CONVERT_WBM, "WBMP", NULL); ++ _php_image_output(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_GDIMG_CONVERT_WBM, "WBMP"); + } + /* }}} */ + +diff -up a/ext/gd/gd_ctx.c.proto b/ext/gd/gd_ctx.c +--- a/ext/gd/gd_ctx.c.proto 2025-02-13 11:42:48.478248591 +0100 ++++ b/ext/gd/gd_ctx.c 2025-02-13 11:52:48.325740296 +0100 +@@ -77,7 +77,7 @@ static void _php_image_stream_ctxfreeand + } /* }}} */ + + /* {{{ _php_image_output_ctx */ +-static void _php_image_output_ctx(INTERNAL_FUNCTION_PARAMETERS, int image_type, char *tn, void (*func_p)()) ++static void _php_image_output_ctx(INTERNAL_FUNCTION_PARAMETERS, int image_type, char *tn) + { + zval *imgind; + char *file = NULL; +@@ -177,16 +177,20 @@ static void _php_image_output_ctx(INTERN + + switch(image_type) { + case PHP_GDIMG_TYPE_JPG: +- (*func_p)(im, ctx, q); ++ gdImageJpegCtx(im, ctx, q); + break; + case PHP_GDIMG_TYPE_WEBP: + if (q == -1) { + q = 80; + } +- (*func_p)(im, ctx, q); ++ gdImageWebpCtx(im, ctx, q); + break; + case PHP_GDIMG_TYPE_PNG: +- (*func_p)(im, ctx, q, f); ++#ifdef HAVE_GD_BUNDLED ++ gdImagePngCtxEx(im, ctx, q, f); ++#else ++ gdImagePngCtxEx(im, ctx, q); ++#endif + break; + case PHP_GDIMG_TYPE_XBM: + case PHP_GDIMG_TYPE_WBM: +@@ -197,16 +201,16 @@ static void _php_image_output_ctx(INTERN + q = i; + } + if (image_type == PHP_GDIMG_TYPE_XBM) { +- (*func_p)(im, file ? file : "", q, ctx); ++ gdImageXbmCtx(im, file ? file : "", q, ctx); + } else { +- (*func_p)(im, q, ctx); ++ gdImageWBMPCtx(im, q, ctx); + } + break; + case PHP_GDIMG_TYPE_BMP: +- (*func_p)(im, ctx, (int) compressed); ++ gdImageBmpCtx(im, ctx, (int) compressed); + break; +- default: +- (*func_p)(im, ctx); ++ case PHP_GDIMG_TYPE_GIF: ++ gdImageGifCtx(im, ctx); + break; + } + +From b7356692f69f4ac0a07ea54e83debdd04b426dcb Mon Sep 17 00:00:00 2001 +From: George Peter Banyard <girgias@php.net> +Date: Wed, 12 May 2021 14:41:11 +0100 +Subject: [PATCH] Specify function pointer signature for scanf implementation + +Fix [-Wstrict-prototypes] warnings in standard/scanf.c +--- + ext/standard/scanf.c | 14 ++++++++------ + 1 file changed, 8 insertions(+), 6 deletions(-) + +diff --git a/ext/standard/scanf.c b/ext/standard/scanf.c +index f58b4195cc599..78ecc1642cf92 100644 +--- a/ext/standard/scanf.c ++++ b/ext/standard/scanf.c +@@ -108,6 +108,8 @@ typedef struct CharSet { + } *ranges; + } CharSet; + ++typedef zend_long (*int_string_formater)(const char*, char**, int); ++ + /* + * Declarations for functions used only in this file. + */ +@@ -585,7 +587,7 @@ PHPAPI int php_sscanf_internal( char *string, char *format, + int base = 0; + int underflow = 0; + size_t width; +- zend_long (*fn)() = NULL; ++ int_string_formater fn = NULL; + char *ch, sch; + int flags; + char buf[64]; /* Temporary buffer to hold scanned number +@@ -750,29 +752,29 @@ PHPAPI int php_sscanf_internal( char *string, char *format, + case 'D': + op = 'i'; + base = 10; +- fn = (zend_long (*)())ZEND_STRTOL_PTR; ++ fn = (int_string_formater)ZEND_STRTOL_PTR; + break; + case 'i': + op = 'i'; + base = 0; +- fn = (zend_long (*)())ZEND_STRTOL_PTR; ++ fn = (int_string_formater)ZEND_STRTOL_PTR; + break; + case 'o': + op = 'i'; + base = 8; +- fn = (zend_long (*)())ZEND_STRTOL_PTR; ++ fn = (int_string_formater)ZEND_STRTOL_PTR; + break; + case 'x': + case 'X': + op = 'i'; + base = 16; +- fn = (zend_long (*)())ZEND_STRTOL_PTR; ++ fn = (int_string_formater)ZEND_STRTOL_PTR; + break; + case 'u': + op = 'i'; + base = 10; + flags |= SCAN_UNSIGNED; +- fn = (zend_long (*)())ZEND_STRTOUL_PTR; ++ fn = (int_string_formater)ZEND_STRTOUL_PTR; + break; + + case 'f': +From 2068d230d981d7b06b41b87ebc37ab2581b79852 Mon Sep 17 00:00:00 2001 +From: George Peter Banyard <girgias@php.net> +Date: Wed, 12 May 2021 18:54:57 +0100 +Subject: [PATCH] Fix [-Wstrict-prototypes] warning in PCNTL extension + +To achieve this we need to introduce a new wrapper function with +dummy arguments which calls pcntl_signal_dispatch() to respect +the function pointer signature for a tick function. +--- + ext/pcntl/pcntl.c | 10 ++++++++-- + 1 file changed, 8 insertions(+), 2 deletions(-) + +diff --git a/ext/pcntl/pcntl.c b/ext/pcntl/pcntl.c +index 1e8690ae75144..c116eff7d034a 100644 +--- a/ext/pcntl/pcntl.c ++++ b/ext/pcntl/pcntl.c +@@ -252,7 +252,8 @@ static void pcntl_siginfo_to_zval(int, s + #else + static void pcntl_signal_handler(int); + #endif +-static void pcntl_signal_dispatch(); ++static void pcntl_signal_dispatch(void); ++static void pcntl_signal_dispatch_tick_function(int dummy_int, void *dummy_pointer); + static void pcntl_interrupt_function(zend_execute_data *execute_data); + + void php_register_signal_constants(INIT_FUNC_ARGS) +@@ -587,7 +588,7 @@ static PHP_GINIT_FUNCTION(pcntl) + + PHP_RINIT_FUNCTION(pcntl) + { +- php_add_tick_function(pcntl_signal_dispatch, NULL); ++ php_add_tick_function(pcntl_signal_dispatch_tick_function, NULL); + zend_hash_init(&PCNTL_G(php_signal_table), 16, NULL, ZVAL_PTR_DTOR, 0); + PCNTL_G(head) = PCNTL_G(tail) = PCNTL_G(spares) = NULL; + PCNTL_G(async_signals) = 0; +@@ -1549,6 +1550,11 @@ void pcntl_signal_dispatch() + sigprocmask(SIG_SETMASK, &old_mask, NULL); + } + ++static void pcntl_signal_dispatch_tick_function(int dummy_int, void *dummy_pointer) ++{ ++ return pcntl_signal_dispatch(); ++} ++ + /* {{{ proto bool pcntl_async_signals([bool on[) + Enable/disable asynchronous signal handling and return the old setting. */ + PHP_FUNCTION(pcntl_async_signals) diff --git a/php-cve-2024-11233.patch b/php-cve-2024-11233.patch new file mode 100644 index 0000000..d6c29ae --- /dev/null +++ b/php-cve-2024-11233.patch @@ -0,0 +1,68 @@ +From 44a5975f83a02eb8169d12af912e6222b28216d0 Mon Sep 17 00:00:00 2001 +From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> +Date: Fri, 8 Nov 2024 22:04:21 +0100 +Subject: [PATCH 5/7] Fix GHSA-r977-prxv-hc43 + +Move the bound check upwards. Since this doesn't generate output we can +check the bound first. + +(cherry picked from commit 81030c9bbb5cd2e740b8398bb7212df9709f0274) +(cherry picked from commit 2cee10a1206f5bc7724232d3988be2cfcb0bc9df) +--- + ext/standard/filters.c | 7 ++++--- + ext/standard/tests/filters/ghsa-r977-prxv-hc43.phpt | 12 ++++++++++++ + 2 files changed, 16 insertions(+), 3 deletions(-) + create mode 100644 ext/standard/tests/filters/ghsa-r977-prxv-hc43.phpt + +diff --git a/ext/standard/filters.c b/ext/standard/filters.c +index 018270c730d..5d5745c6bec 100644 +--- a/ext/standard/filters.c ++++ b/ext/standard/filters.c +@@ -1128,6 +1128,9 @@ static php_conv_err_t php_conv_qprint_decode_convert(php_conv_qprint_decode *ins + } break; + + case 5: { ++ if (icnt == 0) { ++ goto out; ++ } + if (!inst->lbchars && lb_cnt == 1 && *ps == '\n') { + /* auto-detect soft line breaks, found network line break */ + lb_cnt = lb_ptr = 0; +@@ -1141,15 +1144,13 @@ static php_conv_err_t php_conv_qprint_decode_convert(php_conv_qprint_decode *ins + /* soft line break */ + lb_cnt = lb_ptr = 0; + scan_stat = 0; +- } else if (icnt > 0) { ++ } else { + if (*ps == (unsigned char)inst->lbchars[lb_cnt]) { + lb_cnt++; + ps++, icnt--; + } else { + scan_stat = 6; /* no break for short-cut */ + } +- } else { +- goto out; + } + } break; + +diff --git a/ext/standard/tests/filters/ghsa-r977-prxv-hc43.phpt b/ext/standard/tests/filters/ghsa-r977-prxv-hc43.phpt +new file mode 100644 +index 00000000000..8fdcce8ff22 +--- /dev/null ++++ b/ext/standard/tests/filters/ghsa-r977-prxv-hc43.phpt +@@ -0,0 +1,12 @@ ++--TEST-- ++GHSA-r977-prxv-hc43: Single byte overread with convert.quoted-printable-decode filter ++--FILE-- ++<?php ++ ++$input_data = str_repeat('A', 8189)."X=\r"; ++$filter_url = "php://filter/convert.quoted-printable-decode/resource=data:," . urlencode($input_data); ++var_dump(file_get_contents($filter_url)); ++ ++?> ++--EXPECT-- ++string(8190) "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAX" +-- +2.47.0 + diff --git a/php-cve-2024-11234.patch b/php-cve-2024-11234.patch new file mode 100644 index 0000000..0fd31f3 --- /dev/null +++ b/php-cve-2024-11234.patch @@ -0,0 +1,95 @@ +From 494de65139592da0e5e5b6fdf198c2f9c762f4d6 Mon Sep 17 00:00:00 2001 +From: Jakub Zelenka <bukka@php.net> +Date: Fri, 8 Nov 2024 23:43:47 +0100 +Subject: [PATCH 3/7] Fix GHSA-c5f2-jwm7-mmq2: stream HTTP fulluri CRLF + injection + +(cherry picked from commit 426a6d4539ebee34879ac5de857036bb6ff0e732) +(cherry picked from commit bc1f192102dd8cbda028e40aa31604c4885d387c) +(cherry picked from commit 8d130e16fbfda7d154fedfa0f1ff1d5ad5e26815) +--- + ext/standard/http_fopen_wrapper.c | 18 ++++++++---- + .../tests/http/ghsa-c5f2-jwm7-mmq2.phpt | 28 +++++++++++++++++++ + 2 files changed, 40 insertions(+), 6 deletions(-) + create mode 100644 ext/standard/tests/http/ghsa-c5f2-jwm7-mmq2.phpt + +diff --git a/ext/standard/http_fopen_wrapper.c b/ext/standard/http_fopen_wrapper.c +index 4d918b21e65..aeeb438f0f9 100644 +--- a/ext/standard/http_fopen_wrapper.c ++++ b/ext/standard/http_fopen_wrapper.c +@@ -186,6 +186,11 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper, + return NULL; + } + ++ /* Should we send the entire path in the request line, default to no. */ ++ if (context && (tmpzval = php_stream_context_get_option(context, "http", "request_fulluri")) != NULL) { ++ request_fulluri = zend_is_true(tmpzval); ++ } ++ + use_ssl = resource->scheme && (ZSTR_LEN(resource->scheme) > 4) && ZSTR_VAL(resource->scheme)[4] == 's'; + /* choose default ports */ + if (use_ssl && resource->port == 0) +@@ -205,6 +210,13 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper, + } + } + ++ if (request_fulluri && (strchr(path, '\n') != NULL || strchr(path, '\r') != NULL)) { ++ php_stream_wrapper_log_error(wrapper, options, "HTTP wrapper full URI path does not allow CR or LF characters"); ++ php_url_free(resource); ++ efree(transport_string); ++ return NULL; ++ } ++ + if (context && (tmpzval = php_stream_context_get_option(context, wrapper->wops->label, "timeout")) != NULL) { + double d = zval_get_double(tmpzval); + #ifndef PHP_WIN32 +@@ -385,12 +397,6 @@ finish: + smart_str_appends(&req_buf, "GET "); + } + +- /* Should we send the entire path in the request line, default to no. */ +- if (!request_fulluri && context && +- (tmpzval = php_stream_context_get_option(context, "http", "request_fulluri")) != NULL) { +- request_fulluri = zend_is_true(tmpzval); +- } +- + if (request_fulluri) { + /* Ask for everything */ + smart_str_appends(&req_buf, path); +diff --git a/ext/standard/tests/http/ghsa-c5f2-jwm7-mmq2.phpt b/ext/standard/tests/http/ghsa-c5f2-jwm7-mmq2.phpt +new file mode 100644 +index 00000000000..5b2e04f94f2 +--- /dev/null ++++ b/ext/standard/tests/http/ghsa-c5f2-jwm7-mmq2.phpt +@@ -0,0 +1,28 @@ ++--TEST-- ++GHSA-c5f2-jwm7-mmq2 (Configuring a proxy in a stream context might allow for CRLF injection in URIs) ++--INI-- ++allow_url_fopen=1 ++--CONFLICTS-- ++server ++--FILE-- ++<?php ++$serverCode = <<<'CODE' ++echo $_SERVER['REQUEST_URI']; ++CODE; ++ ++include __DIR__."/../../../../sapi/cli/tests/php_cli_server.inc"; ++php_cli_server_start($serverCode, null, []); ++ ++$host = PHP_CLI_SERVER_ADDRESS; ++$userinput = "index.php HTTP/1.1\r\nHost: $host\r\n\r\nGET /index2.php HTTP/1.1\r\nHost: $host\r\n\r\nGET /index.php"; ++$context = stream_context_create(['http' => ['proxy' => 'tcp://' . $host, 'request_fulluri' => true]]); ++echo file_get_contents("http://$host/$userinput", false, $context); ++?> ++--EXPECTF-- ++Warning: file_get_contents(http://localhost:%d/index.php HTTP/1.1 ++Host: localhost:%d ++ ++GET /index2.php HTTP/1.1 ++Host: localhost:%d ++ ++GET /index.php): failed to open stream: HTTP wrapper full URI path does not allow CR or LF characters in %s on line %d +-- +2.47.0 + diff --git a/php-cve-2024-11236.patch b/php-cve-2024-11236.patch new file mode 100644 index 0000000..e917cfc --- /dev/null +++ b/php-cve-2024-11236.patch @@ -0,0 +1,119 @@ +From 97546df8d6900b115536c17af9213f1da837b82e Mon Sep 17 00:00:00 2001 +From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> +Date: Thu, 24 Oct 2024 22:02:17 +0200 +Subject: [PATCH 1/7] Fix GHSA-5hqh-c84r-qjcv: Integer overflow in the dblib + quoter causing OOB writes + +(cherry picked from commit d9baa9fed8c3ba692a36b388c0c7762e5102e2e0) +(cherry picked from commit 5d9e54065ed18c51e4f25d8900635f90810c7394) +--- + ext/pdo_dblib/dblib_driver.c | 8 ++++++- + ext/pdo_dblib/tests/GHSA-5hqh-c84r-qjcv.phpt | 24 ++++++++++++++++++++ + 2 files changed, 31 insertions(+), 1 deletion(-) + create mode 100644 ext/pdo_dblib/tests/GHSA-5hqh-c84r-qjcv.phpt + +diff --git a/ext/pdo_dblib/dblib_driver.c b/ext/pdo_dblib/dblib_driver.c +index f36451afeeb..1dc75a4d2e3 100644 +--- a/ext/pdo_dblib/dblib_driver.c ++++ b/ext/pdo_dblib/dblib_driver.c +@@ -154,6 +154,7 @@ static int dblib_handle_quoter(pdo_dbh_t *dbh, const char *unquoted, size_t unqu + + size_t i; + char * q; ++ size_t extralen = 0; + *quotedlen = 0; + + if (H->assume_national_character_set_strings) { +@@ -168,7 +169,7 @@ static int dblib_handle_quoter(pdo_dbh_t *dbh, const char *unquoted, size_t unqu + + /* Detect quoted length, adding extra char for doubled single quotes */ + for (i = 0; i < unquotedlen; i++) { +- if (unquoted[i] == '\'') ++*quotedlen; ++ if (unquoted[i] == '\'') ++extralen; + ++*quotedlen; + } + +@@ -176,6 +177,11 @@ static int dblib_handle_quoter(pdo_dbh_t *dbh, const char *unquoted, size_t unqu + if (use_national_character_set) { + ++*quotedlen; /* N prefix */ + } ++ if (UNEXPECTED(*quotedlen > ZSTR_MAX_LEN - extralen)) { ++ return 0; ++ } ++ ++ *quotedlen += extralen; + q = *quoted = emalloc(*quotedlen + 1); /* Add byte for terminal null */ + if (use_national_character_set) { + *q++ = 'N'; +diff --git a/ext/pdo_dblib/tests/GHSA-5hqh-c84r-qjcv.phpt b/ext/pdo_dblib/tests/GHSA-5hqh-c84r-qjcv.phpt +new file mode 100644 +index 00000000000..431c61951ee +--- /dev/null ++++ b/ext/pdo_dblib/tests/GHSA-5hqh-c84r-qjcv.phpt +@@ -0,0 +1,24 @@ ++--TEST-- ++GHSA-5hqh-c84r-qjcv (Integer overflow in the dblib quoter causing OOB writes) ++--EXTENSIONS-- ++pdo_dblib ++--SKIPIF-- ++<?php ++if (PHP_INT_SIZE != 4) die("skip for 32bit platforms only"); ++if (PHP_OS_FAMILY === "Windows") die("skip not for Windows because the virtual address space for application is only 2GiB"); ++if (getenv("SKIP_SLOW_TESTS")) die("skip slow test"); ++require __DIR__ . '/config.inc'; ++getDbConnection(); ++?> ++--INI-- ++memory_limit=-1 ++--FILE-- ++<?php ++ ++require __DIR__ . '/config.inc'; ++$db = getDbConnection(); ++var_dump($db->quote(str_repeat("'", 2147483646))); ++ ++?> ++--EXPECT-- ++bool(false) +-- +2.47.0 + +From 0530cbfe5c3044537de52d8382eba5d69dbac726 Mon Sep 17 00:00:00 2001 +From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> +Date: Thu, 24 Oct 2024 22:02:36 +0200 +Subject: [PATCH 2/7] Fix GHSA-5hqh-c84r-qjcv: Integer overflow in the firebird + quoter causing OOB writes + +(cherry picked from commit 69c5f68fdc3deed9ebce2cc44b4bf5e0c47cd28f) +(cherry picked from commit b4f73be75dbdde970a18cc7a636898b10400fb3f) +--- + ext/pdo_firebird/firebird_driver.c | 6 +++++- + 1 file changed, 5 insertions(+), 1 deletion(-) + +diff --git a/ext/pdo_firebird/firebird_driver.c b/ext/pdo_firebird/firebird_driver.c +index 3e403afd368..5b74290abcc 100644 +--- a/ext/pdo_firebird/firebird_driver.c ++++ b/ext/pdo_firebird/firebird_driver.c +@@ -243,7 +243,7 @@ free_statement: + static int firebird_handle_quoter(pdo_dbh_t *dbh, const char *unquoted, size_t unquotedlen, /* {{{ */ + char **quoted, size_t *quotedlen, enum pdo_param_type paramtype) + { +- int qcount = 0; ++ size_t qcount = 0; + char const *co, *l, *r; + char *c; + +@@ -258,6 +258,10 @@ static int firebird_handle_quoter(pdo_dbh_t *dbh, const char *unquoted, size_t u + /* count the number of ' characters */ + for (co = unquoted; (co = strchr(co,'\'')); qcount++, co++); + ++ if (UNEXPECTED(unquotedlen + 2 > ZSTR_MAX_LEN - qcount)) { ++ return 0; ++ } ++ + *quotedlen = unquotedlen + qcount + 2; + *quoted = c = emalloc(*quotedlen+1); + *c++ = '\''; +-- +2.47.0 + diff --git a/php-cve-2024-8929.patch b/php-cve-2024-8929.patch new file mode 100644 index 0000000..fdc844d --- /dev/null +++ b/php-cve-2024-8929.patch @@ -0,0 +1,2714 @@ +From e8bc357123ea19c4e2390374f088c9d4941f19e6 Mon Sep 17 00:00:00 2001 +From: Jakub Zelenka <bukka@php.net> +Date: Tue, 8 Oct 2024 16:17:53 +0100 +Subject: [PATCH 1/6] Fix GHSA-h35g-vwh6-m678: Mysqlnd - various heap buffer + over-reads + +This fixes issues causing buffer over-read that leak heap content: +- RESP packet field default left over for COM_LIST +- RESP packet upsert filename +- OK packet message +- RESP packet for stmt row data + - ps_fetch_from_1_to_8_bytes + - ps_fetch_float + - ps_fetch_double + - ps_fetch_time + - ps_fetch_date + - ps_fetch_datetime + - ps_fetch_string + - ps_fetch_bit +- RESP packet for query row data (just possible overflow on 32bit) + +It also adds various protocol tests using a new fake server. + +(cherry picked from commit 2f5aa9f9d150ca56e356f3ca9acf9d530108cb08) +(cherry picked from commit 0d3ccf4cc54d3844bc9d1c8f6bdcd36180752a2c) + +adapt for 7.x +--- + ext/mysqli/tests/fake_server.inc | 856 ++++++++++++++++++ + .../ghsa-h35g-vwh6-m678-auth-message.phpt | 38 + + ext/mysqli/tests/ghsa-h35g-vwh6-m678-def.phpt | 47 + + .../tests/ghsa-h35g-vwh6-m678-filename.phpt | 43 + + ...hsa-h35g-vwh6-m678-query-len-overflow.phpt | 48 + + .../ghsa-h35g-vwh6-m678-stmt-row-bit.phpt | 53 ++ + .../ghsa-h35g-vwh6-m678-stmt-row-date.phpt | 53 ++ + ...ghsa-h35g-vwh6-m678-stmt-row-datetime.phpt | 53 ++ + .../ghsa-h35g-vwh6-m678-stmt-row-double.phpt | 53 ++ + .../ghsa-h35g-vwh6-m678-stmt-row-float.phpt | 53 ++ + .../ghsa-h35g-vwh6-m678-stmt-row-int.phpt | 53 ++ + ...ghsa-h35g-vwh6-m678-stmt-row-no-space.phpt | 53 ++ + .../ghsa-h35g-vwh6-m678-stmt-row-string.phpt | 53 ++ + .../ghsa-h35g-vwh6-m678-stmt-row-time.phpt | 53 ++ + .../tests/protocol_query_row_fetch_data.phpt | 74 ++ + .../tests/protocol_stmt_row_fetch_data.phpt | 91 ++ + ext/mysqlnd/mysqlnd_ps_codec.c | 69 ++ + ext/mysqlnd/mysqlnd_result.c | 2 +- + ext/mysqlnd/mysqlnd_wireprotocol.c | 71 +- + 19 files changed, 1794 insertions(+), 22 deletions(-) + create mode 100644 ext/mysqli/tests/fake_server.inc + create mode 100644 ext/mysqli/tests/ghsa-h35g-vwh6-m678-auth-message.phpt + create mode 100644 ext/mysqli/tests/ghsa-h35g-vwh6-m678-def.phpt + create mode 100644 ext/mysqli/tests/ghsa-h35g-vwh6-m678-filename.phpt + create mode 100644 ext/mysqli/tests/ghsa-h35g-vwh6-m678-query-len-overflow.phpt + create mode 100644 ext/mysqli/tests/ghsa-h35g-vwh6-m678-stmt-row-bit.phpt + create mode 100644 ext/mysqli/tests/ghsa-h35g-vwh6-m678-stmt-row-date.phpt + create mode 100644 ext/mysqli/tests/ghsa-h35g-vwh6-m678-stmt-row-datetime.phpt + create mode 100644 ext/mysqli/tests/ghsa-h35g-vwh6-m678-stmt-row-double.phpt + create mode 100644 ext/mysqli/tests/ghsa-h35g-vwh6-m678-stmt-row-float.phpt + create mode 100644 ext/mysqli/tests/ghsa-h35g-vwh6-m678-stmt-row-int.phpt + create mode 100644 ext/mysqli/tests/ghsa-h35g-vwh6-m678-stmt-row-no-space.phpt + create mode 100644 ext/mysqli/tests/ghsa-h35g-vwh6-m678-stmt-row-string.phpt + create mode 100644 ext/mysqli/tests/ghsa-h35g-vwh6-m678-stmt-row-time.phpt + create mode 100644 ext/mysqli/tests/protocol_query_row_fetch_data.phpt + create mode 100644 ext/mysqli/tests/protocol_stmt_row_fetch_data.phpt + +diff --git a/ext/mysqli/tests/fake_server.inc b/ext/mysqli/tests/fake_server.inc +new file mode 100644 +index 00000000000..b02fabc584c +--- /dev/null ++++ b/ext/mysqli/tests/fake_server.inc +@@ -0,0 +1,856 @@ ++<?php ++ ++function my_mysqli_data_fields(): array ++{ ++ return [ ++ 'intval' => [ ++ 'type' => '03', ++ 'charset' => '3f00', ++ 'length' => '0b000000', ++ 'flags' => '0110', ++ 'decimal' => '00', ++ 'query_data_packet_length' => '080000', ++ 'query_data_value' => '023134', ++ 'stmt_data_packet_length' => '0b0000', ++ 'stmt_data_value' => '0e000000' ++ ], ++ 'fltval' => [ ++ 'type' => '04', ++ 'charset' => '3f00', ++ 'length' => '0c000000', ++ 'flags' => '0110', ++ 'decimal' => '1f', ++ 'query_data_packet_length' => '090000', ++ 'query_data_value' => '03322e33', ++ 'stmt_data_packet_length' => '0b0000', ++ 'stmt_data_value' => '33331340', ++ ], ++ 'dblval' => [ ++ 'type' => '05', ++ 'charset' => '3f00', ++ 'length' => '16000000', ++ 'flags' => '0110', ++ 'decimal' => '1f', ++ 'query_data_packet_length' => '090000', ++ 'query_data_value' => '03312e32', ++ 'stmt_data_packet_length' => '0f0000', ++ 'stmt_data_value' => '333333333333f33f' ++ ], ++ 'datval' => [ ++ 'type' => '0a', ++ 'charset' => '3f00', ++ 'length' => '0a000000', ++ 'flags' => '8110', ++ 'decimal' => '00', ++ 'query_data_packet_length' => '100000', ++ 'query_data_value' => '0a323031342d31322d3135', ++ 'stmt_data_packet_length' => '0c0000', ++ 'stmt_data_value' => '04de070c0f' ++ ], ++ 'timval' => [ ++ 'type' => '0b', ++ 'charset' => '3f00', ++ 'length' => '0a000000', ++ 'flags' => '8110', ++ 'decimal' => '00', ++ 'query_data_packet_length' => '0e0000', ++ 'query_data_value' => '0831333a30303a3032', ++ 'stmt_data_packet_length' => '100000', ++ 'stmt_data_value' => '080000000000150801' ++ ], ++ 'dtival' => [ ++ 'type' => '0c', ++ 'charset' => '3f00', ++ 'length' => '13000000', ++ 'flags' => '8110', ++ 'decimal' => '00', ++ 'query_data_packet_length' => '190000', ++ 'query_data_value' => '13323031342d31322d31362031333a30303a3031', ++ 'stmt_data_packet_length' => '0f0000', ++ 'stmt_data_value' => '07de070c100d0001' ++ ], ++ 'bitval' => [ ++ 'type' => '10', ++ 'charset' => '3f00', ++ 'length' => '40000000', ++ 'flags' => '2110', ++ 'decimal' => '00', ++ 'query_data_packet_length' => '0e0000', ++ 'query_data_value' => '080808080808080808', ++ 'stmt_data_packet_length' => '100000', ++ 'stmt_data_value' => '080808080808080808' ++ ], ++ 'strval' => [ ++ 'type' => 'fd', ++ 'charset' => 'e000', ++ 'length' => 'c8000000', ++ 'flags' => '0110', ++ 'decimal' => '00', ++ 'query_data_packet_length' => '0a0000', ++ 'query_data_value' => '0474657374', ++ 'stmt_data_packet_length' => '0c0000', ++ 'stmt_data_value' => '0474657374' ++ ], ++ ]; ++} ++ ++function my_mysqli_data_field(string $field): array ++{ ++ $fields = my_mysqli_data_fields(); ++ if (!isset($fields[$field])) { ++ throw new Exception("Unknown field $field"); ++ } ++ return $fields[$field]; ++} ++ ++ ++ ++class my_mysqli_fake_packet_item ++{ ++ public function __construct(public string|null $name, public string $value, public bool $is_hex = true) ++ { ++ } ++} ++ ++class my_mysqli_fake_packet ++{ ++ private array $data = array(); ++ ++ public function __get(string $name) ++ { ++ foreach ($this->data as $item) { ++ if ($item->name === $name) { ++ return $item->value; ++ } ++ } ++ return null; ++ } ++ ++ public function __set(string $name, string|my_mysqli_fake_packet_item $value) ++ { ++ if ($value instanceof my_mysqli_fake_packet_item) { ++ if ($value->name === null) { ++ $value->name = $name; ++ } ++ } else { ++ $value = new my_mysqli_fake_packet_item($name, $value, true); ++ } ++ ++ for ($i = 0; $i < count($this->data); $i++) { ++ if ($this->data[$i]->name === $name) { ++ $this->data[$i] = $value; ++ return; ++ } ++ } ++ ++ $this->data[] = $value; ++ } ++ ++ public function to_bytes(): string ++ { ++ $bytes = ''; ++ foreach ($this->data as $item) { ++ $bytes .= $item->is_hex ? hex2bin($item->value) : $item->value; ++ } ++ return $bytes; ++ } ++} ++ ++class my_mysqli_fake_packet_generator ++{ ++ public static function create_packet_item(int|string $value, bool $is_hex = false, string $format = 'v'): my_mysqli_fake_packet_item ++ { ++ if (is_string($value)) { ++ $packed_value = $value; ++ } else { ++ $packed_value = pack($format, $value); ++ } ++ return new my_mysqli_fake_packet_item(null, $packed_value, $is_hex); ++ } ++ ++ public function server_ok(): my_mysqli_fake_packet ++ { ++ $packet = new my_mysqli_fake_packet(); ++ $packet->packet_length = "070000"; ++ $packet->packet_number = "02"; ++ $packet->header = "00"; // OK ++ $packet->affected_rows = "00"; ++ $packet->last_insert_id = "00"; ++ $packet->server_status = "0200"; ++ $packet->warning_count = "0000"; ++ return $packet; ++ } ++ ++ public function server_greetings(): my_mysqli_fake_packet ++ { ++ $packet = new my_mysqli_fake_packet(); ++ $packet->packet_length = "580000"; ++ $packet->packet_number = "00"; ++ $packet->proto_version = "0a"; ++ $packet->version = self::create_packet_item('5.5.5-10.5.18-MariaDB' . chr(0)); ++ $packet->thread_id = "03000000"; ++ $packet->salt = "473e3f6047257c67"; ++ $packet->filler = "00"; ++ $packet->server_capabilities = self::create_packet_item(0b1111011111111110); ++ $packet->server_character_set = "08"; ++ $packet->server_status = self::create_packet_item(0b000000000000010); ++ $packet->extended_server_capabilities = self::create_packet_item(0b1000000111111111); ++ $packet->auth_plugin = "15"; ++ $packet->unused = "000000000000"; ++ $packet->mariadb_extended_server_capabilities = self::create_packet_item(0b1111, false, 'V'); ++ $packet->mariadb_extended_server_capabilities_salt = "6c6b55463f49335f686c643100"; ++ $packet->mariadb_extended_server_capabilities_auth_plugin = self::create_packet_item('mysql_native_password'); ++ ++ return $packet; ++ } ++ ++ public function server_tabular_query_response(): array ++ { ++ $qr1 = new my_mysqli_fake_packet(); ++ $qr1->packet_length = "010000"; ++ $qr1->packet_number = "01"; ++ $qr1->field_count = "01"; ++ ++ $qr2 = new my_mysqli_fake_packet(); ++ $qr2->packet_length = "190000"; ++ $qr2->packet_number = "02"; ++ $qr2->catalog_length_plus_name = "0164"; ++ $qr2->db_length_plus_name = "0164"; ++ $qr2->table_length_plus_name = "0164"; ++ $qr2->original_t = "0164"; ++ $qr2->name_length_plus_name = "0164"; ++ $qr2->original_n = "0164"; ++ $qr2->canary = "0c"; ++ $qr2->charset = "3f00"; ++ $qr2->length = "0b000000"; ++ $qr2->type = "03"; ++ $qr2->flags = "0350"; ++ $qr2->decimals = "000000"; ++ ++ $qr3 = new my_mysqli_fake_packet(); ++ $qr3->full = "05000003fe00002200"; ++ ++ $qr4 = new my_mysqli_fake_packet(); ++ $qr4->full = "0400000401350174"; ++ ++ $qr5 = new my_mysqli_fake_packet(); ++ $qr5->full = "05000005fe00002200"; ++ ++ return [$qr1, $qr2, $qr3, $qr4, $qr5]; ++ } ++ ++ public function server_upsert_query_response(): array ++ { ++ $qr1 = new my_mysqli_fake_packet(); ++ $qr1->packet_length = "010000"; ++ $qr1->packet_number = "01"; ++ $qr1->field_count = "00"; // UPSERT ++ $qr1->affected_rows = "00"; ++ $qr1->affected_rows = "00"; ++ $qr1->last_insert_id = "00"; ++ $qr1->server_status = "0000"; ++ $qr1->warning_count = "0000"; ++ $qr1->len = "01"; ++ $qr1->filename = "65"; ++ $qr1->packet_length = sprintf("%02x0000", strlen($qr1->to_bytes())-4); ++ ++ return [$qr1]; ++ } ++ ++ public function server_stmt_prepare_response_start($num_field): my_mysqli_fake_packet ++ { ++ $pr1 = new my_mysqli_fake_packet(); ++ $pr1->packet_length = "0c0000"; ++ $pr1->packet_number = "01"; ++ $pr1->response_code = '00'; // OK ++ $pr1->statement_id = '01000000'; ++ $pr1->num_fields = $num_field; ++ $pr1->num_params = '0000'; ++ $pr1->filler = '00'; ++ $pr1->warnings = '0000'; ++ ++ return $pr1; ++ } ++ ++ public function server_stmt_prepare_response_end($packer_number): my_mysqli_fake_packet ++ { ++ $pr3 = new my_mysqli_fake_packet(); ++ $pr3->packet_length = "050000"; ++ $pr3->packet_number = $packer_number; ++ $pr3->packet_type = 'fe'; // EOF ++ $pr3->warnings = '0000'; ++ $pr3->server_status = '0200'; ++ ++ return $pr3; ++ } ++ ++ public function server_stmt_prepare_items_response(): array ++ { ++ $pr1 = $this->server_stmt_prepare_response_start('0100'); ++ ++ $pr2 = new my_mysqli_fake_packet(); ++ $pr2->packet_length = "300000"; ++ $pr2->packet_number = "02"; ++ $pr2->catalogue_len = '03'; ++ $pr2->catalogue = '646566'; // def ++ $pr2->db_len = '08'; ++ $pr2->db = '7068705f74657374'; // php_test ++ $pr2->table_len = '05'; ++ $pr2->table = '6974656d73'; // items ++ $pr2->orig_table_len = '05'; ++ $pr2->orig_table = '6974656d73'; // items ++ $pr2->name_len = '04'; ++ $pr2->name = '6974656d'; ++ $pr2->orig_name_len = '04'; ++ $pr2->orig_name = '6974656d'; ++ $pr2->something = '0c'; ++ $pr2->charset = 'e000'; ++ $pr2->length = 'c8000000'; ++ $pr2->field_type = 'fd'; // FIELD_TYPE_VAR_STRING ++ $pr2->flags = '0110'; ++ $pr2->decimal = '00'; ++ $pr2->padding = '0000'; ++ ++ $pr3 = $this->server_stmt_prepare_response_end('03'); ++ ++ return [$pr1, $pr2, $pr3]; ++ } ++ ++ public function server_stmt_prepare_data_response_field($packet_number, $field_name): my_mysqli_fake_packet ++ { ++ if (strlen($field_name) != 6) { ++ throw new Exception("Invalid field length - only 6 is allowed"); ++ } ++ ++ $field = my_mysqli_data_field($field_name); ++ ++ $pr = new my_mysqli_fake_packet(); ++ $pr->packet_length = "320000"; ++ $pr->packet_number = $packet_number; ++ $pr->catalogue_len = '03'; ++ $pr->catalogue = bin2hex('def'); ++ $pr->db_len = '08'; ++ $pr->db = bin2hex('php_test'); ++ $pr->table_len = '04'; ++ $pr->table = bin2hex('data'); ++ $pr->orig_table_len = '04'; ++ $pr->orig_table = bin2hex('data'); ++ $pr->name_len = '06'; ++ $pr->name = bin2hex($field_name); ++ $pr->orig_name_len = '06'; ++ $pr->orig_name = bin2hex($field_name); ++ $pr->something = '0c'; ++ $pr->charset = $field['charset']; ++ $pr->length = $field['length']; ++ $pr->field_type = $field['type']; ++ $pr->flags = $field['flags']; ++ $pr->decimal = $field['decimal']; ++ $pr->padding = '0000'; ++ ++ return $pr; ++ } ++ ++ public function server_stmt_prepare_data_response(string $field_name): array ++ { ++ $pr1 = $this->server_stmt_prepare_response_start('0200'); ++ ++ $pr2 = $this->server_stmt_prepare_data_response_field('02', 'strval'); ++ $pr3 = $this->server_stmt_prepare_data_response_field('03', $field_name); ++ ++ $pr4 = $this->server_stmt_prepare_response_end('04'); ++ ++ return [$pr1, $pr2, $pr3, $pr4]; ++ } ++ ++ public function server_stmt_execute_items_response(): array ++ { ++ $pr1 = new my_mysqli_fake_packet(); ++ $pr1->packet_length = "010000"; ++ $pr1->packet_number = "01"; ++ $pr1->num_fields = '01'; ++ ++ $pr2 = new my_mysqli_fake_packet(); ++ $pr2->packet_length = "300000"; ++ $pr2->packet_number = "02"; ++ $pr2->catalogue_len = '03'; ++ $pr2->catalogue = '646566'; // def ++ $pr2->db_len = '08'; ++ $pr2->db = '7068705f74657374'; // php_test ++ $pr2->table_len = '05'; ++ $pr2->table = '6974656d73'; // items ++ $pr2->orig_table_len = '05'; ++ $pr2->orig_table = '6974656d73'; // items ++ $pr2->name_len = '04'; ++ $pr2->name = '6974656d'; ++ $pr2->orig_name_len = '04'; ++ $pr2->orig_name = '6974656d'; ++ $pr2->something = '0c'; ++ $pr2->charset = 'e000'; ++ $pr2->length = 'c8000000'; ++ $pr2->field_type = 'fd'; // FIELD_TYPE_VAR_STRING ++ $pr2->flags = '0110'; ++ $pr2->decimal = '00'; ++ $pr2->padding = '0000'; ++ ++ $pr3 = new my_mysqli_fake_packet(); ++ $pr3->packet_length = "050000"; ++ $pr3->packet_number = "03"; ++ $pr3->packet_type = 'fe'; // EOF ++ $pr3->warnings = '0000'; ++ $pr3->server_status = '2200'; ++ ++ $pr4 = new my_mysqli_fake_packet(); ++ $pr4->packet_length = "070000"; ++ $pr4->packet_number = "04"; ++ $pr4->packet_type = '00'; // OK ++ $pr4->affected_rows = '00'; ++ $pr4->row_data_len = '04'; ++ $pr4->row_data = '74657374'; // item ++ ++ $pr5 = new my_mysqli_fake_packet(); ++ $pr5->full = '05000005fe00002200'; ++ ++ return [$pr1, $pr2, $pr3, $pr4, $pr5]; ++ } ++ ++ private function server_execute_data_response_start(string $field_name): array ++ { ++ $pr1 = new my_mysqli_fake_packet(); ++ $pr1->packet_length = "010000"; ++ $pr1->packet_number = "01"; ++ $pr1->num_fields = '02'; ++ ++ $pr2 = new my_mysqli_fake_packet(); ++ $pr2->packet_length = "320000"; ++ $pr2->packet_number = "02"; ++ $pr2->catalogue_len = '03'; ++ $pr2->catalogue = '646566'; // def ++ $pr2->db_len = '08'; ++ $pr2->db = '7068705f74657374'; // php_test ++ $pr2->table_len = '04'; ++ $pr2->table = bin2hex('data'); ++ $pr2->orig_table_len = '04'; ++ $pr2->orig_table = bin2hex('data'); ++ $pr2->name_len = '06'; ++ $pr2->name = bin2hex('strval'); ++ $pr2->orig_name_len = '06'; ++ $pr2->orig_name = bin2hex('strval'); ++ $pr2->something = '0c'; ++ $pr2->charset = 'e000'; ++ $pr2->length = 'c8000000'; ++ $pr2->field_type = 'fd'; // FIELD_TYPE_VAR_STRING ++ $pr2->flags = '0110'; ++ $pr2->decimal = '00'; ++ $pr2->padding = '0000'; ++ ++ $field = my_mysqli_data_field($field_name); ++ ++ $pr3 = new my_mysqli_fake_packet(); ++ $pr3->packet_length = "320000"; ++ $pr3->packet_number = "03"; ++ $pr3->catalogue_len = '03'; ++ $pr3->catalogue = '646566'; // def ++ $pr3->db_len = '08'; ++ $pr3->db = '7068705f74657374'; // php_test ++ $pr3->table_len = '04'; ++ $pr3->table = bin2hex('data'); ++ $pr3->orig_table_len = '04'; ++ $pr3->orig_table = bin2hex('data'); ++ $pr3->name_len = '06'; ++ $pr3->name = bin2hex($field_name); ++ $pr3->orig_name_len = '06'; ++ $pr3->orig_name = bin2hex($field_name); ++ $pr3->something = '0c'; ++ $pr3->charset = $field['charset']; ++ $pr3->length = $field['length']; ++ $pr3->field_type = $field['type']; ++ $pr3->flags = $field['flags']; ++ $pr3->decimal = $field['decimal']; ++ $pr3->padding = '0000'; ++ ++ $pr4 = new my_mysqli_fake_packet(); ++ $pr4->packet_length = "050000"; ++ $pr4->packet_number = "04"; ++ $pr4->packet_type = 'fe'; // EOF ++ $pr4->warnings = '0000'; ++ $pr4->server_status = '2200'; ++ ++ return [$field, $pr1, $pr2, $pr3, $pr4]; ++ } ++ ++ private function server_execute_data_response_end(): my_mysqli_fake_packet ++ { ++ $pr6 = new my_mysqli_fake_packet(); ++ $pr6->packet_length = '050000'; ++ $pr6->packet_number = "06"; ++ $pr6->packet_type = 'fe'; // EOF ++ $pr6->warnings = '0000'; ++ $pr6->server_status = '2200'; ++ ++ return $pr6; ++ } ++ ++ public function server_stmt_execute_data_response(string $field_name): array ++ { ++ [$field, $pr1, $pr2, $pr3, $pr4] = $this->server_execute_data_response_start($field_name); ++ ++ $pr5 = new my_mysqli_fake_packet(); ++ $pr5->packet_length = $field['stmt_data_packet_length']; ++ $pr5->packet_number = "05"; ++ $pr5->packet_type = '00'; // OK ++ $pr5->affected_rows = '00'; ++ $pr5->row_field1_len = '04'; ++ $pr5->row_field1_data = '74657374'; // test ++ $pr5->row_field2 = $field['stmt_data_value']; ++ ++ return [$pr1, $pr2, $pr3, $pr4, $pr5, $this->server_execute_data_response_end()]; ++ } ++ ++ public function server_query_execute_data_response(string $field_name): array ++ { ++ [$field, $pr1, $pr2, $pr3, $pr4] = $this->server_execute_data_response_start($field_name); ++ ++ $pr5 = new my_mysqli_fake_packet(); ++ $pr5->packet_length = $field['query_data_packet_length']; ++ $pr5->packet_number = "05"; ++ $pr5->row_field1_len = '04'; ++ $pr5->row_field1_data = '74657374'; // test ++ $pr5->row_field2 = $field['query_data_value']; ++ ++ return [$pr1, $pr2, $pr3, $pr4, $pr5, $this->server_execute_data_response_end()]; ++ } ++} ++ ++class my_mysqli_fake_server_conn ++{ ++ private $conn; ++ public $packet_generator; ++ ++ public function __construct($socket) ++ { ++ $this->packet_generator = new my_mysqli_fake_packet_generator(); ++ $this->conn = stream_socket_accept($socket); ++ if ($this->conn) { ++ fprintf(STDERR, "[*] Connection established\n"); ++ } else { ++ fprintf(STDERR, "[*] Failed to establish connection\n"); ++ } ++ } ++ ++ public function packets_to_bytes(array $packets): string ++ { ++ return implode('', array_map(fn($s) => $s->to_bytes(), $packets)); ++ } ++ ++ public function send($payload, $message = null): void ++ { ++ if ($message) { ++ fprintf(STDERR, "[*] Sending - %s: %s\n", $message, bin2hex($payload)); ++ } ++ fwrite($this->conn, $payload); ++ } ++ ++ public function read($bytes_len = 1024) ++ { ++ // wait 10ms to fill the buffer ++ usleep(10000); ++ $data = fread($this->conn, $bytes_len); ++ if ($data) { ++ fprintf(STDERR, "[*] Received: %s\n", bin2hex($data)); ++ } ++ } ++ ++ public function close() ++ { ++ fclose($this->conn); ++ } ++ ++ public function send_server_greetings() ++ { ++ $this->send($this->packet_generator->server_greetings()->to_bytes(), "Server Greeting"); ++ } ++ ++ public function send_server_ok() ++ { ++ $this->send($this->packet_generator->server_ok()->to_bytes(), "Server OK"); ++ } ++ ++ public function send_server_tabular_query_response(): void ++ { ++ $packets = $this->packet_generator->server_tabular_query_response(); ++ $this->send($this->packets_to_bytes($packets), "Tabular response"); ++ } ++ ++ public function send_server_stmt_prepare_items_response(): void ++ { ++ $packets = $this->packet_generator->server_stmt_prepare_items_response(); ++ $this->send($this->packets_to_bytes($packets), "Stmt prepare items"); ++ } ++ ++ ++ public function send_server_stmt_prepare_data_response(string $field_name): void ++ { ++ $packets = $this->packet_generator->server_stmt_prepare_data_response($field_name); ++ $this->send($this->packets_to_bytes($packets), "Stmt prepare data $field_name"); ++ } ++ ++ public function send_server_stmt_execute_items_response(): void ++ { ++ $packets = $this->packet_generator->server_stmt_execute_items_response(); ++ $this->send($this->packets_to_bytes($packets), "Stmt execute items"); ++ } ++ ++ public function send_server_stmt_execute_data_response(string $field_name): void ++ { ++ $packets = $this->packet_generator->server_stmt_execute_data_response($field_name); ++ $this->send($this->packets_to_bytes($packets), "Stmt execute data $field_name"); ++ } ++ ++ public function send_server_query_execute_data_response(string $field_name): void ++ { ++ $packets = $this->packet_generator->server_query_execute_data_response($field_name); ++ $this->send($this->packets_to_bytes($packets), "Query execute data $field_name"); ++ } ++} ++ ++class my_mysqli_fake_server_process ++{ ++ public function __construct(private $process, private array $pipes) {} ++ ++ public function terminate(bool $wait = false) ++ { ++ if ($wait) { ++ $this->wait(); ++ } ++ proc_terminate($this->process); ++ } ++ ++ public function wait() ++ { ++ echo fgets($this->pipes[1]); ++ } ++} ++ ++function my_mysqli_test_tabular_response_def_over_read(my_mysqli_fake_server_conn $conn): void ++{ ++ $rh = $conn->packet_generator->server_tabular_query_response(); ++ ++ // Length of the packet is modified to include the next added data ++ $rh[1]->packet_length = "1e0000"; ++ ++ // We add a length field encoded on 4 bytes which evaluates to 65536. If the process crashes because ++ // the heap has been overread, lower this value. ++ $rh[1]->extra_def_size = "fd000001"; # 65536 ++ ++ // Filler ++ $rh[1]->extra_def_data = "aa"; ++ ++ $trrh = $conn->packets_to_bytes($rh); ++ ++ $conn->send_server_greetings(); ++ $conn->read(); ++ $conn->send_server_ok(); ++ $conn->read(); ++ $conn->send($trrh, "Malicious Tabular Response [Extract heap through buffer over-read]"); ++ $conn->read(65536); ++} ++ ++function my_mysqli_test_upsert_response_filename_over_read(my_mysqli_fake_server_conn $conn): void ++{ ++ $rh = $conn->packet_generator->server_upsert_query_response(); ++ ++ // Set extra length to overread ++ $rh[0]->len = "fa"; ++ ++ $trrh = $conn->packets_to_bytes($rh); ++ ++ $conn->send_server_greetings(); ++ $conn->read(); ++ $conn->send_server_ok(); ++ $conn->read(); ++ $conn->send($trrh, "Malicious Tabular Response [Extract heap through buffer over-read]"); ++ $conn->read(65536); ++} ++ ++function my_mysqli_test_auth_response_message_over_read(my_mysqli_fake_server_conn $conn): void ++{ ++ $p = $conn->packet_generator->server_ok(); ++ $p->packet_length = "090000"; ++ $p->message_len = "fcff"; ++ ++ $conn->send_server_greetings(); ++ $conn->read(); ++ $conn->send($p->to_bytes(), "Malicious OK Auth Response [Extract heap through buffer over-read]"); ++ $conn->read(); ++} ++ ++function my_mysqli_test_stmt_response_row_over_read_string(my_mysqli_fake_server_conn $conn): void ++{ ++ $rh = $conn->packet_generator->server_stmt_execute_items_response(); ++ ++ // Set extra length to overread ++ $rh[3]->row_data_len = "fa"; ++ ++ $conn->send_server_greetings(); ++ $conn->read(); ++ $conn->send_server_ok(); ++ $conn->read(); ++ $conn->send_server_stmt_prepare_items_response(); ++ $conn->read(); ++ $conn->send($conn->packets_to_bytes($rh), "Malicious Stmt Response for items [Extract heap through buffer over-read]"); ++ $conn->read(65536); ++} ++ ++function my_mysqli_test_stmt_response_row_over_read_two_fields( ++ my_mysqli_fake_server_conn $conn, ++ string $field_name, ++ string $row_field1_len = '06' ++): void { ++ $rh = $conn->packet_generator->server_stmt_execute_data_response($field_name); ++ ++ // Set extra length to overread by two bytes ++ $rh[4]->row_field1_len = $row_field1_len; ++ ++ $conn->send_server_greetings(); ++ $conn->read(); ++ $conn->send_server_ok(); ++ $conn->read(); ++ $conn->send_server_stmt_prepare_data_response($field_name); ++ $conn->read(); ++ $conn->send( ++ $conn->packets_to_bytes($rh), ++ "Malicious Stmt Response for data $field_name [Extract heap through buffer over-read]" ++ ); ++ $conn->read(65536); ++} ++ ++function my_mysqli_test_stmt_response_row_over_read_int(my_mysqli_fake_server_conn $conn): void ++{ ++ my_mysqli_test_stmt_response_row_over_read_two_fields($conn, 'intval'); ++} ++ ++function my_mysqli_test_stmt_response_row_over_read_float(my_mysqli_fake_server_conn $conn): void ++{ ++ my_mysqli_test_stmt_response_row_over_read_two_fields($conn, 'fltval'); ++} ++ ++function my_mysqli_test_stmt_response_row_over_read_double(my_mysqli_fake_server_conn $conn): void ++{ ++ my_mysqli_test_stmt_response_row_over_read_two_fields($conn, 'dblval'); ++} ++ ++function my_mysqli_test_stmt_response_row_over_read_date(my_mysqli_fake_server_conn $conn): void ++{ ++ my_mysqli_test_stmt_response_row_over_read_two_fields($conn, 'datval'); ++} ++ ++function my_mysqli_test_stmt_response_row_over_read_time(my_mysqli_fake_server_conn $conn): void ++{ ++ my_mysqli_test_stmt_response_row_over_read_two_fields($conn, 'timval', '0c'); ++} ++ ++function my_mysqli_test_stmt_response_row_over_read_datetime(my_mysqli_fake_server_conn $conn): void ++{ ++ my_mysqli_test_stmt_response_row_over_read_two_fields($conn, 'dtival'); ++} ++ ++function my_mysqli_test_stmt_response_row_no_space(my_mysqli_fake_server_conn $conn): void ++{ ++ my_mysqli_test_stmt_response_row_over_read_two_fields($conn, 'strval', '09'); ++} ++ ++function my_mysqli_test_stmt_response_row_over_read_bit(my_mysqli_fake_server_conn $conn): void ++{ ++ my_mysqli_test_stmt_response_row_over_read_two_fields($conn, 'bitval'); ++} ++ ++function my_mysqli_test_stmt_response_row_read_two_fields(my_mysqli_fake_server_conn $conn): void ++{ ++ $conn->send_server_greetings(); ++ $conn->read(); ++ $conn->send_server_ok(); ++ $conn->read(); ++ $field_names = array_keys(my_mysqli_data_fields()); ++ foreach ($field_names as $field_name) { ++ $conn->send_server_stmt_prepare_data_response($field_name); ++ $conn->read(65536); ++ $conn->send_server_stmt_execute_data_response($field_name); ++ $conn->read(65536); ++ } ++} ++ ++function my_mysqli_test_query_response_row_length_overflow(my_mysqli_fake_server_conn $conn): void ++{ ++ $rh = $conn->packet_generator->server_query_execute_data_response('strval'); ++ ++ // Set extra length to overread by two bytes ++ $rh[4]->row_field2 = 'fefefefefe'; ++ ++ $conn->send_server_greetings(); ++ $conn->read(); ++ $conn->send_server_ok(); ++ $conn->read(); ++ $conn->send($conn->packets_to_bytes($rh), "Malicious Query Response for data strval field [length overflow]"); ++ $conn->read(65536); ++} ++ ++function my_mysqli_test_query_response_row_read_two_fields(my_mysqli_fake_server_conn $conn): void ++{ ++ $conn->send_server_greetings(); ++ $conn->read(); ++ $conn->send_server_ok(); ++ $conn->read(); ++ $field_names = array_keys(my_mysqli_data_fields()); ++ foreach ($field_names as $field_name) { ++ $conn->send_server_query_execute_data_response($field_name); ++ $conn->read(); ++ } ++} ++ ++function run_fake_server(string $test_function, $port = 33305): void ++{ ++ $address = '127.0.0.1'; ++ ++ $socket = @stream_socket_server("tcp://$address:$port", $errno, $errstr); ++ if (!$socket) { ++ die("Failed to create socket: $errstr ($errno)\n"); ++ } ++ echo "[*] Server started\n"; ++ ++ try { ++ $conn = new my_mysqli_fake_server_conn($socket); ++ $test_function_name = 'my_mysqli_test_' . $test_function; ++ call_user_func($test_function_name, $conn); ++ $conn->close(); ++ } catch (Exception $e) { ++ fprintf(STDERR, "[!] Exception: " . $e->getMessage() . "\n"); ++ } ++ ++ fclose($socket); ++ ++ echo "[*] Server finished\n"; ++} ++ ++ ++function run_fake_server_in_background($test_function, $port = 33305): my_mysqli_fake_server_process ++{ ++ $command = [PHP_BINARY, '-n', __FILE__, 'mysqli_fake_server', $test_function, $port]; ++ ++ $descriptorspec = array( ++ 0 => array("pipe", "r"), ++ 1 => array("pipe", "w"), ++ 2 => STDERR, ++ ); ++ ++ $process = proc_open($command, $descriptorspec, $pipes); ++ ++ if (is_resource($process)) { ++ return new my_mysqli_fake_server_process($process, $pipes); ++ } else { ++ throw new Exception("Failed to start server process"); ++ } ++} ++ ++if (isset($argv) && $argc > 2 && $argv[1] == 'mysqli_fake_server') { ++ run_fake_server($argv[2], $argv[3] ?? '33305'); ++} +diff --git a/ext/mysqli/tests/ghsa-h35g-vwh6-m678-auth-message.phpt b/ext/mysqli/tests/ghsa-h35g-vwh6-m678-auth-message.phpt +new file mode 100644 +index 00000000000..db54a6c0177 +--- /dev/null ++++ b/ext/mysqli/tests/ghsa-h35g-vwh6-m678-auth-message.phpt +@@ -0,0 +1,38 @@ ++--TEST-- ++GHSA-h35g-vwh6-m678 (mysqlnd leaks partial content of the heap - auth message buffer over-read) ++--EXTENSIONS-- ++mysqli ++--FILE-- ++<?php ++require_once 'fake_server.inc'; ++ ++$port = 50001; ++$servername = "127.0.0.1"; ++$username = "root"; ++$password = ""; ++ ++$process = run_fake_server_in_background('auth_response_message_over_read', $port); ++$process->wait(); ++ ++try { ++ $conn = new mysqli( $servername, $username, $password, "", $port ); ++ $info = mysqli_info($conn); ++ var_dump($info); ++} catch (Exception $e) { ++ echo $e->getMessage() . PHP_EOL; ++} ++ ++$process->terminate(); ++ ++print "done!"; ++?> ++--EXPECTF-- ++[*] Server started ++[*] Connection established ++[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 ++[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 ++[*] Sending - Malicious OK Auth Response [Extract heap through buffer over-read]: 0900000200000002000000fcff ++ ++Warning: mysqli::__construct(): OK packet message length is past the packet size in %s on line %d ++Unknown error while trying to connect via tcp://127.0.0.1:50001 ++done! +diff --git a/ext/mysqli/tests/ghsa-h35g-vwh6-m678-def.phpt b/ext/mysqli/tests/ghsa-h35g-vwh6-m678-def.phpt +new file mode 100644 +index 00000000000..77f2232eca6 +--- /dev/null ++++ b/ext/mysqli/tests/ghsa-h35g-vwh6-m678-def.phpt +@@ -0,0 +1,47 @@ ++--TEST-- ++GHSA-h35g-vwh6-m678 (mysqlnd leaks partial content of the heap - tabular default) ++--EXTENSIONS-- ++mysqli ++--FILE-- ++<?php ++require_once 'fake_server.inc'; ++ ++ ++$port = 33305; ++$servername = "127.0.0.1"; ++$username = "root"; ++$password = ""; ++ ++$process = run_fake_server_in_background('tabular_response_def_over_read', $port); ++$process->wait(); ++ ++$conn = new mysqli($servername, $username, $password, "", $port); ++ ++echo "[*] Running query on the fake server...\n"; ++ ++$result = $conn->query("SELECT * from users"); ++ ++if ($result) { ++ $all_fields = $result->fetch_fields(); ++ var_dump($result->fetch_all(MYSQLI_ASSOC)); ++ var_dump(get_object_vars($all_fields[0])["def"]); ++} ++ ++$conn->close(); ++ ++$process->terminate(); ++ ++print "done!"; ++?> ++--EXPECTF-- ++[*] Server started ++[*] Connection established ++[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 ++[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 ++[*] Sending - Server OK: 0700000200000002000000 ++[*] Running query on the fake server... ++[*] Received: 140000000353454c454354202a2066726f6d207573657273 ++[*] Sending - Malicious Tabular Response [Extract heap through buffer over-read]: 01000001011e0000020164016401640164016401640c3f000b000000030350000000fd000001aa05000003fe00002200040000040135017405000005fe00002200 ++ ++Warning: mysqli::query(): Protocol error. Server sent default for unsupported field list (mysqlnd_wireprotocol.c:%d) in %s on line %d ++done! +diff --git a/ext/mysqli/tests/ghsa-h35g-vwh6-m678-filename.phpt b/ext/mysqli/tests/ghsa-h35g-vwh6-m678-filename.phpt +new file mode 100644 +index 00000000000..0b4db8ccece +--- /dev/null ++++ b/ext/mysqli/tests/ghsa-h35g-vwh6-m678-filename.phpt +@@ -0,0 +1,43 @@ ++--TEST-- ++GHSA-h35g-vwh6-m678 (mysqlnd leaks partial content of the heap - upsert filename buffer over-read) ++--EXTENSIONS-- ++mysqli ++--FILE-- ++<?php ++require_once 'fake_server.inc'; ++ ++$port = 33305; ++$servername = "127.0.0.1"; ++$username = "root"; ++$password = ""; ++ ++$process = run_fake_server_in_background('upsert_response_filename_over_read', $port); ++$process->wait(); ++ ++$conn = new mysqli($servername, $username, $password, "", $port); ++echo "[*] Running query on the fake server...\n"; ++ ++$result = $conn->query("SELECT * from users"); ++$info = mysqli_info($conn); ++ ++var_dump($info); ++ ++$process->terminate(); ++ ++print "done!"; ++?> ++--EXPECTF-- ++[*] Server started ++[*] Connection established ++[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 ++[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 ++[*] Sending - Server OK: 0700000200000002000000 ++[*] Running query on the fake server... ++[*] Received: 140000000353454c454354202a2066726f6d207573657273 ++[*] Sending - Malicious Tabular Response [Extract heap through buffer over-read]: 0900000100000000000000fa65 ++ ++Warning: mysqli::query(): RSET_HEADER packet additional data length is past 249 bytes the packet size in %s on line %d ++ ++Warning: mysqli::query(): Error reading result set's header in %s on line %d ++NULL ++done! +diff --git a/ext/mysqli/tests/ghsa-h35g-vwh6-m678-query-len-overflow.phpt b/ext/mysqli/tests/ghsa-h35g-vwh6-m678-query-len-overflow.phpt +new file mode 100644 +index 00000000000..f141a79bdaa +--- /dev/null ++++ b/ext/mysqli/tests/ghsa-h35g-vwh6-m678-query-len-overflow.phpt +@@ -0,0 +1,48 @@ ++--TEST-- ++GHSA-h35g-vwh6-m678 (mysqlnd leaks partial content of the heap - stmt row no space for the field) ++--EXTENSIONS-- ++mysqli ++--FILE-- ++<?php ++require_once 'fake_server.inc'; ++ ++$port = 33305; ++$servername = "127.0.0.1"; ++$username = "root"; ++$password = ""; ++ ++$process = run_fake_server_in_background('query_response_row_length_overflow', $port); ++$process->wait(); ++ ++$conn = new mysqli($servername, $username, $password, "", $port); ++ ++echo "[*] Query the fake server...\n"; ++$sql = "SELECT strval, strval FROM data"; ++ ++$result = $conn->query($sql); ++ ++if ($result->num_rows > 0) { ++ while ($row = $result->fetch_assoc()) { ++ var_dump($row['strval']); ++ } ++} ++$conn->close(); ++ ++$process->terminate(true); ++ ++print "done!"; ++?> ++--EXPECTF-- ++[*] Server started ++[*] Connection established ++[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 ++[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 ++[*] Sending - Server OK: 0700000200000002000000 ++[*] Query the fake server... ++[*] Received: 200000000353454c4543542073747276616c2c2073747276616c2046524f4d2064617461 ++[*] Sending - Malicious Query Response for data strval field [length overflow]: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd011000000005000004fe000022000a0000050474657374fefefefefe05000006fe00002200 ++ ++Warning: mysqli_result::fetch_assoc(): Malformed server packet. Field length pointing after end of packet in %s on line %d ++[*] Received: 0100000001 ++[*] Server finished ++done! +diff --git a/ext/mysqli/tests/ghsa-h35g-vwh6-m678-stmt-row-bit.phpt b/ext/mysqli/tests/ghsa-h35g-vwh6-m678-stmt-row-bit.phpt +new file mode 100644 +index 00000000000..e43518217eb +--- /dev/null ++++ b/ext/mysqli/tests/ghsa-h35g-vwh6-m678-stmt-row-bit.phpt +@@ -0,0 +1,53 @@ ++--TEST-- ++GHSA-h35g-vwh6-m678 (mysqlnd leaks partial content of the heap - stmt row bit buffer over-read) ++--EXTENSIONS-- ++mysqli ++--FILE-- ++<?php ++require_once 'fake_server.inc'; ++ ++$port = 33305; ++$servername = "127.0.0.1"; ++$username = "root"; ++$password = ""; ++ ++$process = run_fake_server_in_background('stmt_response_row_over_read_bit', $port); ++$process->wait(); ++ ++$conn = new mysqli($servername, $username, $password, "", $port); ++ ++echo "[*] Preparing statement on the fake server...\n"; ++$stmt = $conn->prepare("SELECT bitval, timval FROM data"); ++ ++$stmt->execute(); ++$result = $stmt->get_result(); ++ ++// Fetch and display the results ++if ($result->num_rows > 0) { ++ while ($row = $result->fetch_assoc()) { ++ var_dump($row["bitval"]); ++ } ++} ++$stmt->close(); ++$conn->close(); ++ ++$process->terminate(true); ++ ++print "done!"; ++?> ++--EXPECTF-- ++[*] Server started ++[*] Connection established ++[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 ++[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 ++[*] Sending - Server OK: 0700000200000002000000 ++[*] Preparing statement on the fake server... ++[*] Received: 200000001653454c4543542062697476616c2c2074696d76616c2046524f4d2064617461 ++[*] Sending - Stmt prepare data bitval: 0c0000010001000000020000000000003200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610662697476616c0662697476616c0c3f004000000010211000000005000004fe00000200 ++[*] Received: 0a00000017010000000001000000 ++[*] Sending - Malicious Stmt Response for data bitval [Extract heap through buffer over-read]: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610662697476616c0662697476616c0c3f004000000010211000000005000004fe00002200100000050000067465737408080808080808080805000006fe00002200 ++ ++Warning: mysqli_result::fetch_assoc(): Malformed server packet. Field length pointing after the end of packet in %s on line %d ++[*] Received: 0500000019010000000100000001 ++[*] Server finished ++done! +diff --git a/ext/mysqli/tests/ghsa-h35g-vwh6-m678-stmt-row-date.phpt b/ext/mysqli/tests/ghsa-h35g-vwh6-m678-stmt-row-date.phpt +new file mode 100644 +index 00000000000..76158e940d0 +--- /dev/null ++++ b/ext/mysqli/tests/ghsa-h35g-vwh6-m678-stmt-row-date.phpt +@@ -0,0 +1,53 @@ ++--TEST-- ++GHSA-h35g-vwh6-m678 (mysqlnd leaks partial content of the heap - stmt row date buffer over-read) ++--EXTENSIONS-- ++mysqli ++--FILE-- ++<?php ++require_once 'fake_server.inc'; ++ ++$port = 33305; ++$servername = "127.0.0.1"; ++$username = "root"; ++$password = ""; ++ ++$process = run_fake_server_in_background('stmt_response_row_over_read_date', $port); ++$process->wait(); ++ ++$conn = new mysqli($servername, $username, $password, "", $port); ++ ++echo "[*] Preparing statement on the fake server...\n"; ++$stmt = $conn->prepare("SELECT strval, datval FROM data"); ++ ++$stmt->execute(); ++$result = $stmt->get_result(); ++ ++// Fetch and display the results ++if ($result->num_rows > 0) { ++ while ($row = $result->fetch_assoc()) { ++ var_dump($row["datval"]); ++ } ++} ++$stmt->close(); ++$conn->close(); ++ ++$process->terminate(true); ++ ++print "done!"; ++?> ++--EXPECTF-- ++[*] Server started ++[*] Connection established ++[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 ++[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 ++[*] Sending - Server OK: 0700000200000002000000 ++[*] Preparing statement on the fake server... ++[*] Received: 200000001653454c4543542073747276616c2c2064617476616c2046524f4d2064617461 ++[*] Sending - Stmt prepare data datval: 0c0000010001000000020000000000003200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610664617476616c0664617476616c0c3f000a0000000a811000000005000004fe00000200 ++[*] Received: 0a00000017010000000001000000 ++[*] Sending - Malicious Stmt Response for data datval [Extract heap through buffer over-read]: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610664617476616c0664617476616c0c3f000a0000000a811000000005000004fe000022000c0000050000067465737404de070c0f05000006fe00002200 ++ ++Warning: mysqli_result::fetch_assoc(): Malformed server packet. Field length pointing after the end of packet in %s on line %d ++[*] Received: 0500000019010000000100000001 ++[*] Server finished ++done! +diff --git a/ext/mysqli/tests/ghsa-h35g-vwh6-m678-stmt-row-datetime.phpt b/ext/mysqli/tests/ghsa-h35g-vwh6-m678-stmt-row-datetime.phpt +new file mode 100644 +index 00000000000..f53d5b83bd4 +--- /dev/null ++++ b/ext/mysqli/tests/ghsa-h35g-vwh6-m678-stmt-row-datetime.phpt +@@ -0,0 +1,53 @@ ++--TEST-- ++GHSA-h35g-vwh6-m678 (mysqlnd leaks partial content of the heap - stmt row datetime buffer over-read) ++--EXTENSIONS-- ++mysqli ++--FILE-- ++<?php ++require_once 'fake_server.inc'; ++ ++$port = 33305; ++$servername = "127.0.0.1"; ++$username = "root"; ++$password = ""; ++ ++$process = run_fake_server_in_background('stmt_response_row_over_read_datetime', $port); ++$process->wait(); ++ ++$conn = new mysqli($servername, $username, $password, "", $port); ++ ++echo "[*] Preparing statement on the fake server...\n"; ++$stmt = $conn->prepare("SELECT strval, dtival FROM data"); ++ ++$stmt->execute(); ++$result = $stmt->get_result(); ++ ++// Fetch and display the results ++if ($result->num_rows > 0) { ++ while ($row = $result->fetch_assoc()) { ++ var_dump($row["dtival"]); ++ } ++} ++$stmt->close(); ++$conn->close(); ++ ++$process->terminate(true); ++ ++print "done!"; ++?> ++--EXPECTF-- ++[*] Server started ++[*] Connection established ++[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 ++[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 ++[*] Sending - Server OK: 0700000200000002000000 ++[*] Preparing statement on the fake server... ++[*] Received: 200000001653454c4543542073747276616c2c2064746976616c2046524f4d2064617461 ++[*] Sending - Stmt prepare data dtival: 0c0000010001000000020000000000003200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610664746976616c0664746976616c0c3f00130000000c811000000005000004fe00000200 ++[*] Received: 0a00000017010000000001000000 ++[*] Sending - Malicious Stmt Response for data dtival [Extract heap through buffer over-read]: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610664746976616c0664746976616c0c3f00130000000c811000000005000004fe000022000f0000050000067465737407de070c100d000105000006fe00002200 ++ ++Warning: mysqli_result::fetch_assoc(): Malformed server packet. Field length pointing after the end of packet in %s on line %d ++[*] Received: 0500000019010000000100000001 ++[*] Server finished ++done! +diff --git a/ext/mysqli/tests/ghsa-h35g-vwh6-m678-stmt-row-double.phpt b/ext/mysqli/tests/ghsa-h35g-vwh6-m678-stmt-row-double.phpt +new file mode 100644 +index 00000000000..03c9b045d73 +--- /dev/null ++++ b/ext/mysqli/tests/ghsa-h35g-vwh6-m678-stmt-row-double.phpt +@@ -0,0 +1,53 @@ ++--TEST-- ++GHSA-h35g-vwh6-m678 (mysqlnd leaks partial content of the heap - stmt row double buffer over-read) ++--EXTENSIONS-- ++mysqli ++--FILE-- ++<?php ++require_once 'fake_server.inc'; ++ ++$port = 33305; ++$servername = "127.0.0.1"; ++$username = "root"; ++$password = ""; ++ ++$process = run_fake_server_in_background('stmt_response_row_over_read_double', $port); ++$process->wait(); ++ ++$conn = new mysqli($servername, $username, $password, "", $port); ++ ++echo "[*] Preparing statement on the fake server...\n"; ++$stmt = $conn->prepare("SELECT strval, dblval FROM data"); ++ ++$stmt->execute(); ++$result = $stmt->get_result(); ++ ++// Fetch and display the results ++if ($result->num_rows > 0) { ++ while ($row = $result->fetch_assoc()) { ++ var_dump($row["dblval"]); ++ } ++} ++$stmt->close(); ++$conn->close(); ++ ++$process->terminate(true); ++ ++print "done!"; ++?> ++--EXPECTF-- ++[*] Server started ++[*] Connection established ++[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 ++[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 ++[*] Sending - Server OK: 0700000200000002000000 ++[*] Preparing statement on the fake server... ++[*] Received: 200000001653454c4543542073747276616c2c2064626c76616c2046524f4d2064617461 ++[*] Sending - Stmt prepare data dblval: 0c0000010001000000020000000000003200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610664626c76616c0664626c76616c0c3f00160000000501101f000005000004fe00000200 ++[*] Received: 0a00000017010000000001000000 ++[*] Sending - Malicious Stmt Response for data dblval [Extract heap through buffer over-read]: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610664626c76616c0664626c76616c0c3f00160000000501101f000005000004fe000022000f00000500000674657374333333333333f33f05000006fe00002200 ++ ++Warning: mysqli_result::fetch_assoc(): Malformed server packet. Field length pointing after the end of packet in %s on line %d ++[*] Received: 0500000019010000000100000001 ++[*] Server finished ++done! +diff --git a/ext/mysqli/tests/ghsa-h35g-vwh6-m678-stmt-row-float.phpt b/ext/mysqli/tests/ghsa-h35g-vwh6-m678-stmt-row-float.phpt +new file mode 100644 +index 00000000000..b1ec9aa51ec +--- /dev/null ++++ b/ext/mysqli/tests/ghsa-h35g-vwh6-m678-stmt-row-float.phpt +@@ -0,0 +1,53 @@ ++--TEST-- ++GHSA-h35g-vwh6-m678 (mysqlnd leaks partial content of the heap - stmt row int buffer over-read) ++--EXTENSIONS-- ++mysqli ++--FILE-- ++<?php ++require_once 'fake_server.inc'; ++ ++$port = 33305; ++$servername = "127.0.0.1"; ++$username = "root"; ++$password = ""; ++ ++$process = run_fake_server_in_background('stmt_response_row_over_read_float', $port); ++$process->wait(); ++ ++$conn = new mysqli($servername, $username, $password, "", $port); ++ ++echo "[*] Preparing statement on the fake server...\n"; ++$stmt = $conn->prepare("SELECT strval, fltval FROM data"); ++ ++$stmt->execute(); ++$result = $stmt->get_result(); ++ ++// Fetch and display the results ++if ($result->num_rows > 0) { ++ while ($row = $result->fetch_assoc()) { ++ var_dump($row["fltval"]); ++ } ++} ++$stmt->close(); ++$conn->close(); ++ ++$process->terminate(true); ++ ++print "done!"; ++?> ++--EXPECTF-- ++[*] Server started ++[*] Connection established ++[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 ++[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 ++[*] Sending - Server OK: 0700000200000002000000 ++[*] Preparing statement on the fake server... ++[*] Received: 200000001653454c4543542073747276616c2c20666c7476616c2046524f4d2064617461 ++[*] Sending - Stmt prepare data fltval: 0c0000010001000000020000000000003200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f746573740464617461046461746106666c7476616c06666c7476616c0c3f000c0000000401101f000005000004fe00000200 ++[*] Received: 0a00000017010000000001000000 ++[*] Sending - Malicious Stmt Response for data fltval [Extract heap through buffer over-read]: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f746573740464617461046461746106666c7476616c06666c7476616c0c3f000c0000000401101f000005000004fe000022000b000005000006746573743333134005000006fe00002200 ++ ++Warning: mysqli_result::fetch_assoc(): Malformed server packet. Field length pointing after the end of packet in %s on line %d ++[*] Received: 0500000019010000000100000001 ++[*] Server finished ++done! +diff --git a/ext/mysqli/tests/ghsa-h35g-vwh6-m678-stmt-row-int.phpt b/ext/mysqli/tests/ghsa-h35g-vwh6-m678-stmt-row-int.phpt +new file mode 100644 +index 00000000000..426d9ea7b3f +--- /dev/null ++++ b/ext/mysqli/tests/ghsa-h35g-vwh6-m678-stmt-row-int.phpt +@@ -0,0 +1,53 @@ ++--TEST-- ++GHSA-h35g-vwh6-m678 (mysqlnd leaks partial content of the heap - stmt row int buffer over-read) ++--EXTENSIONS-- ++mysqli ++--FILE-- ++<?php ++require_once 'fake_server.inc'; ++ ++$port = 33305; ++$servername = "127.0.0.1"; ++$username = "root"; ++$password = ""; ++ ++$process = run_fake_server_in_background('stmt_response_row_over_read_int', $port); ++$process->wait(); ++ ++$conn = new mysqli($servername, $username, $password, "", $port); ++ ++echo "[*] Preparing statement on the fake server...\n"; ++$stmt = $conn->prepare("SELECT strval, intval FROM data"); ++ ++$stmt->execute(); ++$result = $stmt->get_result(); ++ ++// Fetch and display the results ++if ($result->num_rows > 0) { ++ while ($row = $result->fetch_assoc()) { ++ var_dump($row["intval"]); ++ } ++} ++$stmt->close(); ++$conn->close(); ++ ++$process->terminate(true); ++ ++print "done!"; ++?> ++--EXPECTF-- ++[*] Server started ++[*] Connection established ++[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 ++[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 ++[*] Sending - Server OK: 0700000200000002000000 ++[*] Preparing statement on the fake server... ++[*] Received: 200000001653454c4543542073747276616c2c20696e7476616c2046524f4d2064617461 ++[*] Sending - Stmt prepare data intval: 0c0000010001000000020000000000003200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f746573740464617461046461746106696e7476616c06696e7476616c0c3f000b00000003011000000005000004fe00000200 ++[*] Received: 0a00000017010000000001000000 ++[*] Sending - Malicious Stmt Response for data intval [Extract heap through buffer over-read]: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f746573740464617461046461746106696e7476616c06696e7476616c0c3f000b00000003011000000005000004fe000022000b000005000006746573740e00000005000006fe00002200 ++ ++Warning: mysqli_result::fetch_assoc(): Malformed server packet. Field length pointing after the end of packet in %s on line %d ++[*] Received: 0500000019010000000100000001 ++[*] Server finished ++done! +diff --git a/ext/mysqli/tests/ghsa-h35g-vwh6-m678-stmt-row-no-space.phpt b/ext/mysqli/tests/ghsa-h35g-vwh6-m678-stmt-row-no-space.phpt +new file mode 100644 +index 00000000000..6db6952d42a +--- /dev/null ++++ b/ext/mysqli/tests/ghsa-h35g-vwh6-m678-stmt-row-no-space.phpt +@@ -0,0 +1,53 @@ ++--TEST-- ++GHSA-h35g-vwh6-m678 (mysqlnd leaks partial content of the heap - stmt row no space for the field) ++--EXTENSIONS-- ++mysqli ++--FILE-- ++<?php ++require_once 'fake_server.inc'; ++ ++$port = 33305; ++$servername = "127.0.0.1"; ++$username = "root"; ++$password = ""; ++ ++$process = run_fake_server_in_background('stmt_response_row_no_space', $port); ++$process->wait(); ++ ++$conn = new mysqli($servername, $username, $password, "", $port); ++ ++echo "[*] Preparing statement on the fake server...\n"; ++$stmt = $conn->prepare("SELECT strval, strval FROM data"); ++ ++$stmt->execute(); ++$result = $stmt->get_result(); ++ ++// Fetch and display the results ++if ($result->num_rows > 0) { ++ while ($row = $result->fetch_assoc()) { ++ var_dump($row["strval"]); ++ } ++} ++$stmt->close(); ++$conn->close(); ++ ++$process->terminate(true); ++ ++print "done!"; ++?> ++--EXPECTF-- ++[*] Server started ++[*] Connection established ++[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 ++[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 ++[*] Sending - Server OK: 0700000200000002000000 ++[*] Preparing statement on the fake server... ++[*] Received: 200000001653454c4543542073747276616c2c2073747276616c2046524f4d2064617461 ++[*] Sending - Stmt prepare data strval: 0c0000010001000000020000000000003200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd011000000005000004fe00000200 ++[*] Received: 0a00000017010000000001000000 ++[*] Sending - Malicious Stmt Response for data strval [Extract heap through buffer over-read]: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd011000000005000004fe000022000c00000500000974657374047465737405000006fe00002200 ++ ++Warning: mysqli_result::fetch_assoc(): Malformed server packet. No packet space left for the field in %s on line %d ++[*] Received: 0500000019010000000100000001 ++[*] Server finished ++done! +diff --git a/ext/mysqli/tests/ghsa-h35g-vwh6-m678-stmt-row-string.phpt b/ext/mysqli/tests/ghsa-h35g-vwh6-m678-stmt-row-string.phpt +new file mode 100644 +index 00000000000..55bad4cc544 +--- /dev/null ++++ b/ext/mysqli/tests/ghsa-h35g-vwh6-m678-stmt-row-string.phpt +@@ -0,0 +1,53 @@ ++--TEST-- ++GHSA-h35g-vwh6-m678 (mysqlnd leaks partial content of the heap - stmt row string buffer over-read) ++--EXTENSIONS-- ++mysqli ++--FILE-- ++<?php ++require_once 'fake_server.inc'; ++ ++$port = 33305; ++$servername = "127.0.0.1"; ++$username = "root"; ++$password = ""; ++ ++$process = run_fake_server_in_background('stmt_response_row_over_read_string', $port); ++$process->wait(); ++ ++$conn = new mysqli($servername, $username, $password, "", $port); ++ ++echo "[*] Preparing statement on the fake server...\n"; ++$stmt = $conn->prepare("SELECT item FROM items"); ++ ++$stmt->execute(); ++$result = $stmt->get_result(); ++ ++// Fetch and display the results ++if ($result->num_rows > 0) { ++ while ($row = $result->fetch_assoc()) { ++ var_dump($row["item"]); ++ } ++} ++$stmt->close(); ++$conn->close(); ++ ++$process->terminate(true); ++ ++print "done!"; ++?> ++--EXPECTF-- ++[*] Server started ++[*] Connection established ++[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 ++[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 ++[*] Sending - Server OK: 0700000200000002000000 ++[*] Preparing statement on the fake server... ++[*] Received: 170000001653454c454354206974656d2046524f4d206974656d73 ++[*] Sending - Stmt prepare items: 0c0000010001000000010000000000003000000203646566087068705f74657374056974656d73056974656d73046974656d046974656d0ce000c8000000fd011000000005000003fe00000200 ++[*] Received: 0a00000017010000000001000000 ++[*] Sending - Malicious Stmt Response for items [Extract heap through buffer over-read]: 01000001013000000203646566087068705f74657374056974656d73056974656d73046974656d046974656d0ce000c8000000fd011000000005000003fe00002200070000040000fa7465737405000005fe00002200 ++ ++Warning: mysqli_result::fetch_assoc(): Malformed server packet. Field length pointing after the end of packet in %s on line %d ++[*] Received: 0500000019010000000100000001 ++[*] Server finished ++done! +diff --git a/ext/mysqli/tests/ghsa-h35g-vwh6-m678-stmt-row-time.phpt b/ext/mysqli/tests/ghsa-h35g-vwh6-m678-stmt-row-time.phpt +new file mode 100644 +index 00000000000..06918c375f3 +--- /dev/null ++++ b/ext/mysqli/tests/ghsa-h35g-vwh6-m678-stmt-row-time.phpt +@@ -0,0 +1,53 @@ ++--TEST-- ++GHSA-h35g-vwh6-m678 (mysqlnd leaks partial content of the heap - stmt row time buffer over-read) ++--EXTENSIONS-- ++mysqli ++--FILE-- ++<?php ++require_once 'fake_server.inc'; ++ ++$port = 33305; ++$servername = "127.0.0.1"; ++$username = "root"; ++$password = ""; ++ ++$process = run_fake_server_in_background('stmt_response_row_over_read_time', $port); ++$process->wait(); ++ ++$conn = new mysqli($servername, $username, $password, "", $port); ++ ++echo "[*] Preparing statement on the fake server...\n"; ++$stmt = $conn->prepare("SELECT strval, timval FROM data"); ++ ++$stmt->execute(); ++$result = $stmt->get_result(); ++ ++// Fetch and display the results ++if ($result->num_rows > 0) { ++ while ($row = $result->fetch_assoc()) { ++ var_dump($row["timval"]); ++ } ++} ++$stmt->close(); ++$conn->close(); ++ ++$process->terminate(true); ++ ++print "done!"; ++?> ++--EXPECTF-- ++[*] Server started ++[*] Connection established ++[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 ++[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 ++[*] Sending - Server OK: 0700000200000002000000 ++[*] Preparing statement on the fake server... ++[*] Received: 200000001653454c4543542073747276616c2c2074696d76616c2046524f4d2064617461 ++[*] Sending - Stmt prepare data timval: 0c0000010001000000020000000000003200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610674696d76616c0674696d76616c0c3f000a0000000b811000000005000004fe00000200 ++[*] Received: 0a00000017010000000001000000 ++[*] Sending - Malicious Stmt Response for data timval [Extract heap through buffer over-read]: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610674696d76616c0674696d76616c0c3f000a0000000b811000000005000004fe000022001000000500000c7465737408000000000015080105000006fe00002200 ++ ++Warning: mysqli_result::fetch_assoc(): Malformed server packet. Field length pointing after the end of packet in %s on line %d ++[*] Received: 0500000019010000000100000001 ++[*] Server finished ++done! +diff --git a/ext/mysqli/tests/protocol_query_row_fetch_data.phpt b/ext/mysqli/tests/protocol_query_row_fetch_data.phpt +new file mode 100644 +index 00000000000..524fe5e587c +--- /dev/null ++++ b/ext/mysqli/tests/protocol_query_row_fetch_data.phpt +@@ -0,0 +1,74 @@ ++--TEST-- ++MySQL protocol - statement row data fetch) ++--EXTENSIONS-- ++mysqli ++--FILE-- ++<?php ++require_once 'fake_server.inc'; ++ ++$port = 33305; ++$servername = "127.0.0.1"; ++$username = "root"; ++$password = ""; ++ ++$process = run_fake_server_in_background('query_response_row_read_two_fields', $port); ++$process->wait(); ++ ++$conn = new mysqli($servername, $username, $password, "", $port); ++ ++function my_query($conn, $field) ++{ ++ $sql = "SELECT strval, $field FROM data"; ++ ++ $result = $conn->query($sql); ++ ++ if ($result->num_rows > 0) { ++ while ($row = $result->fetch_assoc()) { ++ var_dump($row[$field]); ++ } ++ } ++} ++ ++foreach (my_mysqli_data_fields() as $field_name => $field) { ++ my_query($conn, $field_name); ++} ++ ++$conn->close(); ++ ++$process->terminate(true); ++ ++print "done!"; ++?> ++--EXPECT-- ++[*] Server started ++[*] Connection established ++[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 ++[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 ++[*] Sending - Server OK: 0700000200000002000000 ++[*] Received: 200000000353454c4543542073747276616c2c20696e7476616c2046524f4d2064617461 ++[*] Sending - Query execute data intval: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f746573740464617461046461746106696e7476616c06696e7476616c0c3f000b00000003011000000005000004fe0000220008000005047465737402313405000006fe00002200 ++string(2) "14" ++[*] Received: 200000000353454c4543542073747276616c2c20666c7476616c2046524f4d2064617461 ++[*] Sending - Query execute data fltval: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f746573740464617461046461746106666c7476616c06666c7476616c0c3f000c0000000401101f000005000004fe0000220009000005047465737403322e3305000006fe00002200 ++string(3) "2.3" ++[*] Received: 200000000353454c4543542073747276616c2c2064626c76616c2046524f4d2064617461 ++[*] Sending - Query execute data dblval: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610664626c76616c0664626c76616c0c3f00160000000501101f000005000004fe0000220009000005047465737403312e3205000006fe00002200 ++string(3) "1.2" ++[*] Received: 200000000353454c4543542073747276616c2c2064617476616c2046524f4d2064617461 ++[*] Sending - Query execute data datval: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610664617476616c0664617476616c0c3f000a0000000a811000000005000004fe000022001000000504746573740a323031342d31322d313505000006fe00002200 ++string(10) "2014-12-15" ++[*] Received: 200000000353454c4543542073747276616c2c2074696d76616c2046524f4d2064617461 ++[*] Sending - Query execute data timval: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610674696d76616c0674696d76616c0c3f000a0000000b811000000005000004fe000022000e00000504746573740831333a30303a303205000006fe00002200 ++string(8) "13:00:02" ++[*] Received: 200000000353454c4543542073747276616c2c2064746976616c2046524f4d2064617461 ++[*] Sending - Query execute data dtival: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610664746976616c0664746976616c0c3f00130000000c811000000005000004fe0000220019000005047465737413323031342d31322d31362031333a30303a303105000006fe00002200 ++string(19) "2014-12-16 13:00:01" ++[*] Received: 200000000353454c4543542073747276616c2c2062697476616c2046524f4d2064617461 ++[*] Sending - Query execute data bitval: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610662697476616c0662697476616c0c3f004000000010211000000005000004fe000022000e000005047465737408080808080808080805000006fe00002200 ++string(18) "578721382704613384" ++[*] Received: 200000000353454c4543542073747276616c2c2073747276616c2046524f4d2064617461 ++[*] Sending - Query execute data strval: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd011000000005000004fe000022000a0000050474657374047465737405000006fe00002200 ++string(4) "test" ++[*] Received: 0100000001 ++[*] Server finished ++done! +diff --git a/ext/mysqli/tests/protocol_stmt_row_fetch_data.phpt b/ext/mysqli/tests/protocol_stmt_row_fetch_data.phpt +new file mode 100644 +index 00000000000..d461ec24b8c +--- /dev/null ++++ b/ext/mysqli/tests/protocol_stmt_row_fetch_data.phpt +@@ -0,0 +1,91 @@ ++--TEST-- ++MySQL protocol - statement row data fetch) ++--EXTENSIONS-- ++mysqli ++--FILE-- ++<?php ++require_once 'fake_server.inc'; ++ ++$port = 33305; ++$servername = "127.0.0.1"; ++$username = "root"; ++$password = ""; ++ ++$process = run_fake_server_in_background('stmt_response_row_read_two_fields', $port); ++$process->wait(); ++ ++$conn = new mysqli($servername, $username, $password, "", $port); ++ ++function my_query($conn, $field) ++{ ++ $stmt = $conn->prepare("SELECT strval, $field FROM data"); ++ ++ $stmt->execute(); ++ $result = $stmt->get_result(); ++ ++ if ($result->num_rows > 0) { ++ while ($row = $result->fetch_assoc()) { ++ var_dump($row[$field]); ++ } ++ } ++} ++ ++foreach (my_mysqli_data_fields() as $field_name => $field) { ++ my_query($conn, $field_name); ++} ++ ++$conn->close(); ++ ++$process->terminate(true); ++ ++print "done!"; ++?> ++--EXPECT-- ++[*] Server started ++[*] Connection established ++[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 ++[*] Received: 6900000185a21a00000000c0080000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f7264002c0c5f636c69656e745f6e616d65076d7973716c6e640c5f7365727665725f686f7374093132372e302e302e31 ++[*] Sending - Server OK: 0700000200000002000000 ++[*] Received: 200000001653454c4543542073747276616c2c20696e7476616c2046524f4d2064617461 ++[*] Sending - Stmt prepare data intval: 0c0000010001000000020000000000003200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f746573740464617461046461746106696e7476616c06696e7476616c0c3f000b00000003011000000005000004fe00000200 ++[*] Received: 0a00000017010000000001000000 ++[*] Sending - Stmt execute data intval: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f746573740464617461046461746106696e7476616c06696e7476616c0c3f000b00000003011000000005000004fe000022000b000005000004746573740e00000005000006fe00002200 ++int(14) ++[*] Received: 050000001901000000200000001653454c4543542073747276616c2c20666c7476616c2046524f4d2064617461 ++[*] Sending - Stmt prepare data fltval: 0c0000010001000000020000000000003200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f746573740464617461046461746106666c7476616c06666c7476616c0c3f000c0000000401101f000005000004fe00000200 ++[*] Received: 0a00000017010000000001000000 ++[*] Sending - Stmt execute data fltval: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f746573740464617461046461746106666c7476616c06666c7476616c0c3f000c0000000401101f000005000004fe000022000b000005000004746573743333134005000006fe00002200 ++float(2.3) ++[*] Received: 050000001901000000200000001653454c4543542073747276616c2c2064626c76616c2046524f4d2064617461 ++[*] Sending - Stmt prepare data dblval: 0c0000010001000000020000000000003200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610664626c76616c0664626c76616c0c3f00160000000501101f000005000004fe00000200 ++[*] Received: 0a00000017010000000001000000 ++[*] Sending - Stmt execute data dblval: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610664626c76616c0664626c76616c0c3f00160000000501101f000005000004fe000022000f00000500000474657374333333333333f33f05000006fe00002200 ++float(1.2) ++[*] Received: 050000001901000000200000001653454c4543542073747276616c2c2064617476616c2046524f4d2064617461 ++[*] Sending - Stmt prepare data datval: 0c0000010001000000020000000000003200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610664617476616c0664617476616c0c3f000a0000000a811000000005000004fe00000200 ++[*] Received: 0a00000017010000000001000000 ++[*] Sending - Stmt execute data datval: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610664617476616c0664617476616c0c3f000a0000000a811000000005000004fe000022000c0000050000047465737404de070c0f05000006fe00002200 ++string(10) "2014-12-15" ++[*] Received: 050000001901000000200000001653454c4543542073747276616c2c2074696d76616c2046524f4d2064617461 ++[*] Sending - Stmt prepare data timval: 0c0000010001000000020000000000003200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610674696d76616c0674696d76616c0c3f000a0000000b811000000005000004fe00000200 ++[*] Received: 0a00000017010000000001000000 ++[*] Sending - Stmt execute data timval: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610674696d76616c0674696d76616c0c3f000a0000000b811000000005000004fe00002200100000050000047465737408000000000015080105000006fe00002200 ++string(8) "21:08:01" ++[*] Received: 050000001901000000200000001653454c4543542073747276616c2c2064746976616c2046524f4d2064617461 ++[*] Sending - Stmt prepare data dtival: 0c0000010001000000020000000000003200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610664746976616c0664746976616c0c3f00130000000c811000000005000004fe00000200 ++[*] Received: 0a00000017010000000001000000 ++[*] Sending - Stmt execute data dtival: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610664746976616c0664746976616c0c3f00130000000c811000000005000004fe000022000f0000050000047465737407de070c100d000105000006fe00002200 ++string(19) "2014-12-16 13:00:01" ++[*] Received: 050000001901000000200000001653454c4543542073747276616c2c2062697476616c2046524f4d2064617461 ++[*] Sending - Stmt prepare data bitval: 0c0000010001000000020000000000003200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610662697476616c0662697476616c0c3f004000000010211000000005000004fe00000200 ++[*] Received: 0a00000017010000000001000000 ++[*] Sending - Stmt execute data bitval: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610662697476616c0662697476616c0c3f004000000010211000000005000004fe00002200100000050000047465737408080808080808080805000006fe00002200 ++int(578721382704613384) ++[*] Received: 050000001901000000200000001653454c4543542073747276616c2c2073747276616c2046524f4d2064617461 ++[*] Sending - Stmt prepare data strval: 0c0000010001000000020000000000003200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd011000000005000004fe00000200 ++[*] Received: 0a00000017010000000001000000 ++[*] Sending - Stmt execute data strval: 01000001023200000203646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd01100000003200000303646566087068705f74657374046461746104646174610673747276616c0673747276616c0ce000c8000000fd011000000005000004fe000022000c00000500000474657374047465737405000006fe00002200 ++string(4) "test" ++[*] Received: 0500000019010000000100000001 ++[*] Server finished ++done! +diff --git a/ext/mysqlnd/mysqlnd_ps_codec.c b/ext/mysqlnd/mysqlnd_ps_codec.c +index 12edd9a2849..976143cb471 100644 +--- a/ext/mysqlnd/mysqlnd_ps_codec.c ++++ b/ext/mysqlnd/mysqlnd_ps_codec.c +@@ -52,6 +52,37 @@ struct st_mysqlnd_perm_bind mysqlnd_ps_fetch_functions[MYSQL_TYPE_LAST + 1]; + #define MYSQLND_PS_SKIP_RESULT_W_LEN -1 + #define MYSQLND_PS_SKIP_RESULT_STR -2 + ++static inline void ps_fetch_over_read_error(const zend_uchar ** row) ++{ ++ php_error_docref(NULL, E_WARNING, "Malformed server packet. Field length pointing after the end of packet"); ++ *row = NULL; ++} ++ ++static inline zend_bool ps_fetch_is_packet_over_read_with_variable_length(const unsigned int pack_len, ++ const zend_uchar ** row, const zend_uchar *p, unsigned int length) ++{ ++ if (pack_len == 0) { ++ return 0; ++ } ++ size_t length_len = *row - p; ++ if (length_len > pack_len || length > pack_len - length_len) { ++ ps_fetch_over_read_error(row); ++ return 1; ++ } ++ return 0; ++} ++ ++static inline zend_bool ps_fetch_is_packet_over_read_with_static_length(const unsigned int pack_len, ++ const zend_uchar ** row, unsigned int length) ++{ ++ if (pack_len > 0 && length > pack_len) { ++ ps_fetch_over_read_error(row); ++ return 1; ++ } ++ return 0; ++} ++ ++ + /* {{{ ps_fetch_from_1_to_8_bytes */ + void + ps_fetch_from_1_to_8_bytes(zval * zv, const MYSQLND_FIELD * const field, const unsigned int pack_len, +@@ -60,6 +91,11 @@ ps_fetch_from_1_to_8_bytes(zval * zv, const MYSQLND_FIELD * const field, const u + char tmp[22]; + size_t tmp_len = 0; + zend_bool is_bit = field->type == MYSQL_TYPE_BIT; ++ ++ if (UNEXPECTED(ps_fetch_is_packet_over_read_with_static_length(pack_len, row, byte_count))) { ++ return; ++ } ++ + DBG_ENTER("ps_fetch_from_1_to_8_bytes"); + DBG_INF_FMT("zv=%p byte_count=%u", zv, byte_count); + if (field->flags & UNSIGNED_FLAG) { +@@ -178,6 +214,11 @@ ps_fetch_float(zval * zv, const MYSQLND_FIELD * const field, const unsigned int + float fval; + double dval; + DBG_ENTER("ps_fetch_float"); ++ ++ if (UNEXPECTED(ps_fetch_is_packet_over_read_with_static_length(pack_len, row, 4))) { ++ return; ++ } ++ + float4get(fval, *row); + (*row)+= 4; + DBG_INF_FMT("value=%f", fval); +@@ -200,6 +241,11 @@ ps_fetch_double(zval * zv, const MYSQLND_FIELD * const field, const unsigned int + { + double value; + DBG_ENTER("ps_fetch_double"); ++ ++ if (UNEXPECTED(ps_fetch_is_packet_over_read_with_static_length(pack_len, row, 8))) { ++ return; ++ } ++ + float8get(value, *row); + ZVAL_DOUBLE(zv, value); + (*row)+= 8; +@@ -216,9 +262,14 @@ ps_fetch_time(zval * zv, const MYSQLND_FIELD * const field, const unsigned int p + struct st_mysqlnd_time t; + zend_ulong length; /* First byte encodes the length*/ + char * value; ++ const zend_uchar *p = *row; + DBG_ENTER("ps_fetch_time"); + + if ((length = php_mysqlnd_net_field_length(row))) { ++ if (UNEXPECTED(ps_fetch_is_packet_over_read_with_variable_length(pack_len, row, p, length))) { ++ return; ++ } ++ + const zend_uchar * to = *row; + + t.time_type = MYSQLND_TIMESTAMP_TIME; +@@ -273,9 +324,14 @@ ps_fetch_date(zval * zv, const MYSQLND_FIELD * const field, const unsigned int p + struct st_mysqlnd_time t = {0}; + zend_ulong length; /* First byte encodes the length*/ + char * value; ++ const zend_uchar *p = *row; + DBG_ENTER("ps_fetch_date"); + + if ((length = php_mysqlnd_net_field_length(row))) { ++ if (UNEXPECTED(ps_fetch_is_packet_over_read_with_variable_length(pack_len, row, p, length))) { ++ return; ++ } ++ + const zend_uchar * to = *row; + + t.time_type = MYSQLND_TIMESTAMP_DATE; +@@ -310,9 +366,14 @@ ps_fetch_datetime(zval * zv, const MYSQLND_FIELD * const field, const unsigned i + struct st_mysqlnd_time t; + zend_ulong length; /* First byte encodes the length*/ + char * value; ++ const zend_uchar *p = *row; + DBG_ENTER("ps_fetch_datetime"); + + if ((length = php_mysqlnd_net_field_length(row))) { ++ if (UNEXPECTED(ps_fetch_is_packet_over_read_with_variable_length(pack_len, row, p, length))) { ++ return; ++ } ++ + const zend_uchar * to = *row; + + t.time_type = MYSQLND_TIMESTAMP_DATETIME; +@@ -371,7 +432,11 @@ ps_fetch_string(zval * zv, const MYSQLND_FIELD * const field, const unsigned int + For now just copy, before we make it possible + to write \0 to the row buffer + */ ++ const zend_uchar *p = *row; + const zend_ulong length = php_mysqlnd_net_field_length(row); ++ if (UNEXPECTED(ps_fetch_is_packet_over_read_with_variable_length(pack_len, row, p, length))) { ++ return; ++ } + DBG_ENTER("ps_fetch_string"); + DBG_INF_FMT("len = %lu", length); + DBG_INF("copying from the row buffer"); +@@ -387,7 +452,11 @@ ps_fetch_string(zval * zv, const MYSQLND_FIELD * const field, const unsigned int + static void + ps_fetch_bit(zval * zv, const MYSQLND_FIELD * const field, const unsigned int pack_len, const zend_uchar ** row) + { ++ const zend_uchar *p = *row; + const zend_ulong length = php_mysqlnd_net_field_length(row); ++ if (UNEXPECTED(ps_fetch_is_packet_over_read_with_variable_length(pack_len, row, p, length))) { ++ return; ++ } + ps_fetch_from_1_to_8_bytes(zv, field, pack_len, row, length); + } + /* }}} */ +diff --git a/ext/mysqlnd/mysqlnd_result.c b/ext/mysqlnd/mysqlnd_result.c +index 4dcaf121fb7..c0f65f854ac 100644 +--- a/ext/mysqlnd/mysqlnd_result.c ++++ b/ext/mysqlnd/mysqlnd_result.c +@@ -505,7 +505,7 @@ mysqlnd_query_read_result_set_header(MYSQLND_CONN_DATA * conn, MYSQLND_STMT * s) + if (FAIL == (ret = result->m.read_result_metadata(result, conn))) { + /* For PS, we leave them in Prepared state */ + if (!stmt && conn->current_result) { +- mnd_efree(conn->current_result); ++ conn->current_result->m.free_result(conn->current_result, TRUE); + conn->current_result = NULL; + } + DBG_ERR("Error occurred while reading metadata"); +diff --git a/ext/mysqlnd/mysqlnd_wireprotocol.c b/ext/mysqlnd/mysqlnd_wireprotocol.c +index e4a298adaea..844b90c5704 100644 +--- a/ext/mysqlnd/mysqlnd_wireprotocol.c ++++ b/ext/mysqlnd/mysqlnd_wireprotocol.c +@@ -715,7 +715,14 @@ php_mysqlnd_auth_response_read(MYSQLND_CONN_DATA * conn, void * _packet) + + /* There is a message */ + if (packet->header.size > (size_t) (p - buf) && (net_len = php_mysqlnd_net_field_length(&p))) { +- packet->message_len = MIN(net_len, buf_len - (p - begin)); ++ /* p can get past packet size when getting field length so it needs to be checked first ++ * and after that it can be checked that the net_len is not greater than the packet size */ ++ if ((p - buf) > packet->header.size || packet->header.size - (p - buf) < net_len) { ++ DBG_ERR_FMT("OK packet message length is past the packet size"); ++ php_error_docref(NULL, E_WARNING, "OK packet message length is past the packet size"); ++ DBG_RETURN(FAIL); ++ } ++ packet->message_len = net_len; + packet->message = mnd_pestrndup((char *)p, packet->message_len, FALSE); + } else { + packet->message = NULL; +@@ -1113,6 +1120,17 @@ php_mysqlnd_rset_header_read(MYSQLND_CONN_DATA * conn, void * _packet) + BAIL_IF_NO_MORE_DATA; + /* Check for additional textual data */ + if (packet->header.size > (size_t) (p - buf) && (len = php_mysqlnd_net_field_length(&p))) { ++ /* p can get past packet size when getting field length so it needs to be checked first ++ * and after that it can be checked that the len is not greater than the packet size */ ++ if ((p - buf) > packet->header.size || packet->header.size - (p - buf) < len) { ++ size_t local_file_name_over_read = ((p - buf) - packet->header.size) + len; ++ DBG_ERR_FMT("RSET_HEADER packet additional data length is past %zu bytes the packet size", ++ local_file_name_over_read); ++ php_error_docref(NULL, E_WARNING, ++ "RSET_HEADER packet additional data length is past %zu bytes the packet size", ++ local_file_name_over_read); ++ DBG_RETURN(FAIL); ++ } + packet->info_or_local_file.s = mnd_emalloc(len + 1); + if (packet->info_or_local_file.s) { + memcpy(packet->info_or_local_file.s, p, len); +@@ -1271,23 +1289,16 @@ php_mysqlnd_rset_field_read(MYSQLND_CONN_DATA * conn, void * _packet) + meta->flags |= NUM_FLAG; + } + +- +- /* +- def could be empty, thus don't allocate on the root. +- NULL_LENGTH (0xFB) comes from COM_FIELD_LIST when the default value is NULL. +- Otherwise the string is length encoded. +- */ ++ /* COM_FIELD_LIST is no longer supported so def should not be present */ + if (packet->header.size > (size_t) (p - buf) && + (len = php_mysqlnd_net_field_length(&p)) && + len != MYSQLND_NULL_LENGTH) + { +- BAIL_IF_NO_MORE_DATA; +- DBG_INF_FMT("Def found, length %lu", len); +- meta->def = packet->memory_pool->get_chunk(packet->memory_pool, len + 1); +- memcpy(meta->def, p, len); +- meta->def[len] = '\0'; +- meta->def_length = len; +- p += len; ++ DBG_ERR_FMT("Protocol error. Server sent default for unsupported field list"); ++ php_error_docref(NULL, E_WARNING, ++ "Protocol error. Server sent default for unsupported field list (mysqlnd_wireprotocol.c:%u)", ++ __LINE__); ++ DBG_RETURN(FAIL); + } + + root_ptr = meta->root = packet->memory_pool->get_chunk(packet->memory_pool, total_len); +@@ -1462,8 +1473,10 @@ php_mysqlnd_rowp_read_binary_protocol(MYSQLND_ROW_BUFFER * row_buffer, zval * fi + const unsigned int field_count, const MYSQLND_FIELD * const fields_metadata, + const zend_bool as_int_or_float, MYSQLND_STATS * const stats) + { +- unsigned int i; +- const zend_uchar * p = row_buffer->ptr; ++ unsigned int i, j; ++ size_t rbs = row_buffer->size; ++ const zend_uchar * rbp = row_buffer->ptr; ++ const zend_uchar * p = rbp; + const zend_uchar * null_ptr; + zend_uchar bit; + zval *current_field, *end_field, *start_field; +@@ -1496,7 +1509,21 @@ php_mysqlnd_rowp_read_binary_protocol(MYSQLND_ROW_BUFFER * row_buffer, zval * fi + statistic = STAT_BINARY_TYPE_FETCHED_NULL; + } else { + enum_mysqlnd_field_types type = fields_metadata[i].type; +- mysqlnd_ps_fetch_functions[type].func(current_field, &fields_metadata[i], 0, &p); ++ size_t row_position = p - rbp; ++ if (rbs <= row_position) { ++ for (j = 0, current_field = start_field; j < i; current_field++, j++) { ++ zval_ptr_dtor(current_field); ++ } ++ php_error_docref(NULL, E_WARNING, "Malformed server packet. No packet space left for the field"); ++ DBG_RETURN(FAIL); ++ } ++ mysqlnd_ps_fetch_functions[type].func(current_field, &fields_metadata[i], rbs - row_position, &p); ++ if (p == NULL) { ++ for (j = 0, current_field = start_field; j < i; current_field++, j++) { ++ zval_ptr_dtor(current_field); ++ } ++ DBG_RETURN(FAIL); ++ } + + if (MYSQLND_G(collect_statistics)) { + switch (fields_metadata[i].type) { +@@ -1553,7 +1580,7 @@ php_mysqlnd_rowp_read_text_protocol_aux(MYSQLND_ROW_BUFFER * row_buffer, zval * + unsigned int field_count, const MYSQLND_FIELD * fields_metadata, + zend_bool as_int_or_float, MYSQLND_STATS * stats) + { +- unsigned int i; ++ unsigned int i, j; + zval *current_field, *end_field, *start_field; + zend_uchar * p = row_buffer->ptr; + const size_t data_size = row_buffer->size; +@@ -1574,9 +1601,11 @@ php_mysqlnd_rowp_read_text_protocol_aux(MYSQLND_ROW_BUFFER * row_buffer, zval * + /* NULL or NOT NULL, this is the question! */ + if (len == MYSQLND_NULL_LENGTH) { + ZVAL_NULL(current_field); +- } else if ((p + len) > packet_end) { +- php_error_docref(NULL, E_WARNING, "Malformed server packet. Field length pointing "MYSQLND_SZ_T_SPEC +- " bytes after end of packet", (p + len) - packet_end - 1); ++ } else if (p > packet_end || len > packet_end - p) { ++ php_error_docref(NULL, E_WARNING, "Malformed server packet. Field length pointing after end of packet"); ++ for (j = 0, current_field = start_field; j < i; current_field++, j++) { ++ zval_ptr_dtor(current_field); ++ } + DBG_RETURN(FAIL); + } else { + #if defined(MYSQLND_STRING_TO_INT_CONVERSION) +-- +2.47.0 + +From aaeb9549a1bdfa787fc3d3a2d499b418d09a5387 Mon Sep 17 00:00:00 2001 +From: Jakub Zelenka <bukka@php.net> +Date: Mon, 18 Nov 2024 15:54:30 +0100 +Subject: [PATCH 2/6] Fix MySQLnd possible buffer over read in auth_protocol + +(cherry picked from commit 32f905f1d689aaa8eacd6331a18c0dd45972c3c1) +(cherry picked from commit d5f9da0d6af72ae21b0a9f4c94c59dfdd409e3e2) +--- + ext/mysqlnd/mysqlnd_wireprotocol.c | 27 +++++++++++++++++++++++++-- + 1 file changed, 25 insertions(+), 2 deletions(-) + +diff --git a/ext/mysqlnd/mysqlnd_wireprotocol.c b/ext/mysqlnd/mysqlnd_wireprotocol.c +index 844b90c5704..e75c859bab7 100644 +--- a/ext/mysqlnd/mysqlnd_wireprotocol.c ++++ b/ext/mysqlnd/mysqlnd_wireprotocol.c +@@ -441,8 +441,31 @@ php_mysqlnd_greet_read(MYSQLND_CONN_DATA * conn, void * _packet) + if (packet->server_capabilities & CLIENT_PLUGIN_AUTH) { + BAIL_IF_NO_MORE_DATA; + /* The server is 5.5.x and supports authentication plugins */ +- packet->auth_protocol = estrdup((char *)p); +- p+= strlen(packet->auth_protocol) + 1; /* eat the '\0' */ ++ size_t remaining_size = packet->header.size - (size_t)(p - buf); ++ if (remaining_size == 0) { ++ /* Might be better to fail but this will fail anyway */ ++ packet->auth_protocol = estrdup(""); ++ } else { ++ /* Check if NUL present */ ++ char *null_terminator = memchr(p, '\0', remaining_size); ++ size_t auth_protocol_len; ++ if (null_terminator) { ++ /* If present, do basically estrdup */ ++ auth_protocol_len = null_terminator - (char *)p; ++ } else { ++ /* If not present, copy the rest of the buffer */ ++ auth_protocol_len = remaining_size; ++ } ++ char *auth_protocol = emalloc(auth_protocol_len + 1); ++ memcpy(auth_protocol, p, auth_protocol_len); ++ auth_protocol[auth_protocol_len] = '\0'; ++ packet->auth_protocol = auth_protocol; ++ ++ p += auth_protocol_len; ++ if (null_terminator) { ++ p++; ++ } ++ } + } + + DBG_INF_FMT("proto=%u server=%s thread_id=%u", +-- +2.47.0 + +From 83a0d005d51a44bbe77a178c387e2c9f042a335d Mon Sep 17 00:00:00 2001 +From: Remi Collet <remi@remirepo.net> +Date: Wed, 27 Nov 2024 10:54:10 +0100 +Subject: [PATCH 3/6] Avoid using uninitialised struct + + (cherry picked from commit 7e7817bc2f82570bbc510a2bf5e4e0ec09dbc774) + +(cherry picked from commit 69853e12b73a989e2383452356cdc07172427ae3) +--- + ext/mysqlnd/mysqlnd_result.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/ext/mysqlnd/mysqlnd_result.c b/ext/mysqlnd/mysqlnd_result.c +index c0f65f854ac..9f19adb0ce9 100644 +--- a/ext/mysqlnd/mysqlnd_result.c ++++ b/ext/mysqlnd/mysqlnd_result.c +@@ -549,8 +549,8 @@ mysqlnd_query_read_result_set_header(MYSQLND_CONN_DATA * conn, MYSQLND_STMT * s) + } + MYSQLND_INC_CONN_STATISTIC(conn->stats, statistic); + } ++ PACKET_FREE(&fields_eof); + } while (0); +- PACKET_FREE(&fields_eof); + break; /* switch break */ + } + } while (0); +-- +2.47.0 + +From 606322b7f3475fb5980f7785789adfb9c381abbc Mon Sep 17 00:00:00 2001 +From: Jakub Zelenka <bukka@php.net> +Date: Sun, 24 Nov 2024 20:13:47 +0100 +Subject: [PATCH 4/6] Change port for mysqli fake server auth message test + +(cherry picked from commit 51f5539914ae62ef8568ea1ed302dceda897c439) +(cherry picked from commit 7e6af9c78d84d15880cfbc7867501f25ab982f5f) +--- + ext/mysqli/tests/ghsa-h35g-vwh6-m678-auth-message.phpt | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/ext/mysqli/tests/ghsa-h35g-vwh6-m678-auth-message.phpt b/ext/mysqli/tests/ghsa-h35g-vwh6-m678-auth-message.phpt +index db54a6c0177..279aec6a2cb 100644 +--- a/ext/mysqli/tests/ghsa-h35g-vwh6-m678-auth-message.phpt ++++ b/ext/mysqli/tests/ghsa-h35g-vwh6-m678-auth-message.phpt +@@ -6,7 +6,7 @@ mysqli + <?php + require_once 'fake_server.inc'; + +-$port = 50001; ++$port = 33305; + $servername = "127.0.0.1"; + $username = "root"; + $password = ""; +@@ -34,5 +34,5 @@ print "done!"; + [*] Sending - Malicious OK Auth Response [Extract heap through buffer over-read]: 0900000200000002000000fcff + + Warning: mysqli::__construct(): OK packet message length is past the packet size in %s on line %d +-Unknown error while trying to connect via tcp://127.0.0.1:50001 ++Unknown error while trying to connect via tcp://127.0.0.1:33305 + done! +-- +2.47.0 + +From c308c94eefdbddb041ed3cf502ef5dd6969e14f1 Mon Sep 17 00:00:00 2001 +From: Jakub Zelenka <bukka@php.net> +Date: Sun, 24 Nov 2024 23:48:27 +0100 +Subject: [PATCH 5/6] Increase MySQLi fake server read timeout for ASAN job + +(cherry picked from commit eb951b3d11109aa16982a2132f8d1fd5129edc9e) +(cherry picked from commit cae38b1c749d27dc3a65f7d65fdf238439e2676c) +--- + ext/mysqli/tests/fake_server.inc | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/ext/mysqli/tests/fake_server.inc b/ext/mysqli/tests/fake_server.inc +index b02fabc584c..1127f6c00e3 100644 +--- a/ext/mysqli/tests/fake_server.inc ++++ b/ext/mysqli/tests/fake_server.inc +@@ -552,8 +552,8 @@ class my_mysqli_fake_server_conn + + public function read($bytes_len = 1024) + { +- // wait 10ms to fill the buffer +- usleep(10000); ++ // wait 20ms to fill the buffer ++ usleep(20000); + $data = fread($this->conn, $bytes_len); + if ($data) { + fprintf(STDERR, "[*] Received: %s\n", bin2hex($data)); +-- +2.47.0 + +From 016ffd6131a6174fe5ca5f4af3c66ad9f59ed879 Mon Sep 17 00:00:00 2001 +From: Remi Collet <remi@remirepo.net> +Date: Wed, 27 Nov 2024 11:17:48 +0100 +Subject: [PATCH 6/6] adapt test + NEWS + +--- + NEWS | 4 + + ext/mysqli/tests/fake_server.inc | 99 ++++++++++--------- + .../ghsa-h35g-vwh6-m678-auth-message.phpt | 3 +- + ext/mysqli/tests/mysqli_change_user_new.phpt | 3 + + 4 files changed, 61 insertions(+), 48 deletions(-) + +diff --git a/NEWS b/NEWS +index f600d6aea65..09cf2cfa0bb 100644 +--- a/NEWS ++++ b/NEWS +@@ -11,6 +11,10 @@ Backported from 8.1.31 + . Fixed bug GHSA-g665-fm4p-vhff (OOB access in ldap_escape). (CVE-2024-8932) + (nielsdos) + ++- MySQLnd: ++ . Fixed bug GHSA-h35g-vwh6-m678 (Leak partial content of the heap through ++ heap buffer over-read). (CVE-2024-8929) (Jakub Zelenka) ++ + - PDO DBLIB: + . Fixed bug GHSA-5hqh-c84r-qjcv (Integer overflow in the dblib quoter causing + OOB writes). (CVE-2024-11236) (nielsdos) +diff --git a/ext/mysqli/tests/fake_server.inc b/ext/mysqli/tests/fake_server.inc +index 1127f6c00e3..18b77cb1a76 100644 +--- a/ext/mysqli/tests/fake_server.inc ++++ b/ext/mysqli/tests/fake_server.inc +@@ -1,6 +1,6 @@ + <?php + +-function my_mysqli_data_fields(): array ++function my_mysqli_data_fields() + { + return [ + 'intval' => [ +@@ -107,8 +107,11 @@ function my_mysqli_data_field(string $field): array + + class my_mysqli_fake_packet_item + { +- public function __construct(public string|null $name, public string $value, public bool $is_hex = true) ++ public function __construct($name, string $value, bool $is_hex = true) + { ++ $this->name = $name; ++ $this->value = $value; ++ $this->is_hex = $is_hex; + } + } + +@@ -126,7 +129,7 @@ class my_mysqli_fake_packet + return null; + } + +- public function __set(string $name, string|my_mysqli_fake_packet_item $value) ++ public function __set(string $name, $value) + { + if ($value instanceof my_mysqli_fake_packet_item) { + if ($value->name === null) { +@@ -146,7 +149,7 @@ class my_mysqli_fake_packet + $this->data[] = $value; + } + +- public function to_bytes(): string ++ public function to_bytes() + { + $bytes = ''; + foreach ($this->data as $item) { +@@ -158,7 +161,7 @@ class my_mysqli_fake_packet + + class my_mysqli_fake_packet_generator + { +- public static function create_packet_item(int|string $value, bool $is_hex = false, string $format = 'v'): my_mysqli_fake_packet_item ++ public static function create_packet_item($value, bool $is_hex = false, string $format = 'v') + { + if (is_string($value)) { + $packed_value = $value; +@@ -168,7 +171,7 @@ class my_mysqli_fake_packet_generator + return new my_mysqli_fake_packet_item(null, $packed_value, $is_hex); + } + +- public function server_ok(): my_mysqli_fake_packet ++ public function server_ok() + { + $packet = new my_mysqli_fake_packet(); + $packet->packet_length = "070000"; +@@ -181,7 +184,7 @@ class my_mysqli_fake_packet_generator + return $packet; + } + +- public function server_greetings(): my_mysqli_fake_packet ++ public function server_greetings() + { + $packet = new my_mysqli_fake_packet(); + $packet->packet_length = "580000"; +@@ -204,7 +207,7 @@ class my_mysqli_fake_packet_generator + return $packet; + } + +- public function server_tabular_query_response(): array ++ public function server_tabular_query_response() + { + $qr1 = new my_mysqli_fake_packet(); + $qr1->packet_length = "010000"; +@@ -239,7 +242,7 @@ class my_mysqli_fake_packet_generator + return [$qr1, $qr2, $qr3, $qr4, $qr5]; + } + +- public function server_upsert_query_response(): array ++ public function server_upsert_query_response() + { + $qr1 = new my_mysqli_fake_packet(); + $qr1->packet_length = "010000"; +@@ -257,7 +260,7 @@ class my_mysqli_fake_packet_generator + return [$qr1]; + } + +- public function server_stmt_prepare_response_start($num_field): my_mysqli_fake_packet ++ public function server_stmt_prepare_response_start($num_field) + { + $pr1 = new my_mysqli_fake_packet(); + $pr1->packet_length = "0c0000"; +@@ -272,7 +275,7 @@ class my_mysqli_fake_packet_generator + return $pr1; + } + +- public function server_stmt_prepare_response_end($packer_number): my_mysqli_fake_packet ++ public function server_stmt_prepare_response_end($packer_number) + { + $pr3 = new my_mysqli_fake_packet(); + $pr3->packet_length = "050000"; +@@ -284,7 +287,7 @@ class my_mysqli_fake_packet_generator + return $pr3; + } + +- public function server_stmt_prepare_items_response(): array ++ public function server_stmt_prepare_items_response() + { + $pr1 = $this->server_stmt_prepare_response_start('0100'); + +@@ -316,7 +319,7 @@ class my_mysqli_fake_packet_generator + return [$pr1, $pr2, $pr3]; + } + +- public function server_stmt_prepare_data_response_field($packet_number, $field_name): my_mysqli_fake_packet ++ public function server_stmt_prepare_data_response_field($packet_number, $field_name) + { + if (strlen($field_name) != 6) { + throw new Exception("Invalid field length - only 6 is allowed"); +@@ -350,7 +353,7 @@ class my_mysqli_fake_packet_generator + return $pr; + } + +- public function server_stmt_prepare_data_response(string $field_name): array ++ public function server_stmt_prepare_data_response(string $field_name) + { + $pr1 = $this->server_stmt_prepare_response_start('0200'); + +@@ -362,7 +365,7 @@ class my_mysqli_fake_packet_generator + return [$pr1, $pr2, $pr3, $pr4]; + } + +- public function server_stmt_execute_items_response(): array ++ public function server_stmt_execute_items_response() + { + $pr1 = new my_mysqli_fake_packet(); + $pr1->packet_length = "010000"; +@@ -413,7 +416,7 @@ class my_mysqli_fake_packet_generator + return [$pr1, $pr2, $pr3, $pr4, $pr5]; + } + +- private function server_execute_data_response_start(string $field_name): array ++ private function server_execute_data_response_start(string $field_name) + { + $pr1 = new my_mysqli_fake_packet(); + $pr1->packet_length = "010000"; +@@ -478,7 +481,7 @@ class my_mysqli_fake_packet_generator + return [$field, $pr1, $pr2, $pr3, $pr4]; + } + +- private function server_execute_data_response_end(): my_mysqli_fake_packet ++ private function server_execute_data_response_end() + { + $pr6 = new my_mysqli_fake_packet(); + $pr6->packet_length = '050000'; +@@ -490,7 +493,7 @@ class my_mysqli_fake_packet_generator + return $pr6; + } + +- public function server_stmt_execute_data_response(string $field_name): array ++ public function server_stmt_execute_data_response(string $field_name) + { + [$field, $pr1, $pr2, $pr3, $pr4] = $this->server_execute_data_response_start($field_name); + +@@ -506,7 +509,7 @@ class my_mysqli_fake_packet_generator + return [$pr1, $pr2, $pr3, $pr4, $pr5, $this->server_execute_data_response_end()]; + } + +- public function server_query_execute_data_response(string $field_name): array ++ public function server_query_execute_data_response(string $field_name) + { + [$field, $pr1, $pr2, $pr3, $pr4] = $this->server_execute_data_response_start($field_name); + +@@ -537,12 +540,12 @@ class my_mysqli_fake_server_conn + } + } + +- public function packets_to_bytes(array $packets): string ++ public function packets_to_bytes(array $packets) + { + return implode('', array_map(fn($s) => $s->to_bytes(), $packets)); + } + +- public function send($payload, $message = null): void ++ public function send($payload, $message = null) + { + if ($message) { + fprintf(STDERR, "[*] Sending - %s: %s\n", $message, bin2hex($payload)); +@@ -575,38 +578,38 @@ class my_mysqli_fake_server_conn + $this->send($this->packet_generator->server_ok()->to_bytes(), "Server OK"); + } + +- public function send_server_tabular_query_response(): void ++ public function send_server_tabular_query_response() + { + $packets = $this->packet_generator->server_tabular_query_response(); + $this->send($this->packets_to_bytes($packets), "Tabular response"); + } + +- public function send_server_stmt_prepare_items_response(): void ++ public function send_server_stmt_prepare_items_response() + { + $packets = $this->packet_generator->server_stmt_prepare_items_response(); + $this->send($this->packets_to_bytes($packets), "Stmt prepare items"); + } + + +- public function send_server_stmt_prepare_data_response(string $field_name): void ++ public function send_server_stmt_prepare_data_response(string $field_name) + { + $packets = $this->packet_generator->server_stmt_prepare_data_response($field_name); + $this->send($this->packets_to_bytes($packets), "Stmt prepare data $field_name"); + } + +- public function send_server_stmt_execute_items_response(): void ++ public function send_server_stmt_execute_items_response() + { + $packets = $this->packet_generator->server_stmt_execute_items_response(); + $this->send($this->packets_to_bytes($packets), "Stmt execute items"); + } + +- public function send_server_stmt_execute_data_response(string $field_name): void ++ public function send_server_stmt_execute_data_response(string $field_name) + { + $packets = $this->packet_generator->server_stmt_execute_data_response($field_name); + $this->send($this->packets_to_bytes($packets), "Stmt execute data $field_name"); + } + +- public function send_server_query_execute_data_response(string $field_name): void ++ public function send_server_query_execute_data_response(string $field_name) + { + $packets = $this->packet_generator->server_query_execute_data_response($field_name); + $this->send($this->packets_to_bytes($packets), "Query execute data $field_name"); +@@ -615,7 +618,11 @@ class my_mysqli_fake_server_conn + + class my_mysqli_fake_server_process + { +- public function __construct(private $process, private array $pipes) {} ++ public function __construct($process, array $pipes) ++ { ++ $this->process = $process; ++ $this->pipes = $pipes; ++ } + + public function terminate(bool $wait = false) + { +@@ -631,7 +638,7 @@ class my_mysqli_fake_server_process + } + } + +-function my_mysqli_test_tabular_response_def_over_read(my_mysqli_fake_server_conn $conn): void ++function my_mysqli_test_tabular_response_def_over_read(my_mysqli_fake_server_conn $conn) + { + $rh = $conn->packet_generator->server_tabular_query_response(); + +@@ -655,7 +662,7 @@ function my_mysqli_test_tabular_response_def_over_read(my_mysqli_fake_server_con + $conn->read(65536); + } + +-function my_mysqli_test_upsert_response_filename_over_read(my_mysqli_fake_server_conn $conn): void ++function my_mysqli_test_upsert_response_filename_over_read(my_mysqli_fake_server_conn $conn) + { + $rh = $conn->packet_generator->server_upsert_query_response(); + +@@ -672,7 +679,7 @@ function my_mysqli_test_upsert_response_filename_over_read(my_mysqli_fake_server + $conn->read(65536); + } + +-function my_mysqli_test_auth_response_message_over_read(my_mysqli_fake_server_conn $conn): void ++function my_mysqli_test_auth_response_message_over_read(my_mysqli_fake_server_conn $conn) + { + $p = $conn->packet_generator->server_ok(); + $p->packet_length = "090000"; +@@ -684,7 +691,7 @@ function my_mysqli_test_auth_response_message_over_read(my_mysqli_fake_server_co + $conn->read(); + } + +-function my_mysqli_test_stmt_response_row_over_read_string(my_mysqli_fake_server_conn $conn): void ++function my_mysqli_test_stmt_response_row_over_read_string(my_mysqli_fake_server_conn $conn) + { + $rh = $conn->packet_generator->server_stmt_execute_items_response(); + +@@ -705,7 +712,7 @@ function my_mysqli_test_stmt_response_row_over_read_two_fields( + my_mysqli_fake_server_conn $conn, + string $field_name, + string $row_field1_len = '06' +-): void { ++) { + $rh = $conn->packet_generator->server_stmt_execute_data_response($field_name); + + // Set extra length to overread by two bytes +@@ -724,47 +731,47 @@ function my_mysqli_test_stmt_response_row_over_read_two_fields( + $conn->read(65536); + } + +-function my_mysqli_test_stmt_response_row_over_read_int(my_mysqli_fake_server_conn $conn): void ++function my_mysqli_test_stmt_response_row_over_read_int(my_mysqli_fake_server_conn $conn) + { + my_mysqli_test_stmt_response_row_over_read_two_fields($conn, 'intval'); + } + +-function my_mysqli_test_stmt_response_row_over_read_float(my_mysqli_fake_server_conn $conn): void ++function my_mysqli_test_stmt_response_row_over_read_float(my_mysqli_fake_server_conn $conn) + { + my_mysqli_test_stmt_response_row_over_read_two_fields($conn, 'fltval'); + } + +-function my_mysqli_test_stmt_response_row_over_read_double(my_mysqli_fake_server_conn $conn): void ++function my_mysqli_test_stmt_response_row_over_read_double(my_mysqli_fake_server_conn $conn) + { + my_mysqli_test_stmt_response_row_over_read_two_fields($conn, 'dblval'); + } + +-function my_mysqli_test_stmt_response_row_over_read_date(my_mysqli_fake_server_conn $conn): void ++function my_mysqli_test_stmt_response_row_over_read_date(my_mysqli_fake_server_conn $conn) + { + my_mysqli_test_stmt_response_row_over_read_two_fields($conn, 'datval'); + } + +-function my_mysqli_test_stmt_response_row_over_read_time(my_mysqli_fake_server_conn $conn): void ++function my_mysqli_test_stmt_response_row_over_read_time(my_mysqli_fake_server_conn $conn) + { + my_mysqli_test_stmt_response_row_over_read_two_fields($conn, 'timval', '0c'); + } + +-function my_mysqli_test_stmt_response_row_over_read_datetime(my_mysqli_fake_server_conn $conn): void ++function my_mysqli_test_stmt_response_row_over_read_datetime(my_mysqli_fake_server_conn $conn) + { + my_mysqli_test_stmt_response_row_over_read_two_fields($conn, 'dtival'); + } + +-function my_mysqli_test_stmt_response_row_no_space(my_mysqli_fake_server_conn $conn): void ++function my_mysqli_test_stmt_response_row_no_space(my_mysqli_fake_server_conn $conn) + { + my_mysqli_test_stmt_response_row_over_read_two_fields($conn, 'strval', '09'); + } + +-function my_mysqli_test_stmt_response_row_over_read_bit(my_mysqli_fake_server_conn $conn): void ++function my_mysqli_test_stmt_response_row_over_read_bit(my_mysqli_fake_server_conn $conn) + { + my_mysqli_test_stmt_response_row_over_read_two_fields($conn, 'bitval'); + } + +-function my_mysqli_test_stmt_response_row_read_two_fields(my_mysqli_fake_server_conn $conn): void ++function my_mysqli_test_stmt_response_row_read_two_fields(my_mysqli_fake_server_conn $conn) + { + $conn->send_server_greetings(); + $conn->read(); +@@ -779,7 +786,7 @@ function my_mysqli_test_stmt_response_row_read_two_fields(my_mysqli_fake_server_ + } + } + +-function my_mysqli_test_query_response_row_length_overflow(my_mysqli_fake_server_conn $conn): void ++function my_mysqli_test_query_response_row_length_overflow(my_mysqli_fake_server_conn $conn) + { + $rh = $conn->packet_generator->server_query_execute_data_response('strval'); + +@@ -794,7 +801,7 @@ function my_mysqli_test_query_response_row_length_overflow(my_mysqli_fake_server + $conn->read(65536); + } + +-function my_mysqli_test_query_response_row_read_two_fields(my_mysqli_fake_server_conn $conn): void ++function my_mysqli_test_query_response_row_read_two_fields(my_mysqli_fake_server_conn $conn) + { + $conn->send_server_greetings(); + $conn->read(); +@@ -807,7 +814,7 @@ function my_mysqli_test_query_response_row_read_two_fields(my_mysqli_fake_server + } + } + +-function run_fake_server(string $test_function, $port = 33305): void ++function run_fake_server(string $test_function, $port = 33305) + { + $address = '127.0.0.1'; + +@@ -832,7 +839,7 @@ function run_fake_server(string $test_function, $port = 33305): void + } + + +-function run_fake_server_in_background($test_function, $port = 33305): my_mysqli_fake_server_process ++function run_fake_server_in_background($test_function, $port = 33305) + { + $command = [PHP_BINARY, '-n', __FILE__, 'mysqli_fake_server', $test_function, $port]; + +diff --git a/ext/mysqli/tests/ghsa-h35g-vwh6-m678-auth-message.phpt b/ext/mysqli/tests/ghsa-h35g-vwh6-m678-auth-message.phpt +index 279aec6a2cb..161c9a5b8e6 100644 +--- a/ext/mysqli/tests/ghsa-h35g-vwh6-m678-auth-message.phpt ++++ b/ext/mysqli/tests/ghsa-h35g-vwh6-m678-auth-message.phpt +@@ -34,5 +34,4 @@ print "done!"; + [*] Sending - Malicious OK Auth Response [Extract heap through buffer over-read]: 0900000200000002000000fcff + + Warning: mysqli::__construct(): OK packet message length is past the packet size in %s on line %d +-Unknown error while trying to connect via tcp://127.0.0.1:33305 +-done! ++%A +diff --git a/ext/mysqli/tests/mysqli_change_user_new.phpt b/ext/mysqli/tests/mysqli_change_user_new.phpt +index 2b8993fc20d..d8fed6dfaee 100644 +--- a/ext/mysqli/tests/mysqli_change_user_new.phpt ++++ b/ext/mysqli/tests/mysqli_change_user_new.phpt +@@ -12,6 +12,9 @@ if (!$link = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket)) + + if (mysqli_get_server_version($link) < 50600) + die("SKIP For MySQL >= 5.6.0"); ++ ++if (mysqli_get_server_version($link) >= 10_00_00) ++ die("SKIP Not applicable for MariaDB"); + ?> + --FILE-- + <?php +-- +2.47.0 + diff --git a/php-cve-2024-8932.patch b/php-cve-2024-8932.patch new file mode 100644 index 0000000..1efcff9 --- /dev/null +++ b/php-cve-2024-8932.patch @@ -0,0 +1,139 @@ +From 50e9e72530a4805980384b8ea6672877af816145 Mon Sep 17 00:00:00 2001 +From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> +Date: Thu, 26 Sep 2024 22:22:27 +0200 +Subject: [PATCH 4/7] Fix GHSA-g665-fm4p-vhff: OOB access in ldap_escape + +(cherry picked from commit f9ecf90070a11dad09ca7671a712f81cc2a7d52f) +(cherry picked from commit 9f367d847989b339c33369737daf573e30bab5f1) +--- + ext/ldap/ldap.c | 21 ++++++++++++++-- + ext/ldap/tests/GHSA-g665-fm4p-vhff-1.phpt | 28 ++++++++++++++++++++++ + ext/ldap/tests/GHSA-g665-fm4p-vhff-2.phpt | 29 +++++++++++++++++++++++ + 3 files changed, 76 insertions(+), 2 deletions(-) + create mode 100644 ext/ldap/tests/GHSA-g665-fm4p-vhff-1.phpt + create mode 100644 ext/ldap/tests/GHSA-g665-fm4p-vhff-2.phpt + +diff --git a/ext/ldap/ldap.c b/ext/ldap/ldap.c +index 72a39bd93df..75adf1b5df2 100644 +--- a/ext/ldap/ldap.c ++++ b/ext/ldap/ldap.c +@@ -49,6 +49,7 @@ + + #include "ext/standard/php_string.h" + #include "ext/standard/info.h" ++#include "Zend/zend_exceptions.h" + + #ifdef HAVE_LDAP_SASL + #include <sasl/sasl.h> +@@ -3836,13 +3837,23 @@ static zend_string* php_ldap_do_escape(const zend_bool *map, const char *value, + zend_string *ret; + + for (i = 0; i < valuelen; i++) { +- len += (map[(unsigned char) value[i]]) ? 3 : 1; ++ size_t addend = (map[(unsigned char) value[i]]) ? 3 : 1; ++ if (len > ZSTR_MAX_LEN - addend) { ++ return NULL; ++ } ++ len += addend; + } + /* Per RFC 4514, a leading and trailing space must be escaped */ + if ((flags & PHP_LDAP_ESCAPE_DN) && (value[0] == ' ')) { ++ if (len > ZSTR_MAX_LEN - 2) { ++ return NULL; ++ } + len += 2; + } + if ((flags & PHP_LDAP_ESCAPE_DN) && ((valuelen > 1) && (value[valuelen - 1] == ' '))) { ++ if (len > ZSTR_MAX_LEN - 2) { ++ return NULL; ++ } + len += 2; + } + +@@ -3909,7 +3920,13 @@ PHP_FUNCTION(ldap_escape) + php_ldap_escape_map_set_chars(map, ignores, ignoreslen, 0); + } + +- RETURN_NEW_STR(php_ldap_do_escape(map, value, valuelen, flags)); ++ zend_string *result = php_ldap_do_escape(map, value, valuelen, flags); ++ if (UNEXPECTED(!result)) { ++ zend_throw_exception(NULL, "Argument #1 ($value) is too long", 0); ++ return; ++ } ++ ++ RETURN_NEW_STR(result); + } + + #ifdef STR_TRANSLATION +diff --git a/ext/ldap/tests/GHSA-g665-fm4p-vhff-1.phpt b/ext/ldap/tests/GHSA-g665-fm4p-vhff-1.phpt +new file mode 100644 +index 00000000000..734bbe91d42 +--- /dev/null ++++ b/ext/ldap/tests/GHSA-g665-fm4p-vhff-1.phpt +@@ -0,0 +1,28 @@ ++--TEST-- ++GHSA-g665-fm4p-vhff (OOB access in ldap_escape) ++--EXTENSIONS-- ++ldap ++--INI-- ++memory_limit=-1 ++--SKIPIF-- ++<?php ++if (PHP_INT_SIZE !== 4) die("skip only for 32-bit"); ++if (getenv("SKIP_SLOW_TESTS")) die("skip slow test"); ++?> ++--FILE-- ++<?php ++try { ++ ldap_escape(' '.str_repeat("#", 1431655758), "", LDAP_ESCAPE_DN); ++} catch (Exception $e) { ++ echo $e->getMessage(), "\n"; ++} ++ ++try { ++ ldap_escape(str_repeat("#", 1431655758).' ', "", LDAP_ESCAPE_DN); ++} catch (Exception $e) { ++ echo $e->getMessage(), "\n"; ++} ++?> ++--EXPECT-- ++ldap_escape(): Argument #1 ($value) is too long ++ldap_escape(): Argument #1 ($value) is too long +diff --git a/ext/ldap/tests/GHSA-g665-fm4p-vhff-2.phpt b/ext/ldap/tests/GHSA-g665-fm4p-vhff-2.phpt +new file mode 100644 +index 00000000000..5c1b0fb6611 +--- /dev/null ++++ b/ext/ldap/tests/GHSA-g665-fm4p-vhff-2.phpt +@@ -0,0 +1,29 @@ ++--TEST-- ++GHSA-g665-fm4p-vhff (OOB access in ldap_escape) ++--EXTENSIONS-- ++ldap ++--INI-- ++memory_limit=-1 ++--SKIPIF-- ++<?php ++if (PHP_INT_SIZE !== 4) die("skip only for 32-bit"); ++if (getenv("SKIP_SLOW_TESTS")) die("skip slow test"); ++?> ++--FILE-- ++<?php ++try { ++ ldap_escape(str_repeat("*", 1431655759), "", LDAP_ESCAPE_FILTER); ++} catch (Exception $e) { ++ echo $e->getMessage(), "\n"; ++} ++ ++// would allocate a string of length 2 ++try { ++ ldap_escape(str_repeat("*", 1431655766), "", LDAP_ESCAPE_FILTER); ++} catch (Exception $e) { ++ echo $e->getMessage(), "\n"; ++} ++?> ++--EXPECT-- ++ldap_escape(): Argument #1 ($value) is too long ++ldap_escape(): Argument #1 ($value) is too long +-- +2.47.0 + diff --git a/php-cve-2025-1217.patch b/php-cve-2025-1217.patch new file mode 100644 index 0000000..23d8b04 --- /dev/null +++ b/php-cve-2025-1217.patch @@ -0,0 +1,917 @@ +From bf4a8df2b3972118c87b05450e9062d3926f6be8 Mon Sep 17 00:00:00 2001 +From: Jakub Zelenka <bukka@php.net> +Date: Tue, 31 Dec 2024 18:57:02 +0100 +Subject: [PATCH 01/11] Fix GHSA-ghsa-v8xr-gpvj-cx9g: http header folding +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This adds HTTP header folding support for HTTP wrapper response +headers. + +Reviewed-by: Tim Düsterhus <tim@tideways-gmbh.com> +(cherry picked from commit d20b4c97a9f883b62b65b82d939c5af9a2028ef1) +(cherry picked from commit 4fec08542748c25573063ffc53ea89cd5de1edf0) +--- + ext/openssl/tests/ServerClientTestCase.inc | 65 +++- + ext/standard/http_fopen_wrapper.c | 347 ++++++++++++------ + .../tests/http/ghsa-v8xr-gpvj-cx9g-001.phpt | 49 +++ + .../tests/http/ghsa-v8xr-gpvj-cx9g-002.phpt | 51 +++ + .../tests/http/ghsa-v8xr-gpvj-cx9g-003.phpt | 49 +++ + .../tests/http/ghsa-v8xr-gpvj-cx9g-004.phpt | 48 +++ + .../tests/http/ghsa-v8xr-gpvj-cx9g-005.phpt | 48 +++ + .../tests/http/http_response_header_05.phpt | 35 -- + 8 files changed, 537 insertions(+), 155 deletions(-) + create mode 100644 ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-001.phpt + create mode 100644 ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-002.phpt + create mode 100644 ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-003.phpt + create mode 100644 ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-004.phpt + create mode 100644 ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-005.phpt + delete mode 100644 ext/standard/tests/http/http_response_header_05.phpt + +diff --git a/ext/openssl/tests/ServerClientTestCase.inc b/ext/openssl/tests/ServerClientTestCase.inc +index 753366df6f4..c74da444102 100644 +--- a/ext/openssl/tests/ServerClientTestCase.inc ++++ b/ext/openssl/tests/ServerClientTestCase.inc +@@ -4,14 +4,19 @@ const WORKER_ARGV_VALUE = 'RUN_WORKER'; + + const WORKER_DEFAULT_NAME = 'server'; + +-function phpt_notify($worker = WORKER_DEFAULT_NAME) ++function phpt_notify(string $worker = WORKER_DEFAULT_NAME, string $message = ""): void + { +- ServerClientTestCase::getInstance()->notify($worker); ++ ServerClientTestCase::getInstance()->notify($worker, $message); + } + +-function phpt_wait($worker = WORKER_DEFAULT_NAME, $timeout = null) ++function phpt_wait($worker = WORKER_DEFAULT_NAME, $timeout = null): ?string + { +- ServerClientTestCase::getInstance()->wait($worker, $timeout); ++ return ServerClientTestCase::getInstance()->wait($worker, $timeout); ++} ++ ++function phpt_notify_server_start($server): void ++{ ++ ServerClientTestCase::getInstance()->notify_server_start($server); + } + + function phpt_has_sslv3() { +@@ -119,43 +124,73 @@ class ServerClientTestCase + eval($code); + } + +- public function run($masterCode, $workerCode) ++ /** ++ * Run client and all workers ++ * ++ * @param string $clientCode The client PHP code ++ * @param string|array $workerCode ++ * @param bool $ephemeral Select whether automatic port selection and automatic awaiting is used ++ * @return void ++ * @throws Exception ++ */ ++ public function run(string $clientCode, $workerCode, bool $ephemeral = true): void + { + if (!is_array($workerCode)) { + $workerCode = [WORKER_DEFAULT_NAME => $workerCode]; + } +- foreach ($workerCode as $worker => $code) { ++ reset($workerCode); ++ $code = current($workerCode); ++ $worker = key($workerCode); ++ while ($worker != null) { + $this->spawnWorkerProcess($worker, $this->stripPhpTagsFromCode($code)); ++ $code = next($workerCode); ++ if ($ephemeral) { ++ $addr = trim($this->wait($worker)); ++ if (empty($addr)) { ++ throw new \Exception("Failed server start"); ++ } ++ if ($code === false) { ++ $clientCode = preg_replace('/{{\s*ADDR\s*}}/', $addr, $clientCode); ++ } else { ++ $code = preg_replace('/{{\s*ADDR\s*}}/', $addr, $code); ++ } ++ } ++ $worker = key($workerCode); + } +- eval($this->stripPhpTagsFromCode($masterCode)); ++ ++ eval($this->stripPhpTagsFromCode($clientCode)); + foreach ($workerCode as $worker => $code) { + $this->cleanupWorkerProcess($worker); + } + } + +- public function wait($worker, $timeout = null) ++ public function wait($worker, $timeout = null): ?string + { + $handle = $this->isWorker ? STDIN : $this->workerStdOut[$worker]; + if ($timeout === null) { +- fgets($handle); +- return true; ++ return fgets($handle); + } + + stream_set_blocking($handle, false); + $read = [$handle]; + $result = stream_select($read, $write, $except, $timeout); + if (!$result) { +- return false; ++ return null; + } + +- fgets($handle); ++ $result = fgets($handle); + stream_set_blocking($handle, true); +- return true; ++ return $result; ++ } ++ ++ public function notify(string $worker, string $message = ""): void ++ { ++ fwrite($this->isWorker ? STDOUT : $this->workerStdIn[$worker], "$message\n"); + } + +- public function notify($worker) ++ public function notify_server_start($server): void + { +- fwrite($this->isWorker ? STDOUT : $this->workerStdIn[$worker], "\n"); ++ echo stream_socket_get_name($server, false) . "\n"; + } + } + +diff --git a/ext/standard/http_fopen_wrapper.c b/ext/standard/http_fopen_wrapper.c +index aeeb438f0f9..08386cfafcd 100644 +--- a/ext/standard/http_fopen_wrapper.c ++++ b/ext/standard/http_fopen_wrapper.c +@@ -116,6 +116,172 @@ static zend_bool check_has_header(const char *headers, const char *header) { + return 0; + } + ++typedef struct _php_stream_http_response_header_info { ++ php_stream_filter *transfer_encoding; ++ size_t file_size; ++ zend_bool follow_location; ++ char location[HTTP_HEADER_BLOCK_SIZE]; ++} php_stream_http_response_header_info; ++ ++static void php_stream_http_response_header_info_init( ++ php_stream_http_response_header_info *header_info) ++{ ++ header_info->transfer_encoding = NULL; ++ header_info->file_size = 0; ++ header_info->follow_location = 1; ++ header_info->location[0] = '\0'; ++} ++ ++/* Trim white spaces from response header line and update its length */ ++static zend_bool php_stream_http_response_header_trim(char *http_header_line, ++ size_t *http_header_line_length) ++{ ++ char *http_header_line_end = http_header_line + *http_header_line_length - 1; ++ while (http_header_line_end >= http_header_line && ++ (*http_header_line_end == '\n' || *http_header_line_end == '\r')) { ++ http_header_line_end--; ++ } ++ ++ /* The primary definition of an HTTP header in RFC 7230 states: ++ * > Each header field consists of a case-insensitive field name followed ++ * > by a colon (":"), optional leading whitespace, the field value, and ++ * > optional trailing whitespace. */ ++ ++ /* Strip trailing whitespace */ ++ zend_bool space_trim = (*http_header_line_end == ' ' || *http_header_line_end == '\t'); ++ if (space_trim) { ++ do { ++ http_header_line_end--; ++ } while (http_header_line_end >= http_header_line && ++ (*http_header_line_end == ' ' || *http_header_line_end == '\t')); ++ } ++ http_header_line_end++; ++ *http_header_line_end = '\0'; ++ *http_header_line_length = http_header_line_end - http_header_line; ++ ++ return space_trim; ++} ++ ++/* Process folding headers of the current line and if there are none, parse last full response ++ * header line. It returns NULL if the last header is finished, otherwise it returns updated ++ * last header line. */ ++static zend_string *php_stream_http_response_headers_parse(php_stream *stream, ++ php_stream_context *context, int options, zend_string *last_header_line_str, ++ char *header_line, size_t *header_line_length, int response_code, ++ zval *response_header, php_stream_http_response_header_info *header_info) ++{ ++ char *last_header_line = ZSTR_VAL(last_header_line_str); ++ size_t last_header_line_length = ZSTR_LEN(last_header_line_str); ++ char *last_header_line_end = ZSTR_VAL(last_header_line_str) + ZSTR_LEN(last_header_line_str) - 1; ++ ++ /* Process non empty header line. */ ++ if (header_line && (*header_line != '\n' && *header_line != '\r')) { ++ /* Removing trailing white spaces. */ ++ if (php_stream_http_response_header_trim(header_line, header_line_length) && ++ *header_line_length == 0) { ++ /* Only spaces so treat as an empty folding header. */ ++ return last_header_line_str; ++ } ++ ++ /* Process folding headers if starting with a space or a tab. */ ++ if (header_line && (*header_line == ' ' || *header_line == '\t')) { ++ char *http_folded_header_line = header_line; ++ size_t http_folded_header_line_length = *header_line_length; ++ /* Remove the leading white spaces. */ ++ while (*http_folded_header_line == ' ' || *http_folded_header_line == '\t') { ++ http_folded_header_line++; ++ http_folded_header_line_length--; ++ } ++ /* It has to have some characters because it would get returned after the call ++ * php_stream_http_response_header_trim above. */ ++ ZEND_ASSERT(http_folded_header_line_length > 0); ++ /* Concatenate last header line, space and current header line. */ ++ zend_string *extended_header_str = zend_string_alloc(last_header_line_length + 1 + http_folded_header_line_length, 0); ++ memcpy(ZSTR_VAL(extended_header_str), last_header_line, last_header_line_length); ++ ZSTR_VAL(extended_header_str)[last_header_line_length] = ' '; ++ memcpy(ZSTR_VAL(extended_header_str) + last_header_line_length + 1, http_folded_header_line, http_folded_header_line_length); ++ ZSTR_VAL(extended_header_str)[ZSTR_LEN(extended_header_str)] = 0; ++ zend_string_efree(last_header_line_str); ++ last_header_line_str = extended_header_str; ++ /* Return new header line. */ ++ return last_header_line_str; ++ } ++ } ++ ++ /* Find header separator position. */ ++ char *last_header_value = memchr(last_header_line, ':', last_header_line_length); ++ if (last_header_value) { ++ last_header_value++; /* Skip ':'. */ ++ ++ /* Strip leading whitespace. */ ++ while (last_header_value < last_header_line_end ++ && (*last_header_value == ' ' || *last_header_value == '\t')) { ++ last_header_value++; ++ } ++ } else { ++ /* There is no colon. Set the value to the end of the header line, which is effectively ++ * an empty string. */ ++ last_header_value = last_header_line_end; ++ } ++ ++ zend_bool store_header = 1; ++ zval *tmpzval = NULL; ++ ++ if (!strncasecmp(last_header_line, "Location:", sizeof("Location:")-1)) { ++ /* Check if the location should be followed. */ ++ if (context && (tmpzval = php_stream_context_get_option(context, "http", "follow_location")) != NULL) { ++ header_info->follow_location = zval_is_true(tmpzval); ++ } else if (!((response_code >= 300 && response_code < 304) ++ || 307 == response_code || 308 == response_code)) { ++ /* The redirection should not be automatic if follow_location is not set and ++ * response_code not in (300, 301, 302, 303 and 307) ++ * see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.1 ++ * RFC 7238 defines 308: http://tools.ietf.org/html/rfc7238 */ ++ header_info->follow_location = 0; ++ } ++ strlcpy(header_info->location, last_header_value, sizeof(header_info->location)); ++ } else if (!strncasecmp(last_header_line, "Content-Type:", sizeof("Content-Type:")-1)) { ++ php_stream_notify_info(context, PHP_STREAM_NOTIFY_MIME_TYPE_IS, last_header_value, 0); ++ } else if (!strncasecmp(last_header_line, "Content-Length:", sizeof("Content-Length:")-1)) { ++ header_info->file_size = atoi(last_header_value); ++ php_stream_notify_file_size(context, header_info->file_size, last_header_line, 0); ++ } else if ( ++ !strncasecmp(last_header_line, "Transfer-Encoding:", sizeof("Transfer-Encoding:")-1) ++ && !strncasecmp(last_header_value, "Chunked", sizeof("Chunked")-1) ++ ) { ++ /* Create filter to decode response body. */ ++ if (!(options & STREAM_ONLY_GET_HEADERS)) { ++ zend_long decode = 1; ++ ++ if (context && (tmpzval = php_stream_context_get_option(context, "http", "auto_decode")) != NULL) { ++ decode = zend_is_true(tmpzval); ++ } ++ if (decode) { ++ if (header_info->transfer_encoding != NULL) { ++ /* Prevent a memory leak in case there are more transfer-encoding headers. */ ++ php_stream_filter_free(header_info->transfer_encoding); ++ } ++ header_info->transfer_encoding = php_stream_filter_create( ++ "dechunk", NULL, php_stream_is_persistent(stream)); ++ if (header_info->transfer_encoding != NULL) { ++ /* Do not store transfer-encoding header. */ ++ store_header = 0; ++ } ++ } ++ } ++ } ++ ++ if (store_header) { ++ zval http_header; ++ ZVAL_NEW_STR(&http_header, last_header_line_str); ++ zend_hash_next_index_insert(Z_ARRVAL_P(response_header), &http_header); ++ } else { ++ zend_string_efree(last_header_line_str); ++ } ++ ++ return NULL; ++} ++ + static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper, + const char *path, const char *mode, int options, zend_string **opened_path, + php_stream_context *context, int redirect_max, int flags, +@@ -128,11 +294,12 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper, + zend_string *tmp = NULL; + char *ua_str = NULL; + zval *ua_zval = NULL, *tmpzval = NULL, ssl_proxy_peer_name; +- char location[HTTP_HEADER_BLOCK_SIZE]; + int reqok = 0; + char *http_header_line = NULL; ++ zend_string *last_header_line_str = NULL; ++ php_stream_http_response_header_info header_info; + char tmp_line[128]; +- size_t chunk_size = 0, file_size = 0; ++ size_t chunk_size = 0; + int eol_detect = 0; + char *transport_string; + zend_string *errstr = NULL; +@@ -143,8 +310,6 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper, + char *user_headers = NULL; + int header_init = ((flags & HTTP_WRAPPER_HEADER_INIT) != 0); + int redirected = ((flags & HTTP_WRAPPER_REDIRECTED) != 0); +- zend_bool follow_location = 1; +- php_stream_filter *transfer_encoding = NULL; + int response_code; + smart_str req_buf = {0}; + zend_bool custom_request_method; +@@ -657,8 +822,6 @@ finish: + /* send it */ + php_stream_write(stream, ZSTR_VAL(req_buf.s), ZSTR_LEN(req_buf.s)); + +- location[0] = '\0'; +- + if (Z_ISUNDEF_P(response_header)) { + array_init(response_header); + } +@@ -736,125 +899,101 @@ finish: + } + } + +- /* read past HTTP headers */ ++ php_stream_http_response_header_info_init(&header_info); + ++ /* read past HTTP headers */ + while (!php_stream_eof(stream)) { + size_t http_header_line_length; + + if (http_header_line != NULL) { + efree(http_header_line); + } +- if ((http_header_line = php_stream_get_line(stream, NULL, 0, &http_header_line_length)) && *http_header_line != '\n' && *http_header_line != '\r') { +- char *e = http_header_line + http_header_line_length - 1; +- char *http_header_value; +- +- while (e >= http_header_line && (*e == '\n' || *e == '\r')) { +- e--; +- } +- +- /* The primary definition of an HTTP header in RFC 7230 states: +- * > Each header field consists of a case-insensitive field name followed +- * > by a colon (":"), optional leading whitespace, the field value, and +- * > optional trailing whitespace. */ +- +- /* Strip trailing whitespace */ +- while (e >= http_header_line && (*e == ' ' || *e == '\t')) { +- e--; +- } +- +- /* Terminate header line */ +- e++; +- *e = '\0'; +- http_header_line_length = e - http_header_line; +- +- http_header_value = memchr(http_header_line, ':', http_header_line_length); +- if (http_header_value) { +- http_header_value++; /* Skip ':' */ +- +- /* Strip leading whitespace */ +- while (http_header_value < e +- && (*http_header_value == ' ' || *http_header_value == '\t')) { +- http_header_value++; ++ if ((http_header_line = php_stream_get_line(stream, NULL, 0, &http_header_line_length))) { ++ zend_bool last_line; ++ if (*http_header_line == '\r') { ++ if (http_header_line[1] != '\n') { ++ php_stream_close(stream); ++ stream = NULL; ++ php_stream_wrapper_log_error(wrapper, options, ++ "HTTP invalid header name (cannot start with CR character)!"); ++ goto out; + } ++ last_line = 1; ++ } else if (*http_header_line == '\n') { ++ last_line = 1; + } else { +- /* There is no colon. Set the value to the end of the header line, which is +- * effectively an empty string. */ +- http_header_value = e; ++ last_line = 0; + } +- +- if (!strncasecmp(http_header_line, "Location:", sizeof("Location:")-1)) { +- if (context && (tmpzval = php_stream_context_get_option(context, "http", "follow_location")) != NULL) { +- follow_location = zval_is_true(tmpzval); +- } else if (!((response_code >= 300 && response_code < 304) +- || 307 == response_code || 308 == response_code)) { +- /* we shouldn't redirect automatically +- if follow_location isn't set and response_code not in (300, 301, 302, 303 and 307) +- see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.1 +- RFC 7238 defines 308: http://tools.ietf.org/html/rfc7238 */ +- follow_location = 0; ++ ++ if (last_header_line_str != NULL) { ++ /* Parse last header line. */ ++ last_header_line_str = php_stream_http_response_headers_parse(stream, context, ++ options, last_header_line_str, http_header_line, &http_header_line_length, ++ response_code, response_header, &header_info); ++ if (last_header_line_str != NULL) { ++ /* Folding header present so continue. */ ++ continue; + } +- strlcpy(location, http_header_value, sizeof(location)); +- } else if (!strncasecmp(http_header_line, "Content-Type:", sizeof("Content-Type:")-1)) { +- php_stream_notify_info(context, PHP_STREAM_NOTIFY_MIME_TYPE_IS, http_header_value, 0); +- } else if (!strncasecmp(http_header_line, "Content-Length:", sizeof("Content-Length:")-1)) { +- file_size = atoi(http_header_value); +- php_stream_notify_file_size(context, file_size, http_header_line, 0); +- } else if ( +- !strncasecmp(http_header_line, "Transfer-Encoding:", sizeof("Transfer-Encoding:")-1) +- && !strncasecmp(http_header_value, "Chunked", sizeof("Chunked")-1) +- ) { +- +- /* create filter to decode response body */ +- if (!(options & STREAM_ONLY_GET_HEADERS)) { +- zend_long decode = 1; +- +- if (context && (tmpzval = php_stream_context_get_option(context, "http", "auto_decode")) != NULL) { +- decode = zend_is_true(tmpzval); +- } +- if (decode) { +- transfer_encoding = php_stream_filter_create("dechunk", NULL, php_stream_is_persistent(stream)); +- if (transfer_encoding) { +- /* don't store transfer-encodeing header */ +- continue; +- } +- } ++ } else if (!last_line) { ++ /* The first line cannot start with spaces. */ ++ if (*http_header_line == ' ' || *http_header_line == '\t') { ++ php_stream_close(stream); ++ stream = NULL; ++ php_stream_wrapper_log_error(wrapper, options, ++ "HTTP invalid response format (folding header at the start)!"); ++ goto out; + } ++ /* Trim the first line if it is not the last line. */ ++ php_stream_http_response_header_trim(http_header_line, &http_header_line_length); + } +- +- { +- zval http_header; +- ZVAL_STRINGL(&http_header, http_header_line, http_header_line_length); +- zend_hash_next_index_insert(Z_ARRVAL_P(response_header), &http_header); ++ if (last_line) { ++ /* For the last line the last header line must be NULL. */ ++ ZEND_ASSERT(last_header_line_str == NULL); ++ break; + } ++ /* Save current line as the last line so it gets parsed in the next round. */ ++ last_header_line_str = zend_string_init(http_header_line, http_header_line_length, 0); + } else { + break; + } + } + +- if (!reqok || (location[0] != '\0' && follow_location)) { +- if (!follow_location || (((options & STREAM_ONLY_GET_HEADERS) || ignore_errors) && redirect_max <= 1)) { ++ /* If the stream was closed early, we still want to process the last line to keep BC. */ ++ if (last_header_line_str != NULL) { ++ php_stream_http_response_headers_parse(stream, context, options, last_header_line_str, ++ NULL, NULL, response_code, response_header, &header_info); ++ } ++ ++ if (!reqok || (header_info.location[0] != '\0' && header_info.follow_location)) { ++ if (!header_info.follow_location || (((options & STREAM_ONLY_GET_HEADERS) || ignore_errors) && redirect_max <= 1)) { + goto out; + } + +- if (location[0] != '\0') +- php_stream_notify_info(context, PHP_STREAM_NOTIFY_REDIRECTED, location, 0); ++ if (header_info.location[0] != '\0') ++ php_stream_notify_info(context, PHP_STREAM_NOTIFY_REDIRECTED, header_info.location, 0); + + php_stream_close(stream); + stream = NULL; + +- if (location[0] != '\0') { ++ if (header_info.transfer_encoding) { ++ php_stream_filter_free(header_info.transfer_encoding); ++ header_info.transfer_encoding = NULL; ++ } ++ ++ if (header_info.location[0] != '\0') { + + char new_path[HTTP_HEADER_BLOCK_SIZE]; + char loc_path[HTTP_HEADER_BLOCK_SIZE]; + + *new_path='\0'; +- if (strlen(location)<8 || (strncasecmp(location, "http://", sizeof("http://")-1) && +- strncasecmp(location, "https://", sizeof("https://")-1) && +- strncasecmp(location, "ftp://", sizeof("ftp://")-1) && +- strncasecmp(location, "ftps://", sizeof("ftps://")-1))) ++ if (strlen(header_info.location) < 8 || ++ (strncasecmp(header_info.location, "http://", sizeof("http://")-1) && ++ strncasecmp(header_info.location, "https://", sizeof("https://")-1) && ++ strncasecmp(header_info.location, "ftp://", sizeof("ftp://")-1) && ++ strncasecmp(header_info.location, "ftps://", sizeof("ftps://")-1))) + { +- if (*location != '/') { +- if (*(location+1) != '\0' && resource->path) { ++ if (*header_info.location != '/') { ++ if (*(header_info.location+1) != '\0' && resource->path) { + char *s = strrchr(ZSTR_VAL(resource->path), '/'); + if (!s) { + s = ZSTR_VAL(resource->path); +@@ -870,15 +1009,17 @@ finish: + if (resource->path && + ZSTR_VAL(resource->path)[0] == '/' && + ZSTR_VAL(resource->path)[1] == '\0') { +- snprintf(loc_path, sizeof(loc_path) - 1, "%s%s", ZSTR_VAL(resource->path), location); ++ snprintf(loc_path, sizeof(loc_path) - 1, "%s%s", ++ ZSTR_VAL(resource->path), header_info.location); + } else { +- snprintf(loc_path, sizeof(loc_path) - 1, "%s/%s", ZSTR_VAL(resource->path), location); ++ snprintf(loc_path, sizeof(loc_path) - 1, "%s/%s", ++ ZSTR_VAL(resource->path), header_info.location); + } + } else { +- snprintf(loc_path, sizeof(loc_path) - 1, "/%s", location); ++ snprintf(loc_path, sizeof(loc_path) - 1, "/%s", header_info.location); + } + } else { +- strlcpy(loc_path, location, sizeof(loc_path)); ++ strlcpy(loc_path, header_info.location, sizeof(loc_path)); + } + if ((use_ssl && resource->port != 443) || (!use_ssl && resource->port != 80)) { + snprintf(new_path, sizeof(new_path) - 1, "%s://%s:%d%s", ZSTR_VAL(resource->scheme), ZSTR_VAL(resource->host), resource->port, loc_path); +@@ -886,7 +1027,7 @@ finish: + snprintf(new_path, sizeof(new_path) - 1, "%s://%s%s", ZSTR_VAL(resource->scheme), ZSTR_VAL(resource->host), loc_path); + } + } else { +- strlcpy(new_path, location, sizeof(new_path)); ++ strlcpy(new_path, header_info.location, sizeof(new_path)); + } + + php_url_free(resource); +@@ -939,7 +1080,7 @@ out: + if (header_init) { + ZVAL_COPY(&stream->wrapperdata, response_header); + } +- php_stream_notify_progress_init(context, 0, file_size); ++ php_stream_notify_progress_init(context, 0, header_info.file_size); + + /* Restore original chunk size now that we're done with headers */ + if (options & STREAM_WILL_CAST) +@@ -955,12 +1096,8 @@ out: + /* restore mode */ + strlcpy(stream->mode, mode, sizeof(stream->mode)); + +- if (transfer_encoding) { +- php_stream_filter_append(&stream->readfilters, transfer_encoding); +- } +- } else { +- if (transfer_encoding) { +- php_stream_filter_free(transfer_encoding); ++ if (header_info.transfer_encoding) { ++ php_stream_filter_append(&stream->readfilters, header_info.transfer_encoding); + } + } + +diff --git a/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-001.phpt b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-001.phpt +new file mode 100644 +index 00000000000..64904bfcd1d +--- /dev/null ++++ b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-001.phpt +@@ -0,0 +1,49 @@ ++--TEST-- ++GHSA-v8xr-gpvj-cx9g: Header parser of http stream wrapper does not handle folded headers (single) ++--FILE-- ++<?php ++$serverCode = <<<'CODE' ++ $ctxt = stream_context_create([ ++ "socket" => [ ++ "tcp_nodelay" => true ++ ] ++ ]); ++ ++ $server = stream_socket_server( ++ "tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt); ++ phpt_notify_server_start($server); ++ ++ $conn = stream_socket_accept($server); ++ ++ phpt_notify(WORKER_DEFAULT_NAME, "server-accepted"); ++ ++ fwrite($conn, "HTTP/1.0 200 Ok\r\nContent-Type: text/html;\r\n charset=utf-8\r\n\r\nbody\r\n"); ++CODE; ++ ++$clientCode = <<<'CODE' ++ function stream_notification_callback($notification_code, $severity, $message, $message_code, $bytes_transferred, $bytes_max) { ++ switch($notification_code) { ++ case STREAM_NOTIFY_MIME_TYPE_IS: ++ echo "Found the mime-type: ", $message, PHP_EOL; ++ break; ++ } ++ } ++ ++ $ctx = stream_context_create(); ++ stream_context_set_params($ctx, array("notification" => "stream_notification_callback")); ++ var_dump(trim(file_get_contents("http://{{ ADDR }}", false, $ctx))); ++ var_dump($http_response_header); ++CODE; ++ ++include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++ServerClientTestCase::getInstance()->run($clientCode, $serverCode); ++?> ++--EXPECTF-- ++Found the mime-type: text/html; charset=utf-8 ++string(4) "body" ++array(2) { ++ [0]=> ++ string(15) "HTTP/1.0 200 Ok" ++ [1]=> ++ string(38) "Content-Type: text/html; charset=utf-8" ++} +diff --git a/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-002.phpt b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-002.phpt +new file mode 100644 +index 00000000000..a6d9d00fd58 +--- /dev/null ++++ b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-002.phpt +@@ -0,0 +1,51 @@ ++--TEST-- ++GHSA-v8xr-gpvj-cx9g: Header parser of http stream wrapper does not handle folded headers (multiple) ++--FILE-- ++<?php ++$serverCode = <<<'CODE' ++ $ctxt = stream_context_create([ ++ "socket" => [ ++ "tcp_nodelay" => true ++ ] ++ ]); ++ ++ $server = stream_socket_server( ++ "tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt); ++ phpt_notify_server_start($server); ++ ++ $conn = stream_socket_accept($server); ++ ++ phpt_notify(WORKER_DEFAULT_NAME, "server-accepted"); ++ ++ fwrite($conn, "HTTP/1.0 200 Ok\r\nContent-Type: text/html;\r\nCustom-Header: somevalue;\r\n param1=value1; \r\n param2=value2\r\n\r\nbody\r\n"); ++CODE; ++ ++$clientCode = <<<'CODE' ++ function stream_notification_callback($notification_code, $severity, $message, $message_code, $bytes_transferred, $bytes_max) { ++ switch($notification_code) { ++ case STREAM_NOTIFY_MIME_TYPE_IS: ++ echo "Found the mime-type: ", $message, PHP_EOL; ++ break; ++ } ++ } ++ ++ $ctx = stream_context_create(); ++ stream_context_set_params($ctx, array("notification" => "stream_notification_callback")); ++ var_dump(trim(file_get_contents("http://{{ ADDR }}", false, $ctx))); ++ var_dump($http_response_header); ++CODE; ++ ++include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++ServerClientTestCase::getInstance()->run($clientCode, $serverCode); ++?> ++--EXPECTF-- ++Found the mime-type: text/html; ++string(4) "body" ++array(3) { ++ [0]=> ++ string(15) "HTTP/1.0 200 Ok" ++ [1]=> ++ string(24) "Content-Type: text/html;" ++ [2]=> ++ string(54) "Custom-Header: somevalue; param1=value1; param2=value2" ++} +diff --git a/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-003.phpt b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-003.phpt +new file mode 100644 +index 00000000000..4eff7fc63f3 +--- /dev/null ++++ b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-003.phpt +@@ -0,0 +1,49 @@ ++--TEST-- ++GHSA-v8xr-gpvj-cx9g: Header parser of http stream wrapper does not handle folded headers (empty) ++--FILE-- ++<?php ++$serverCode = <<<'CODE' ++ $ctxt = stream_context_create([ ++ "socket" => [ ++ "tcp_nodelay" => true ++ ] ++ ]); ++ ++ $server = stream_socket_server( ++ "tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt); ++ phpt_notify_server_start($server); ++ ++ $conn = stream_socket_accept($server); ++ ++ phpt_notify(WORKER_DEFAULT_NAME, "server-accepted"); ++ ++ fwrite($conn, "HTTP/1.0 200 Ok\r\nContent-Type: text/html;\r\n \r\n charset=utf-8\r\n\r\nbody\r\n"); ++CODE; ++ ++$clientCode = <<<'CODE' ++ function stream_notification_callback($notification_code, $severity, $message, $message_code, $bytes_transferred, $bytes_max) { ++ switch($notification_code) { ++ case STREAM_NOTIFY_MIME_TYPE_IS: ++ echo "Found the mime-type: ", $message, PHP_EOL; ++ break; ++ } ++ } ++ ++ $ctx = stream_context_create(); ++ stream_context_set_params($ctx, array("notification" => "stream_notification_callback")); ++ var_dump(trim(file_get_contents("http://{{ ADDR }}", false, $ctx))); ++ var_dump($http_response_header); ++CODE; ++ ++include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++ServerClientTestCase::getInstance()->run($clientCode, $serverCode); ++?> ++--EXPECTF-- ++Found the mime-type: text/html; charset=utf-8 ++string(4) "body" ++array(2) { ++ [0]=> ++ string(15) "HTTP/1.0 200 Ok" ++ [1]=> ++ string(38) "Content-Type: text/html; charset=utf-8" ++} +diff --git a/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-004.phpt b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-004.phpt +new file mode 100644 +index 00000000000..71aed2fa2e8 +--- /dev/null ++++ b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-004.phpt +@@ -0,0 +1,48 @@ ++--TEST-- ++GHSA-v8xr-gpvj-cx9g: Header parser of http stream wrapper does not handle folded headers (first line) ++--FILE-- ++<?php ++$serverCode = <<<'CODE' ++ $ctxt = stream_context_create([ ++ "socket" => [ ++ "tcp_nodelay" => true ++ ] ++ ]); ++ ++ $server = stream_socket_server( ++ "tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt); ++ phpt_notify_server_start($server); ++ ++ $conn = stream_socket_accept($server); ++ ++ phpt_notify(WORKER_DEFAULT_NAME, "server-accepted"); ++ ++ fwrite($conn, "HTTP/1.0 200 Ok\r\n Content-Type: text/html;\r\n \r\n charset=utf-8\r\n\r\nbody\r\n"); ++CODE; ++ ++$clientCode = <<<'CODE' ++ function stream_notification_callback($notification_code, $severity, $message, $message_code, $bytes_transferred, $bytes_max) { ++ switch($notification_code) { ++ case STREAM_NOTIFY_MIME_TYPE_IS: ++ echo "Found the mime-type: ", $message, PHP_EOL; ++ break; ++ } ++ } ++ ++ $ctx = stream_context_create(); ++ stream_context_set_params($ctx, array("notification" => "stream_notification_callback")); ++ var_dump(file_get_contents("http://{{ ADDR }}", false, $ctx)); ++ var_dump($http_response_header); ++CODE; ++ ++include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++ServerClientTestCase::getInstance()->run($clientCode, $serverCode); ++?> ++--EXPECTF-- ++ ++Warning: file_get_contents(http://127.0.0.1:%d): failed to open stream: HTTP invalid response format (folding header at the start)! in %s ++bool(false) ++array(1) { ++ [0]=> ++ string(15) "HTTP/1.0 200 Ok" ++} +diff --git a/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-005.phpt b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-005.phpt +new file mode 100644 +index 00000000000..49d845d84b4 +--- /dev/null ++++ b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-005.phpt +@@ -0,0 +1,48 @@ ++--TEST-- ++GHSA-v8xr-gpvj-cx9g: Header parser of http stream wrapper does not handle folded headers (CR before header name) ++--FILE-- ++<?php ++$serverCode = <<<'CODE' ++ $ctxt = stream_context_create([ ++ "socket" => [ ++ "tcp_nodelay" => true ++ ] ++ ]); ++ ++ $server = stream_socket_server( ++ "tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt); ++ phpt_notify_server_start($server); ++ ++ $conn = stream_socket_accept($server); ++ ++ phpt_notify(WORKER_DEFAULT_NAME, "server-accepted"); ++ ++ fwrite($conn, "HTTP/1.0 200 Ok\r\n\rIgnored: ignored\r\n\r\nbody\r\n"); ++CODE; ++ ++$clientCode = <<<'CODE' ++ function stream_notification_callback($notification_code, $severity, $message, $message_code, $bytes_transferred, $bytes_max) { ++ switch($notification_code) { ++ case STREAM_NOTIFY_MIME_TYPE_IS: ++ echo "Found the mime-type: ", $message, PHP_EOL; ++ break; ++ } ++ } ++ ++ $ctx = stream_context_create(); ++ stream_context_set_params($ctx, array("notification" => "stream_notification_callback")); ++ var_dump(file_get_contents("http://{{ ADDR }}", false, $ctx)); ++ var_dump($http_response_header); ++CODE; ++ ++include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++ServerClientTestCase::getInstance()->run($clientCode, $serverCode); ++?> ++--EXPECTF-- ++ ++Warning: file_get_contents(http://127.0.0.1:%d): failed to open stream: HTTP invalid header name (cannot start with CR character)! in %s ++bool(false) ++array(1) { ++ [0]=> ++ string(15) "HTTP/1.0 200 Ok" ++} +diff --git a/ext/standard/tests/http/http_response_header_05.phpt b/ext/standard/tests/http/http_response_header_05.phpt +deleted file mode 100644 +index dbdd7b8b1a0..00000000000 +--- a/ext/standard/tests/http/http_response_header_05.phpt ++++ /dev/null +@@ -1,35 +0,0 @@ +---TEST-- +-$http_reponse_header (whitespace-only "header") +---SKIPIF-- +-<?php require 'server.inc'; http_server_skipif('tcp://127.0.0.1:22350'); ?> +---INI-- +-allow_url_fopen=1 +---FILE-- +-<?php +-require 'server.inc'; +- +-$responses = array( +- "data://text/plain,HTTP/1.0 200 Ok\r\n \r\n\r\nBody", +-); +- +-$pid = http_server("tcp://127.0.0.1:22350", $responses, $output); +- +-function test() { +- $f = file_get_contents('http://127.0.0.1:22350/'); +- var_dump($f); +- var_dump($http_response_header); +-} +-test(); +- +-http_server_kill($pid); +-?> +-==DONE== +---EXPECT-- +-string(4) "Body" +-array(2) { +- [0]=> +- string(15) "HTTP/1.0 200 Ok" +- [1]=> +- string(0) "" +-} +-==DONE== +-- +2.48.1 + diff --git a/php-cve-2025-1219.patch b/php-cve-2025-1219.patch new file mode 100644 index 0000000..5d164b6 --- /dev/null +++ b/php-cve-2025-1219.patch @@ -0,0 +1,1906 @@ +From 8a2195d2c64e0323d05656238c5c72414e8ad340 Mon Sep 17 00:00:00 2001 +From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> +Date: Sat, 29 Apr 2023 21:07:50 +0200 +Subject: [PATCH 05/11] Fix GH-11160: Few tests failed building with new libxml + 2.11.0 + +It's possible to categorise the failures into 2 categories: + - Changed error message. In this case we either duplicate the test and + modify the error message. Or if the change in error message is + small, we use the EXPECTF matchers to make the test compatible with both + old and new versions of libxml2. + - Missing warnings. This is caused by a change in libxml2 where the + parser started using SAX APIs internally [1]. In this case the + error_type passed to php_libxml_internal_error_handler() changed from + PHP_LIBXML_ERROR to PHP_LIBXML_CTX_WARNING because it internally + started to use the SAX handlers instead of the generic handlers. + However, for the SAX handlers the current input stack is empty, so + nothing is actually printed. I fixed this by falling back to a + regular warning without a filename & line number reference, which + mimicks the old behaviour. Furthermore, this change now also shows + an additional warning in a test which was previously hidden. + +[1] https://gitlab.gnome.org/GNOME/libxml2/-/commit/9a82b94a94bd310db426edd453b0f38c6c8f69f5 + +Closes GH-11162. + +(cherry picked from commit 7c0dfc5cf58d3c445b935fa14ea8f5f13568c419) +(cherry picked from commit 78ae0886bd1a3e42c53c9ba65764b6e6357640b5) +--- + .../DOMDocument_loadXML_error2_gte2_11.phpt | 34 +++ + ...> DOMDocument_loadXML_error2_pre2_11.phpt} | 7 +- + .../DOMDocument_load_error2_gte2_11.phpt | 34 +++ + ...t => DOMDocument_load_error2_pre2_11.phpt} | 7 +- + ext/libxml/libxml.c | 2 + + ext/libxml/tests/bug61367-read_2.phpt | 2 +- + .../tests/libxml_disable_entity_loader_2.phpt | 2 +- + ...xml_set_external_entity_loader_error1.phpt | 2 + + ...set_external_entity_loader_variation2.phpt | 2 + + ext/openssl/tests/ServerClientTestCase.inc | 65 ++---- + .../tests/http/ServerClientTestCase.inc | 199 ++++++++++++++++++ + .../tests/http/ghsa-52jp-hrpf-2jff-001.phpt | 2 +- + .../tests/http/ghsa-52jp-hrpf-2jff-002.phpt | 2 +- + .../tests/http/ghsa-hgf5-96fm-v528-001.phpt | 2 +- + .../tests/http/ghsa-hgf5-96fm-v528-002.phpt | 2 +- + .../tests/http/ghsa-hgf5-96fm-v528-003.phpt | 2 +- + .../tests/http/ghsa-pcmh-g36c-qc44-001.phpt | 2 +- + .../tests/http/ghsa-pcmh-g36c-qc44-002.phpt | 2 +- + .../tests/http/ghsa-v8xr-gpvj-cx9g-001.phpt | 2 +- + .../tests/http/ghsa-v8xr-gpvj-cx9g-002.phpt | 2 +- + .../tests/http/ghsa-v8xr-gpvj-cx9g-003.phpt | 2 +- + .../tests/http/ghsa-v8xr-gpvj-cx9g-004.phpt | 2 +- + .../tests/http/ghsa-v8xr-gpvj-cx9g-005.phpt | 2 +- + ext/xml/tests/bug26614_libxml_gte2_11.phpt | 95 +++++++++ + ...bxml.phpt => bug26614_libxml_pre2_11.phpt} | 1 + + 25 files changed, 408 insertions(+), 68 deletions(-) + create mode 100644 ext/dom/tests/DOMDocument_loadXML_error2_gte2_11.phpt + rename ext/dom/tests/{DOMDocument_loadXML_error2.phpt => DOMDocument_loadXML_error2_pre2_11.phpt} (89%) + create mode 100644 ext/dom/tests/DOMDocument_load_error2_gte2_11.phpt + rename ext/dom/tests/{DOMDocument_load_error2.phpt => DOMDocument_load_error2_pre2_11.phpt} (89%) + create mode 100644 ext/standard/tests/http/ServerClientTestCase.inc + create mode 100644 ext/xml/tests/bug26614_libxml_gte2_11.phpt + rename ext/xml/tests/{bug26614_libxml.phpt => bug26614_libxml_pre2_11.phpt} (96%) + +diff --git a/ext/dom/tests/DOMDocument_loadXML_error2_gte2_11.phpt b/ext/dom/tests/DOMDocument_loadXML_error2_gte2_11.phpt +new file mode 100644 +index 00000000000..ff5ceb3fbed +--- /dev/null ++++ b/ext/dom/tests/DOMDocument_loadXML_error2_gte2_11.phpt +@@ -0,0 +1,34 @@ ++--TEST-- ++Test DOMDocument::loadXML() detects not-well formed XML ++--SKIPIF-- ++<?php ++if (LIBXML_VERSION < 21100) die('skip libxml2 test variant for version >= 2.11'); ++?> ++--DESCRIPTION-- ++This test verifies the method detects attributes values not closed between " or ' ++Environment variables used in the test: ++- XML_FILE: the xml file to load ++- LOAD_OPTIONS: the second parameter to pass to the method ++- EXPECTED_RESULT: the expected result ++--CREDITS-- ++Antonio Diaz Ruiz <dejalatele@gmail.com> ++--INI-- ++assert.bail=true ++--EXTENSIONS-- ++dom ++--ENV-- ++XML_FILE=/not_well_formed2.xml ++LOAD_OPTIONS=0 ++EXPECTED_RESULT=0 ++--FILE_EXTERNAL-- ++domdocumentloadxml_test_method.inc ++--EXPECTF-- ++Warning: DOMDocument::loadXML(): AttValue: " or ' expected in Entity, line: 4 in %s on line %d ++ ++Warning: DOMDocument::loadXML(): internal error: xmlParseStartTag: problem parsing attributes in Entity, line: 4 in %s on line %d ++ ++Warning: DOMDocument::loadXML(): Couldn't find end of Start Tag book line 4 in Entity, line: 4 in %s on line %d ++ ++Warning: DOMDocument::loadXML(): Opening and ending tag mismatch: books line 3 and book in Entity, line: 7 in %s on line %d ++ ++Warning: DOMDocument::loadXML(): Extra content at the end of the document in Entity, line: 8 in %s on line %d +diff --git a/ext/dom/tests/DOMDocument_loadXML_error2.phpt b/ext/dom/tests/DOMDocument_loadXML_error2_pre2_11.phpt +similarity index 89% +rename from ext/dom/tests/DOMDocument_loadXML_error2.phpt +rename to ext/dom/tests/DOMDocument_loadXML_error2_pre2_11.phpt +index 6d56a317ed7..7e10771fdb7 100644 +--- a/ext/dom/tests/DOMDocument_loadXML_error2.phpt ++++ b/ext/dom/tests/DOMDocument_loadXML_error2_pre2_11.phpt +@@ -1,5 +1,10 @@ + --TEST-- + Test DOMDocument::loadXML() detects not-well formed XML ++--SKIPIF-- ++<?php ++include('skipif.inc'); ++if (LIBXML_VERSION >= 21100) die('skip libxml2 test variant for version < 2.11'); ++?> + --DESCRIPTION-- + This test verifies the method detects attributes values not closed between " or ' + Environment variables used in the test: +@@ -10,8 +15,6 @@ Environment variables used in the test: + Antonio Diaz Ruiz <dejalatele@gmail.com> + --INI-- + assert.bail=true +---SKIPIF-- +-<?php include('skipif.inc'); ?> + --ENV-- + XML_FILE=/not_well_formed2.xml + LOAD_OPTIONS=0 +diff --git a/ext/dom/tests/DOMDocument_load_error2_gte2_11.phpt b/ext/dom/tests/DOMDocument_load_error2_gte2_11.phpt +new file mode 100644 +index 00000000000..32b6bf16114 +--- /dev/null ++++ b/ext/dom/tests/DOMDocument_load_error2_gte2_11.phpt +@@ -0,0 +1,34 @@ ++--TEST-- ++Test DOMDocument::load() detects not-well formed ++--SKIPIF-- ++<?php ++if (LIBXML_VERSION < 21100) die('skip libxml2 test variant for version >= 2.11'); ++?> ++--DESCRIPTION-- ++This test verifies the method detects attributes values not closed between " or ' ++Environment variables used in the test: ++- XML_FILE: the xml file to load ++- LOAD_OPTIONS: the second parameter to pass to the method ++- EXPECTED_RESULT: the expected result ++--CREDITS-- ++Antonio Diaz Ruiz <dejalatele@gmail.com> ++--INI-- ++assert.bail=true ++--EXTENSIONS-- ++dom ++--ENV-- ++XML_FILE=/not_well_formed2.xml ++LOAD_OPTIONS=0 ++EXPECTED_RESULT=0 ++--FILE_EXTERNAL-- ++domdocumentload_test_method.inc ++--EXPECTF-- ++Warning: DOMDocument::load(): AttValue: " or ' expected in %s on line %d ++ ++Warning: DOMDocument::load(): internal error: xmlParseStartTag: problem parsing attributes in %s on line %d ++ ++Warning: DOMDocument::load(): Couldn't find end of Start Tag book line 4 in %s on line %d ++ ++Warning: DOMDocument::load(): Opening and ending tag mismatch: books line 3 and book in %s on line %d ++ ++Warning: DOMDocument::load(): Extra content at the end of the document in %s on line %d +diff --git a/ext/dom/tests/DOMDocument_load_error2.phpt b/ext/dom/tests/DOMDocument_load_error2_pre2_11.phpt +similarity index 89% +rename from ext/dom/tests/DOMDocument_load_error2.phpt +rename to ext/dom/tests/DOMDocument_load_error2_pre2_11.phpt +index f450cf16545..74b20c171e0 100644 +--- a/ext/dom/tests/DOMDocument_load_error2.phpt ++++ b/ext/dom/tests/DOMDocument_load_error2_pre2_11.phpt +@@ -1,5 +1,10 @@ + --TEST-- + Test DOMDocument::load() detects not-well formed XML ++--SKIPIF-- ++<?php ++include('skipif.inc'); ++if (LIBXML_VERSION >= 21100) die('skip libxml2 test variant for version < 2.11'); ++?> + --DESCRIPTION-- + This test verifies the method detects attributes values not closed between " or ' + Environment variables used in the test: +@@ -10,8 +15,6 @@ Environment variables used in the test: + Antonio Diaz Ruiz <dejalatele@gmail.com> + --INI-- + assert.bail=true +---SKIPIF-- +-<?php include('skipif.inc'); ?> + --ENV-- + XML_FILE=/not_well_formed2.xml + LOAD_OPTIONS=0 +diff --git a/ext/libxml/libxml.c b/ext/libxml/libxml.c +index d343135b98d..5d9c23e0d7e 100644 +--- a/ext/libxml/libxml.c ++++ b/ext/libxml/libxml.c +@@ -574,6 +574,8 @@ static void php_libxml_ctx_error_level(int level, void *ctx, const char *msg) + } else { + php_error_docref(NULL, level, "%s in Entity, line: %d", msg, parser->input->line); + } ++ } else { ++ php_error_docref(NULL, E_WARNING, "%s", msg); + } + } + +diff --git a/ext/libxml/tests/bug61367-read_2.phpt b/ext/libxml/tests/bug61367-read_2.phpt +index 8cc0b50144c..12743adab1c 100644 +--- a/ext/libxml/tests/bug61367-read_2.phpt ++++ b/ext/libxml/tests/bug61367-read_2.phpt +@@ -55,6 +55,6 @@ bool(true) + int(4) + bool(true) + +-Warning: DOMDocument::loadXML(): I/O warning : failed to load external entity "file:///%s/test_bug_61367-read/bad" in %s on line %d ++Warning: DOMDocument::loadXML(): %Sfailed to load external entity "file:///%s/test_bug_61367-read/bad" in %s on line %d + + Notice: Trying to get property 'nodeValue' of non-object in %s on line %d +diff --git a/ext/libxml/tests/libxml_disable_entity_loader_2.phpt b/ext/libxml/tests/libxml_disable_entity_loader_2.phpt +index 845bd4bbe3c..55d8e61ee09 100644 +--- a/ext/libxml/tests/libxml_disable_entity_loader_2.phpt ++++ b/ext/libxml/tests/libxml_disable_entity_loader_2.phpt +@@ -36,6 +36,6 @@ echo "Done\n"; + bool(true) + bool(false) + +-Warning: DOMDocument::loadXML(): I/O warning : failed to load external entity "%s" in %s on line %d ++Warning: DOMDocument::loadXML(): %Sfailed to load external entity "%s" in %s on line %d + bool(true) + Done +diff --git a/ext/libxml/tests/libxml_set_external_entity_loader_error1.phpt b/ext/libxml/tests/libxml_set_external_entity_loader_error1.phpt +index 40b31ea85d3..00e06eb8a25 100644 +--- a/ext/libxml/tests/libxml_set_external_entity_loader_error1.phpt ++++ b/ext/libxml/tests/libxml_set_external_entity_loader_error1.phpt +@@ -35,6 +35,8 @@ Warning: libxml_set_external_entity_loader() expects exactly 1 parameter, 2 give + NULL + bool(true) + ++Warning: DOMDocument::validate(): Call to user entity loader callback %s ++ + Warning: DOMDocument::validate(): Could not load the external subset "http://example.com/foobar" in %s on line %d + Exception: Too few arguments to function {closure}(), 3 passed and exactly 4 expected + Done. +diff --git a/ext/libxml/tests/libxml_set_external_entity_loader_variation2.phpt b/ext/libxml/tests/libxml_set_external_entity_loader_variation2.phpt +index e51869cf47f..0664de1ea6b 100644 +--- a/ext/libxml/tests/libxml_set_external_entity_loader_variation2.phpt ++++ b/ext/libxml/tests/libxml_set_external_entity_loader_variation2.phpt +@@ -38,6 +38,8 @@ echo "Done.\n"; + string(10) "-//FOO/BAR" + string(%d) "%sfoobar.dtd" + ++Warning: DOMDocument::validate(): Failed to load external entity "-//FOO/BAR" in %s on line %d ++ + Warning: DOMDocument::validate(): Could not load the external subset "foobar.dtd" in %s on line %d + bool(false) + bool(true) +diff --git a/ext/openssl/tests/ServerClientTestCase.inc b/ext/openssl/tests/ServerClientTestCase.inc +index c74da444102..753366df6f4 100644 +--- a/ext/openssl/tests/ServerClientTestCase.inc ++++ b/ext/openssl/tests/ServerClientTestCase.inc +@@ -4,19 +4,14 @@ const WORKER_ARGV_VALUE = 'RUN_WORKER'; + + const WORKER_DEFAULT_NAME = 'server'; + +-function phpt_notify(string $worker = WORKER_DEFAULT_NAME, string $message = ""): void ++function phpt_notify($worker = WORKER_DEFAULT_NAME) + { +- ServerClientTestCase::getInstance()->notify($worker, $message); ++ ServerClientTestCase::getInstance()->notify($worker); + } + +-function phpt_wait($worker = WORKER_DEFAULT_NAME, $timeout = null): ?string ++function phpt_wait($worker = WORKER_DEFAULT_NAME, $timeout = null) + { +- return ServerClientTestCase::getInstance()->wait($worker, $timeout); +-} +- +-function phpt_notify_server_start($server): void +-{ +- ServerClientTestCase::getInstance()->notify_server_start($server); ++ ServerClientTestCase::getInstance()->wait($worker, $timeout); + } + + function phpt_has_sslv3() { +@@ -124,73 +119,43 @@ class ServerClientTestCase + eval($code); + } + +- /** +- * Run client and all workers +- * +- * @param string $clientCode The client PHP code +- * @param string|array $workerCode +- * @param bool $ephemeral Select whether automatic port selection and automatic awaiting is used +- * @return void +- * @throws Exception +- */ +- public function run(string $clientCode, $workerCode, bool $ephemeral = true): void ++ public function run($masterCode, $workerCode) + { + if (!is_array($workerCode)) { + $workerCode = [WORKER_DEFAULT_NAME => $workerCode]; + } +- reset($workerCode); +- $code = current($workerCode); +- $worker = key($workerCode); +- while ($worker != null) { ++ foreach ($workerCode as $worker => $code) { + $this->spawnWorkerProcess($worker, $this->stripPhpTagsFromCode($code)); +- $code = next($workerCode); +- if ($ephemeral) { +- $addr = trim($this->wait($worker)); +- if (empty($addr)) { +- throw new \Exception("Failed server start"); +- } +- if ($code === false) { +- $clientCode = preg_replace('/{{\s*ADDR\s*}}/', $addr, $clientCode); +- } else { +- $code = preg_replace('/{{\s*ADDR\s*}}/', $addr, $code); +- } +- } +- $worker = key($workerCode); + } +- +- eval($this->stripPhpTagsFromCode($clientCode)); ++ eval($this->stripPhpTagsFromCode($masterCode)); + foreach ($workerCode as $worker => $code) { + $this->cleanupWorkerProcess($worker); + } + } + +- public function wait($worker, $timeout = null): ?string ++ public function wait($worker, $timeout = null) + { + $handle = $this->isWorker ? STDIN : $this->workerStdOut[$worker]; + if ($timeout === null) { +- return fgets($handle); ++ fgets($handle); ++ return true; + } + + stream_set_blocking($handle, false); + $read = [$handle]; + $result = stream_select($read, $write, $except, $timeout); + if (!$result) { +- return null; ++ return false; + } + +- $result = fgets($handle); ++ fgets($handle); + stream_set_blocking($handle, true); +- return $result; +- } +- +- public function notify(string $worker, string $message = ""): void +- { +- fwrite($this->isWorker ? STDOUT : $this->workerStdIn[$worker], "$message\n"); ++ return true; + } + +- public function notify_server_start($server): void ++ public function notify($worker) + { +- echo stream_socket_get_name($server, false) . "\n"; ++ fwrite($this->isWorker ? STDOUT : $this->workerStdIn[$worker], "\n"); + } + } + +diff --git a/ext/standard/tests/http/ServerClientTestCase.inc b/ext/standard/tests/http/ServerClientTestCase.inc +new file mode 100644 +index 00000000000..c74da444102 +--- /dev/null ++++ b/ext/standard/tests/http/ServerClientTestCase.inc +@@ -0,0 +1,199 @@ ++<?php ++ ++const WORKER_ARGV_VALUE = 'RUN_WORKER'; ++ ++const WORKER_DEFAULT_NAME = 'server'; ++ ++function phpt_notify(string $worker = WORKER_DEFAULT_NAME, string $message = ""): void ++{ ++ ServerClientTestCase::getInstance()->notify($worker, $message); ++} ++ ++function phpt_wait($worker = WORKER_DEFAULT_NAME, $timeout = null): ?string ++{ ++ return ServerClientTestCase::getInstance()->wait($worker, $timeout); ++} ++ ++function phpt_notify_server_start($server): void ++{ ++ ServerClientTestCase::getInstance()->notify_server_start($server); ++} ++ ++function phpt_has_sslv3() { ++ static $result = null; ++ if (!is_null($result)) { ++ return $result; ++ } ++ $server = @stream_socket_server('sslv3://127.0.0.1:10013'); ++ if ($result = !!$server) { ++ fclose($server); ++ } ++ return $result; ++} ++ ++/** ++ * This is a singleton to let the wait/notify functions work ++ * I know it's horrible, but it's a means to an end ++ */ ++class ServerClientTestCase ++{ ++ private $isWorker = false; ++ ++ private $workerHandle = []; ++ ++ private $workerStdIn = []; ++ ++ private $workerStdOut = []; ++ ++ private static $instance; ++ ++ public static function getInstance($isWorker = false) ++ { ++ if (!isset(self::$instance)) { ++ self::$instance = new self($isWorker); ++ } ++ ++ return self::$instance; ++ } ++ ++ public function __construct($isWorker = false) ++ { ++ if (!isset(self::$instance)) { ++ self::$instance = $this; ++ } ++ ++ $this->isWorker = $isWorker; ++ } ++ ++ private function spawnWorkerProcess($worker, $code) ++ { ++ if (defined("PHP_WINDOWS_VERSION_MAJOR")) { ++ $ini = php_ini_loaded_file(); ++ $cmd = sprintf( ++ '%s %s "%s" %s', ++ PHP_BINARY, $ini ? "-n -c $ini" : "", ++ __FILE__, ++ WORKER_ARGV_VALUE ++ ); ++ } else { ++ $cmd = sprintf( ++ '%s "%s" %s %s', ++ PHP_BINARY, ++ __FILE__, ++ WORKER_ARGV_VALUE, ++ $worker ++ ); ++ } ++ $this->workerHandle[$worker] = proc_open( ++ $cmd, ++ [['pipe', 'r'], ['pipe', 'w'], STDERR], ++ $pipes ++ ); ++ $this->workerStdIn[$worker] = $pipes[0]; ++ $this->workerStdOut[$worker] = $pipes[1]; ++ ++ fwrite($this->workerStdIn[$worker], $code . "\n---\n"); ++ } ++ ++ private function cleanupWorkerProcess($worker) ++ { ++ fclose($this->workerStdIn[$worker]); ++ fclose($this->workerStdOut[$worker]); ++ proc_close($this->workerHandle[$worker]); ++ } ++ ++ private function stripPhpTagsFromCode($code) ++ { ++ return preg_replace('/^\s*<\?(?:php)?|\?>\s*$/i', '', $code); ++ } ++ ++ public function runWorker() ++ { ++ $code = ''; ++ ++ while (1) { ++ $line = fgets(STDIN); ++ ++ if (trim($line) === "---") { ++ break; ++ } ++ ++ $code .= $line; ++ } ++ ++ eval($code); ++ } ++ ++ /** ++ * Run client and all workers ++ * ++ * @param string $clientCode The client PHP code ++ * @param string|array $workerCode ++ * @param bool $ephemeral Select whether automatic port selection and automatic awaiting is used ++ * @return void ++ * @throws Exception ++ */ ++ public function run(string $clientCode, $workerCode, bool $ephemeral = true): void ++ { ++ if (!is_array($workerCode)) { ++ $workerCode = [WORKER_DEFAULT_NAME => $workerCode]; ++ } ++ reset($workerCode); ++ $code = current($workerCode); ++ $worker = key($workerCode); ++ while ($worker != null) { ++ $this->spawnWorkerProcess($worker, $this->stripPhpTagsFromCode($code)); ++ $code = next($workerCode); ++ if ($ephemeral) { ++ $addr = trim($this->wait($worker)); ++ if (empty($addr)) { ++ throw new \Exception("Failed server start"); ++ } ++ if ($code === false) { ++ $clientCode = preg_replace('/{{\s*ADDR\s*}}/', $addr, $clientCode); ++ } else { ++ $code = preg_replace('/{{\s*ADDR\s*}}/', $addr, $code); ++ } ++ } ++ $worker = key($workerCode); ++ } ++ ++ eval($this->stripPhpTagsFromCode($clientCode)); ++ foreach ($workerCode as $worker => $code) { ++ $this->cleanupWorkerProcess($worker); ++ } ++ } ++ ++ public function wait($worker, $timeout = null): ?string ++ { ++ $handle = $this->isWorker ? STDIN : $this->workerStdOut[$worker]; ++ if ($timeout === null) { ++ return fgets($handle); ++ } ++ ++ stream_set_blocking($handle, false); ++ $read = [$handle]; ++ $result = stream_select($read, $write, $except, $timeout); ++ if (!$result) { ++ return null; ++ } ++ ++ $result = fgets($handle); ++ stream_set_blocking($handle, true); ++ return $result; ++ } ++ ++ public function notify(string $worker, string $message = ""): void ++ { ++ fwrite($this->isWorker ? STDOUT : $this->workerStdIn[$worker], "$message\n"); ++ } ++ ++ public function notify_server_start($server): void ++ { ++ echo stream_socket_get_name($server, false) . "\n"; ++ } ++} ++ ++if (isset($argv[1]) && $argv[1] === WORKER_ARGV_VALUE) { ++ ServerClientTestCase::getInstance(true)->runWorker(); ++} +diff --git a/ext/standard/tests/http/ghsa-52jp-hrpf-2jff-001.phpt b/ext/standard/tests/http/ghsa-52jp-hrpf-2jff-001.phpt +index 46d77ec4aff..3475a03beed 100644 +--- a/ext/standard/tests/http/ghsa-52jp-hrpf-2jff-001.phpt ++++ b/ext/standard/tests/http/ghsa-52jp-hrpf-2jff-001.phpt +@@ -39,7 +39,7 @@ $clientCode = <<<'CODE' + var_dump($http_response_header); + CODE; + +-include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++include sprintf("%s/ServerClientTestCase.inc", __DIR__); + ServerClientTestCase::getInstance()->run($clientCode, $serverCode); + ?> + --EXPECTF-- +diff --git a/ext/standard/tests/http/ghsa-52jp-hrpf-2jff-002.phpt b/ext/standard/tests/http/ghsa-52jp-hrpf-2jff-002.phpt +index d25c89d06e5..706a85f410b 100644 +--- a/ext/standard/tests/http/ghsa-52jp-hrpf-2jff-002.phpt ++++ b/ext/standard/tests/http/ghsa-52jp-hrpf-2jff-002.phpt +@@ -39,7 +39,7 @@ $clientCode = <<<'CODE' + var_dump($http_response_header); + CODE; + +-include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++include sprintf("%s/ServerClientTestCase.inc", __DIR__); + ServerClientTestCase::getInstance()->run($clientCode, $serverCode); + ?> + --EXPECTF-- +diff --git a/ext/standard/tests/http/ghsa-hgf5-96fm-v528-001.phpt b/ext/standard/tests/http/ghsa-hgf5-96fm-v528-001.phpt +index c8dcd47a4a4..121f077c9f5 100644 +--- a/ext/standard/tests/http/ghsa-hgf5-96fm-v528-001.phpt ++++ b/ext/standard/tests/http/ghsa-hgf5-96fm-v528-001.phpt +@@ -36,7 +36,7 @@ $clientCode = <<<'CODE' + var_dump($http_response_header); + CODE; + +-include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++include sprintf("%s/ServerClientTestCase.inc", __DIR__); + ServerClientTestCase::getInstance()->run($clientCode, $serverCode); + ?> + --EXPECTF-- +diff --git a/ext/standard/tests/http/ghsa-hgf5-96fm-v528-002.phpt b/ext/standard/tests/http/ghsa-hgf5-96fm-v528-002.phpt +index ca8f75f0327..0d141f93af3 100644 +--- a/ext/standard/tests/http/ghsa-hgf5-96fm-v528-002.phpt ++++ b/ext/standard/tests/http/ghsa-hgf5-96fm-v528-002.phpt +@@ -36,7 +36,7 @@ $clientCode = <<<'CODE' + var_dump($http_response_header); + CODE; + +-include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++include sprintf("%s/ServerClientTestCase.inc", __DIR__); + ServerClientTestCase::getInstance()->run($clientCode, $serverCode); + ?> + --EXPECTF-- +diff --git a/ext/standard/tests/http/ghsa-hgf5-96fm-v528-003.phpt b/ext/standard/tests/http/ghsa-hgf5-96fm-v528-003.phpt +index 4cfbc7ee804..8041487d044 100644 +--- a/ext/standard/tests/http/ghsa-hgf5-96fm-v528-003.phpt ++++ b/ext/standard/tests/http/ghsa-hgf5-96fm-v528-003.phpt +@@ -36,7 +36,7 @@ $clientCode = <<<'CODE' + var_dump($http_response_header); + CODE; + +-include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++include sprintf("%s/ServerClientTestCase.inc", __DIR__); + ServerClientTestCase::getInstance()->run($clientCode, $serverCode); + ?> + --EXPECTF-- +diff --git a/ext/standard/tests/http/ghsa-pcmh-g36c-qc44-001.phpt b/ext/standard/tests/http/ghsa-pcmh-g36c-qc44-001.phpt +index 53baa1c92d6..f491acfae27 100644 +--- a/ext/standard/tests/http/ghsa-pcmh-g36c-qc44-001.phpt ++++ b/ext/standard/tests/http/ghsa-pcmh-g36c-qc44-001.phpt +@@ -35,7 +35,7 @@ $clientCode = <<<'CODE' + var_dump($http_response_header); + CODE; + +-include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++include sprintf("%s/ServerClientTestCase.inc", __DIR__); + ServerClientTestCase::getInstance()->run($clientCode, $serverCode); + ?> + --EXPECTF-- +diff --git a/ext/standard/tests/http/ghsa-pcmh-g36c-qc44-002.phpt b/ext/standard/tests/http/ghsa-pcmh-g36c-qc44-002.phpt +index 5aa0ee00618..4320b17b97d 100644 +--- a/ext/standard/tests/http/ghsa-pcmh-g36c-qc44-002.phpt ++++ b/ext/standard/tests/http/ghsa-pcmh-g36c-qc44-002.phpt +@@ -35,7 +35,7 @@ $clientCode = <<<'CODE' + var_dump($http_response_header); + CODE; + +-include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++include sprintf("%s/ServerClientTestCase.inc", __DIR__); + ServerClientTestCase::getInstance()->run($clientCode, $serverCode); + ?> + --EXPECTF-- +diff --git a/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-001.phpt b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-001.phpt +index 64904bfcd1d..3f1cc79bd9c 100644 +--- a/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-001.phpt ++++ b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-001.phpt +@@ -35,7 +35,7 @@ $clientCode = <<<'CODE' + var_dump($http_response_header); + CODE; + +-include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++include sprintf("%s/ServerClientTestCase.inc", __DIR__); + ServerClientTestCase::getInstance()->run($clientCode, $serverCode); + ?> + --EXPECTF-- +diff --git a/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-002.phpt b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-002.phpt +index a6d9d00fd58..c7c13877fef 100644 +--- a/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-002.phpt ++++ b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-002.phpt +@@ -35,7 +35,7 @@ $clientCode = <<<'CODE' + var_dump($http_response_header); + CODE; + +-include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++include sprintf("%s/ServerClientTestCase.inc", __DIR__); + ServerClientTestCase::getInstance()->run($clientCode, $serverCode); + ?> + --EXPECTF-- +diff --git a/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-003.phpt b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-003.phpt +index 4eff7fc63f3..c67663b65f7 100644 +--- a/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-003.phpt ++++ b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-003.phpt +@@ -35,7 +35,7 @@ $clientCode = <<<'CODE' + var_dump($http_response_header); + CODE; + +-include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++include sprintf("%s/ServerClientTestCase.inc", __DIR__); + ServerClientTestCase::getInstance()->run($clientCode, $serverCode); + ?> + --EXPECTF-- +diff --git a/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-004.phpt b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-004.phpt +index 71aed2fa2e8..7a59e2688fd 100644 +--- a/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-004.phpt ++++ b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-004.phpt +@@ -35,7 +35,7 @@ $clientCode = <<<'CODE' + var_dump($http_response_header); + CODE; + +-include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++include sprintf("%s/ServerClientTestCase.inc", __DIR__); + ServerClientTestCase::getInstance()->run($clientCode, $serverCode); + ?> + --EXPECTF-- +diff --git a/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-005.phpt b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-005.phpt +index 49d845d84b4..f097762ef9e 100644 +--- a/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-005.phpt ++++ b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-005.phpt +@@ -35,7 +35,7 @@ $clientCode = <<<'CODE' + var_dump($http_response_header); + CODE; + +-include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++include sprintf("%s/ServerClientTestCase.inc", __DIR__); + ServerClientTestCase::getInstance()->run($clientCode, $serverCode); + ?> + --EXPECTF-- +diff --git a/ext/xml/tests/bug26614_libxml_gte2_11.phpt b/ext/xml/tests/bug26614_libxml_gte2_11.phpt +new file mode 100644 +index 00000000000..9a81b67686d +--- /dev/null ++++ b/ext/xml/tests/bug26614_libxml_gte2_11.phpt +@@ -0,0 +1,95 @@ ++--TEST-- ++Bug #26614 (CDATA sections skipped on line count) ++--EXTENSIONS-- ++xml ++--SKIPIF-- ++<?php ++if (!defined("LIBXML_VERSION")) die('skip libxml2 test'); ++if (LIBXML_VERSION < 21100) die('skip libxml2 test variant for version >= 2.11'); ++?> ++--FILE-- ++<?php ++/* ++this test works fine with Expat but fails with libxml ++which we now use as default ++ ++further investigation has shown that not only line count ++is skipped on CDATA sections but that libxml does also ++show different column numbers and byte positions depending ++on context and in opposition to what one would expect to ++see and what good old Expat reported just fine ... ++*/ ++ ++$xmls = array(); ++ ++// Case 1: CDATA Sections ++$xmls["CDATA"] ='<?xml version="1.0" encoding="iso-8859-1" ?> ++<data> ++<![CDATA[ ++multi ++line ++CDATA ++block ++]]> ++</data>'; ++ ++// Case 2: replace some characters so that we get comments instead ++$xmls["Comment"] ='<?xml version="1.0" encoding="iso-8859-1" ?> ++<data> ++<!-- ATA[ ++multi ++line ++CDATA ++block ++--> ++</data>'; ++ ++// Case 3: replace even more characters so that only textual data is left ++$xmls["Text"] ='<?xml version="1.0" encoding="iso-8859-1" ?> ++<data> ++-!-- ATA[ ++multi ++line ++CDATA ++block ++--- ++</data>'; ++ ++function startElement($parser, $name, $attrs) { ++ printf("<$name> at line %d, col %d (byte %d)\n", ++ xml_get_current_line_number($parser), ++ xml_get_current_column_number($parser), ++ xml_get_current_byte_index($parser)); ++} ++ ++function endElement($parser, $name) { ++ printf("</$name> at line %d, col %d (byte %d)\n", ++ xml_get_current_line_number($parser), ++ xml_get_current_column_number($parser), ++ xml_get_current_byte_index($parser)); ++} ++ ++function characterData($parser, $data) { ++ // dummy ++} ++ ++foreach ($xmls as $desc => $xml) { ++ echo "$desc\n"; ++ $xml_parser = xml_parser_create(); ++ xml_set_element_handler($xml_parser, "startElement", "endElement"); ++ xml_set_character_data_handler($xml_parser, "characterData"); ++ if (!xml_parse($xml_parser, $xml, true)) ++ echo "Error: ".xml_error_string(xml_get_error_code($xml_parser))."\n"; ++ xml_parser_free($xml_parser); ++} ++?> ++--EXPECTF-- ++CDATA ++<DATA> at line 2, col %d (byte 50) ++</DATA> at line 9, col %d (byte 96) ++Comment ++<DATA> at line 2, col %d (byte 50) ++</DATA> at line 9, col %d (byte 96) ++Text ++<DATA> at line 2, col %d (byte 50) ++</DATA> at line 9, col %d (byte 96) +diff --git a/ext/xml/tests/bug26614_libxml.phpt b/ext/xml/tests/bug26614_libxml_pre2_11.phpt +similarity index 96% +rename from ext/xml/tests/bug26614_libxml.phpt +rename to ext/xml/tests/bug26614_libxml_pre2_11.phpt +index 3ddd35ed0ea..afacaa1c59a 100644 +--- a/ext/xml/tests/bug26614_libxml.phpt ++++ b/ext/xml/tests/bug26614_libxml_pre2_11.phpt +@@ -4,6 +4,7 @@ Bug #26614 (CDATA sections skipped on line count) + <?php + require_once("skipif.inc"); + if (!defined("LIBXML_VERSION")) die('skip libxml2 test'); ++if (LIBXML_VERSION >= 21100) die('skip libxml2 test variant for version < 2.11'); + ?> + --FILE-- + <?php +-- +2.48.1 + +From 087e974d74efc977dfbd18fd3cd568b60fb7675d Mon Sep 17 00:00:00 2001 +From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> +Date: Fri, 1 Dec 2023 18:03:35 +0100 +Subject: [PATCH 06/11] Backport 0a39890c: Fix libxml2 2.12 build due to API + breaks + +See https://github.com/php/php-src/actions/runs/7062192818/job/19225478601 + +(cherry picked from commit fa6a0f80f644932506666beb7c85e4041c4a4646) +(cherry picked from commit 6e8e9f558aa0903e9650dd166a0a53c359d9e9e0) +--- + ext/libxml/libxml.c | 14 ++++++++++---- + ext/soap/php_sdl.c | 2 +- + 2 files changed, 11 insertions(+), 5 deletions(-) + +diff --git a/ext/libxml/libxml.c b/ext/libxml/libxml.c +index 5d9c23e0d7e..7917f636a9e 100644 +--- a/ext/libxml/libxml.c ++++ b/ext/libxml/libxml.c +@@ -530,7 +530,11 @@ static int _php_libxml_free_error(xmlErrorPtr error) + return 1; + } + +-static void _php_list_set_error_structure(xmlErrorPtr error, const char *msg) ++#if LIBXML_VERSION >= 21200 ++static void _php_list_set_error_structure(const xmlError *error, const char *msg) ++#else ++static void _php_list_set_error_structure(xmlError *error, const char *msg) ++#endif + { + xmlError error_copy; + int ret; +@@ -784,7 +788,11 @@ PHP_LIBXML_API void php_libxml_ctx_warning(void *ctx, const char *msg, ...) + va_end(args); + } + ++#if LIBXML_VERSION >= 21200 ++PHP_LIBXML_API void php_libxml_structured_error_handler(void *userData, const xmlError *error) ++#else + PHP_LIBXML_API void php_libxml_structured_error_handler(void *userData, xmlErrorPtr error) ++#endif + { + _php_list_set_error_structure(error, NULL); + +@@ -1063,9 +1071,7 @@ static PHP_FUNCTION(libxml_use_internal_errors) + Retrieve last error from libxml */ + static PHP_FUNCTION(libxml_get_last_error) + { +- xmlErrorPtr error; +- +- error = xmlGetLastError(); ++ const xmlError *error = xmlGetLastError(); + + if (error) { + object_init_ex(return_value, libxmlerror_class_entry); +diff --git a/ext/soap/php_sdl.c b/ext/soap/php_sdl.c +index 26a23f57db2..3df532a2d65 100644 +--- a/ext/soap/php_sdl.c ++++ b/ext/soap/php_sdl.c +@@ -333,7 +333,7 @@ static void load_wsdl_ex(zval *this_ptr, char *struri, sdlCtx *ctx, int include) + sdl_restore_uri_credentials(ctx); + + if (!wsdl) { +- xmlErrorPtr xmlErrorPtr = xmlGetLastError(); ++ const xmlError *xmlErrorPtr = xmlGetLastError(); + + if (xmlErrorPtr) { + soap_error2(E_ERROR, "Parsing WSDL: Couldn't load from '%s' : %s", struri, xmlErrorPtr->message); +-- +2.48.1 + +From aa8817ab42f758c988dfd3158f705da770238a88 Mon Sep 17 00:00:00 2001 +From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> +Date: Thu, 4 Jul 2024 06:29:50 -0700 +Subject: [PATCH 07/11] Backport 4fe82131: Backport libxml2 2.13.2 fixes + (#14816) + +Backproted from https://github.com/php/php-src/pull/14789 + +(cherry picked from commit bb46b4b799b583528025a775af45308133bfd4c1) +(cherry picked from commit 6cb68826aaf68ffe8c70c8782450c38970236040) +--- + ext/dom/document.c | 6 ++-- + .../DOMDocument_loadHTMLfile_error1.phpt | 2 +- + .../DOMDocument_relaxNGValidate_error2.phpt | 2 +- + .../tests/DOMDocument_saveHTMLFile_basic.phpt | 1 + + ...DOMDocument_saveHTMLFile_formatOutput.phpt | 1 + + ...nt_saveHTMLFile_formatOutput_gte_2_13.phpt | 32 +++++++++++++++++++ + .../DOMDocument_saveHTML_basic_gte_2_13.phpt | 31 ++++++++++++++++++ + .../DOMDocument_schemaValidate_error5.phpt | 2 +- + ext/dom/tests/dom_create_element.phpt | 14 +++----- + ext/libxml/libxml.c | 4 ++- + ext/simplexml/tests/bug79971_1.phpt | 2 +- + ext/soap/php_encoding.c | 9 ++++-- + ext/soap/php_xml.c | 4 ++- + ext/soap/tests/bugs/bug42151.phpt | 4 +-- + ext/xml/compat.c | 3 +- + ext/xmlwriter/php_xmlwriter.c | 3 +- + 16 files changed, 95 insertions(+), 25 deletions(-) + create mode 100644 ext/dom/tests/DOMDocument_saveHTMLFile_formatOutput_gte_2_13.phpt + create mode 100644 ext/dom/tests/DOMDocument_saveHTML_basic_gte_2_13.phpt + +diff --git a/ext/dom/document.c b/ext/dom/document.c +index 989b5b3dd24..af06fb41240 100644 +--- a/ext/dom/document.c ++++ b/ext/dom/document.c +@@ -1457,11 +1457,13 @@ static xmlDocPtr dom_document_parser(zval *id, int mode, char *source, size_t so + if (keep_blanks == 0 && ! (options & XML_PARSE_NOBLANKS)) { + options |= XML_PARSE_NOBLANKS; + } ++ if (recover) { ++ options |= XML_PARSE_RECOVER; ++ } + + php_libxml_sanitize_parse_ctxt_options(ctxt); + xmlCtxtUseOptions(ctxt, options); + +- ctxt->recovery = recover; + if (recover) { + old_error_reporting = EG(error_reporting); + EG(error_reporting) = old_error_reporting | E_WARNING; +@@ -1471,7 +1473,7 @@ static xmlDocPtr dom_document_parser(zval *id, int mode, char *source, size_t so + + if (ctxt->wellFormed || recover) { + ret = ctxt->myDoc; +- if (ctxt->recovery) { ++ if (recover) { + EG(error_reporting) = old_error_reporting; + } + /* If loading from memory, set the base reference uri for the document */ +diff --git a/ext/dom/tests/DOMDocument_loadHTMLfile_error1.phpt b/ext/dom/tests/DOMDocument_loadHTMLfile_error1.phpt +index cfb41686e87..fc78273c85f 100644 +--- a/ext/dom/tests/DOMDocument_loadHTMLfile_error1.phpt ++++ b/ext/dom/tests/DOMDocument_loadHTMLfile_error1.phpt +@@ -15,4 +15,4 @@ $result = $doc->loadHTMLFile(__DIR__ . "/ffff/test.html"); + assert($result === false); + ?> + --EXPECTF-- +-%r(PHP ){0,1}%rWarning: DOMDocument::loadHTMLFile(): I/O warning : failed to load external entity %s ++%r(PHP ){0,1}%rWarning: DOMDocument::loadHTMLFile(): I/O %s +diff --git a/ext/dom/tests/DOMDocument_relaxNGValidate_error2.phpt b/ext/dom/tests/DOMDocument_relaxNGValidate_error2.phpt +index cdd6e64194c..19bb4dce2d6 100644 +--- a/ext/dom/tests/DOMDocument_relaxNGValidate_error2.phpt ++++ b/ext/dom/tests/DOMDocument_relaxNGValidate_error2.phpt +@@ -22,7 +22,7 @@ $result = $doc->relaxNGValidate($rng); + var_dump($result); + ?> + --EXPECTF-- +-Warning: DOMDocument::relaxNGValidate(): I/O warning : failed to load external entity "%s/foo.rng" in %s on line %d ++Warning: DOMDocument::relaxNGValidate(): I/O %s : failed to load %s + + Warning: DOMDocument::relaxNGValidate(): xmlRelaxNGParse: could not load %s/foo.rng in %s on line %d + +diff --git a/ext/dom/tests/DOMDocument_saveHTMLFile_basic.phpt b/ext/dom/tests/DOMDocument_saveHTMLFile_basic.phpt +index f71db0c32a3..c51852e120c 100644 +--- a/ext/dom/tests/DOMDocument_saveHTMLFile_basic.phpt ++++ b/ext/dom/tests/DOMDocument_saveHTMLFile_basic.phpt +@@ -6,6 +6,7 @@ Knut Urdalen <knut@php.net> + --SKIPIF-- + <?php + require_once __DIR__ .'/skipif.inc'; ++if (LIBXML_VERSION >= 21300) die("skip see https://gitlab.gnome.org/GNOME/libxml2/-/issues/756"); + ?> + --FILE-- + <?php +diff --git a/ext/dom/tests/DOMDocument_saveHTMLFile_formatOutput.phpt b/ext/dom/tests/DOMDocument_saveHTMLFile_formatOutput.phpt +index 376c9a8e323..8d7baa7b7e8 100644 +--- a/ext/dom/tests/DOMDocument_saveHTMLFile_formatOutput.phpt ++++ b/ext/dom/tests/DOMDocument_saveHTMLFile_formatOutput.phpt +@@ -6,6 +6,7 @@ Knut Urdalen <knut@php.net> + --SKIPIF-- + <?php + require_once __DIR__ .'/skipif.inc'; ++if (LIBXML_VERSION >= 21300) die("skip see https://gitlab.gnome.org/GNOME/libxml2/-/issues/756"); + ?> + --FILE-- + <?php +diff --git a/ext/dom/tests/DOMDocument_saveHTMLFile_formatOutput_gte_2_13.phpt b/ext/dom/tests/DOMDocument_saveHTMLFile_formatOutput_gte_2_13.phpt +new file mode 100644 +index 00000000000..3477edfcf5f +--- /dev/null ++++ b/ext/dom/tests/DOMDocument_saveHTMLFile_formatOutput_gte_2_13.phpt +@@ -0,0 +1,32 @@ ++--TEST-- ++DOMDocument::saveHTMLFile() should format output on demand ++--CREDITS-- ++Knut Urdalen <knut@php.net> ++#PHPTestFest2009 Norway 2009-06-09 \o/ ++--EXTENSIONS-- ++dom ++--SKIPIF-- ++<?php ++if (LIBXML_VERSION < 21300) die("skip see https://gitlab.gnome.org/GNOME/libxml2/-/issues/756"); ++?> ++--FILE-- ++<?php ++$filename = __DIR__."/DOMDocument_saveHTMLFile_formatOutput_gte_2_13.html"; ++$doc = new DOMDocument('1.0'); ++$doc->formatOutput = true; ++$root = $doc->createElement('html'); ++$root = $doc->appendChild($root); ++$head = $doc->createElement('head'); ++$head = $root->appendChild($head); ++$title = $doc->createElement('title'); ++$title = $head->appendChild($title); ++$text = $doc->createTextNode('This is the title'); ++$text = $title->appendChild($text); ++$bytes = $doc->saveHTMLFile($filename); ++var_dump($bytes); ++echo file_get_contents($filename); ++unlink($filename); ++?> ++--EXPECT-- ++int(59) ++<html><head><title>This is the title</title></head></html> +diff --git a/ext/dom/tests/DOMDocument_saveHTML_basic_gte_2_13.phpt b/ext/dom/tests/DOMDocument_saveHTML_basic_gte_2_13.phpt +new file mode 100644 +index 00000000000..c0be105253d +--- /dev/null ++++ b/ext/dom/tests/DOMDocument_saveHTML_basic_gte_2_13.phpt +@@ -0,0 +1,31 @@ ++--TEST-- ++DOMDocument::saveHTMLFile() should dump the internal document into a file using HTML formatting ++--CREDITS-- ++Knut Urdalen <knut@php.net> ++#PHPTestFest2009 Norway 2009-06-09 \o/ ++--EXTENSIONS-- ++dom ++--SKIPIF-- ++<?php ++if (LIBXML_VERSION < 21300) die("skip see https://gitlab.gnome.org/GNOME/libxml2/-/issues/756"); ++?> ++--FILE-- ++<?php ++$filename = __DIR__."/DOMDocument_saveHTMLFile_basic_gte_2_13.html"; ++$doc = new DOMDocument('1.0'); ++$root = $doc->createElement('html'); ++$root = $doc->appendChild($root); ++$head = $doc->createElement('head'); ++$head = $root->appendChild($head); ++$title = $doc->createElement('title'); ++$title = $head->appendChild($title); ++$text = $doc->createTextNode('This is the title'); ++$text = $title->appendChild($text); ++$bytes = $doc->saveHTMLFile($filename); ++var_dump($bytes); ++echo file_get_contents($filename); ++unlink($filename); ++?> ++--EXPECT-- ++int(59) ++<html><head><title>This is the title</title></head></html> +diff --git a/ext/dom/tests/DOMDocument_schemaValidate_error5.phpt b/ext/dom/tests/DOMDocument_schemaValidate_error5.phpt +index cb57b55b41a..44ea52c2d06 100644 +--- a/ext/dom/tests/DOMDocument_schemaValidate_error5.phpt ++++ b/ext/dom/tests/DOMDocument_schemaValidate_error5.phpt +@@ -17,7 +17,7 @@ var_dump($result); + + ?> + --EXPECTF-- +-Warning: DOMDocument::schemaValidate(): I/O warning : failed to load external entity "%snon-existent-file" in %s.php on line %d ++Warning: DOMDocument::schemaValidate(): I/O %s : failed to load %s + + Warning: DOMDocument::schemaValidate(): Failed to locate the main schema resource at '%s/non-existent-file'. in %s.php on line %d + +diff --git a/ext/dom/tests/dom_create_element.phpt b/ext/dom/tests/dom_create_element.phpt +index bd2c8f11dae..70ae54a11bb 100644 +--- a/ext/dom/tests/dom_create_element.phpt ++++ b/ext/dom/tests/dom_create_element.phpt +@@ -251,14 +251,10 @@ try { + print $e->getMessage() . "\n"; + } + +-/* This isn't because the xml namespace isn't there and we can't create it */ +-print "29 DOMElement::__construct('xml:valid', '', 'http://www.w3.org/XML/1998/namespace')\n"; +-try { +- $element = new DomElement('xml:valid', '', 'http://www.w3.org/XML/1998/namespace'); +- print "valid\n"; +-} catch (Exception $e) { +- print $e->getMessage() . "\n"; +-} ++/* There used to be a 29 here that tested DOMElement::__construct('xml:valid', '', 'http://www.w3.org/XML/1998/namespace'). ++ * In libxml2 version 2.12 or prior this didn't work because the xml namespace isn't there and you can't create it without ++ * a document. Starting from libxml2 version 2.13 it does actually work because the XML namespace is statically defined. ++ * The behaviour from version 2.13 is actually the desired behaviour anyway. */ + + + /* the qualifiedName or its prefix is "xmlns" and the namespaceURI is +@@ -378,8 +374,6 @@ Namespace Error + Namespace Error + 28 DOMDocument::createElementNS('http://www.w3.org/XML/1998/namespace', 'xml:valid') + valid +-29 DOMElement::__construct('xml:valid', '', 'http://www.w3.org/XML/1998/namespace') +-Namespace Error + 30 DOMDocument::createElementNS('http://wrong.namespaceURI.com', 'xmlns:valid') + Namespace Error + 31 DOMElement::__construct('xmlns:valid', '', 'http://wrong.namespaceURI.com') +diff --git a/ext/libxml/libxml.c b/ext/libxml/libxml.c +index 7917f636a9e..4b9e6a918d4 100644 +--- a/ext/libxml/libxml.c ++++ b/ext/libxml/libxml.c +@@ -476,8 +476,10 @@ php_libxml_input_buffer_create_filename(const char *URI, xmlCharEncoding enc) + static xmlOutputBufferPtr + php_libxml_output_buffer_create_filename(const char *URI, + xmlCharEncodingHandlerPtr encoder, +- int compression ATTRIBUTE_UNUSED) ++ int compression) + { ++ ZEND_IGNORE_VALUE(compression); ++ + xmlOutputBufferPtr ret; + xmlURIPtr puri; + void *context = NULL; +diff --git a/ext/simplexml/tests/bug79971_1.phpt b/ext/simplexml/tests/bug79971_1.phpt +index 197776d82d3..2ee24e89f12 100644 +--- a/ext/simplexml/tests/bug79971_1.phpt ++++ b/ext/simplexml/tests/bug79971_1.phpt +@@ -20,7 +20,7 @@ var_dump($sxe->asXML("$uri.out%00foo")); + --EXPECTF-- + Warning: simplexml_load_file(): URI must not contain percent-encoded NUL bytes in %s on line %d + +-Warning: simplexml_load_file(): I/O warning : failed to load external entity "%s/bug79971_1.xml%00foo" in %s on line %d ++Warning: simplexml_load_file(): I/O warning : failed to load %s + bool(false) + + Warning: SimpleXMLElement::asXML(): URI must not contain percent-encoded NUL bytes in %s on line %d +diff --git a/ext/soap/php_encoding.c b/ext/soap/php_encoding.c +index e0cf63dd1da..0a6edbf5a41 100644 +--- a/ext/soap/php_encoding.c ++++ b/ext/soap/php_encoding.c +@@ -3381,7 +3381,6 @@ xmlNsPtr encode_add_ns(xmlNodePtr node, const char* ns) + } else { + smart_str prefix = {0}; + int num = ++SOAP_GLOBAL(cur_uniq_ns); +- xmlChar *enc_ns; + + while (1) { + smart_str_appendl(&prefix, "ns", 2); +@@ -3395,9 +3394,15 @@ xmlNsPtr encode_add_ns(xmlNodePtr node, const char* ns) + num = ++SOAP_GLOBAL(cur_uniq_ns); + } + +- enc_ns = xmlEncodeSpecialChars(node->doc, BAD_CAST(ns)); ++ /* Starting with libxml 2.13, we don't have to do this workaround anymore, otherwise we get double-encoded ++ * entities. See libxml2 commit f506ec66547ef9bac97a2bf306d368ecea8c0c9e. */ ++#if LIBXML_VERSION < 21300 ++ xmlChar *enc_ns = xmlEncodeSpecialChars(node->doc, BAD_CAST(ns)); + xmlns = xmlNewNs(node->doc->children, enc_ns, BAD_CAST(prefix.s ? ZSTR_VAL(prefix.s) : "")); + xmlFree(enc_ns); ++#else ++ xmlns = xmlNewNs(node->doc->children, BAD_CAST(ns), BAD_CAST(prefix.s ? ZSTR_VAL(prefix.s) : "")); ++#endif + smart_str_free(&prefix); + } + } +diff --git a/ext/soap/php_xml.c b/ext/soap/php_xml.c +index 1bb7fa00a37..446017eb5c8 100644 +--- a/ext/soap/php_xml.c ++++ b/ext/soap/php_xml.c +@@ -94,13 +94,14 @@ xmlDocPtr soap_xmlParseFile(const char *filename) + zend_bool old; + + php_libxml_sanitize_parse_ctxt_options(ctxt); ++ /* TODO: In libxml2 2.14.0 change this to the new options API so we don't rely on deprecated APIs. */ + ctxt->keepBlanks = 0; ++ ctxt->options |= XML_PARSE_HUGE; + ctxt->sax->ignorableWhitespace = soap_ignorableWhitespace; + ctxt->sax->comment = soap_Comment; + ctxt->sax->warning = NULL; + ctxt->sax->error = NULL; + /*ctxt->sax->fatalError = NULL;*/ +- ctxt->options |= XML_PARSE_HUGE; + old = php_libxml_disable_entity_loader(1); + xmlParseDocument(ctxt); + php_libxml_disable_entity_loader(old); +@@ -148,6 +149,7 @@ xmlDocPtr soap_xmlParseMemory(const void *buf, size_t buf_size) + ctxt->sax->warning = NULL; + ctxt->sax->error = NULL; + /*ctxt->sax->fatalError = NULL;*/ ++ /* TODO: In libxml2 2.14.0 change this to the new options API so we don't rely on deprecated APIs. */ + ctxt->options |= XML_PARSE_HUGE; + old = php_libxml_disable_entity_loader(1); + xmlParseDocument(ctxt); +diff --git a/ext/soap/tests/bugs/bug42151.phpt b/ext/soap/tests/bugs/bug42151.phpt +index ee53e6d525d..d1bcae83364 100644 +--- a/ext/soap/tests/bugs/bug42151.phpt ++++ b/ext/soap/tests/bugs/bug42151.phpt +@@ -25,8 +25,8 @@ try { + } + echo "ok\n"; + ?> +---EXPECT-- +-SOAP-ERROR: Parsing WSDL: Couldn't load from 'httpx://' : failed to load external entity "httpx://" ++--EXPECTF-- ++SOAP-ERROR: Parsing WSDL: Couldn't load from 'httpx://' : failed to load %s + + ok + I don't get executed either. +diff --git a/ext/xml/compat.c b/ext/xml/compat.c +index 57eb00dd429..ea1fd835059 100644 +--- a/ext/xml/compat.c ++++ b/ext/xml/compat.c +@@ -716,8 +716,7 @@ XML_GetCurrentByteCount(XML_Parser parser) + { + /* WARNING: this is identical to ByteIndex; it should probably + * be different */ +- return parser->parser->input->consumed + +- (parser->parser->input->cur - parser->parser->input->base); ++ return XML_GetCurrentByteIndex(parser); + } + + PHP_XML_API const XML_Char *XML_ExpatVersion(void) +diff --git a/ext/xmlwriter/php_xmlwriter.c b/ext/xmlwriter/php_xmlwriter.c +index 5cb141dad39..55874420f3b 100644 +--- a/ext/xmlwriter/php_xmlwriter.c ++++ b/ext/xmlwriter/php_xmlwriter.c +@@ -1785,7 +1785,8 @@ static void php_xmlwriter_flush(INTERNAL_FUNCTION_PARAMETERS, int force_string) + } + output_bytes = xmlTextWriterFlush(ptr); + if (buffer) { +- RETVAL_STRING((char *) buffer->content); ++ const xmlChar *content = xmlBufferContent(buffer); ++ RETVAL_STRING((const char *) content); + if (empty) { + xmlBufferEmpty(buffer); + } +-- +2.48.1 + +From 238d5f0aeaedc9c355f1bc1159b01e357bdaf344 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= <tim@tideways-gmbh.com> +Date: Wed, 20 Nov 2024 10:47:27 +0100 +Subject: [PATCH 08/11] Fix GHSA-p3x9-6h7p-cgfc: libxml streams wrong + `content-type` on redirect + +libxml streams use wrong content-type header when requesting a +redirected resource. + +(cherry picked from commit b6004a043c16b211d462218fbb3f72db68ec2b18) +(cherry picked from commit 1196e566681a34564c02173ba234b5a42587ff07) +--- + ext/dom/tests/ghsa-p3x9-6h7p-cgfc_001.phpt | 60 ++++++++++ + ext/dom/tests/ghsa-p3x9-6h7p-cgfc_002.phpt | 60 ++++++++++ + ext/dom/tests/ghsa-p3x9-6h7p-cgfc_003.phpt | 60 ++++++++++ + ext/libxml/libxml.c | 77 +++++++------ + ext/standard/tests/http/newserver.inc | 124 +++++++++++++++++++++ + 5 files changed, 348 insertions(+), 33 deletions(-) + create mode 100644 ext/dom/tests/ghsa-p3x9-6h7p-cgfc_001.phpt + create mode 100644 ext/dom/tests/ghsa-p3x9-6h7p-cgfc_002.phpt + create mode 100644 ext/dom/tests/ghsa-p3x9-6h7p-cgfc_003.phpt + create mode 100644 ext/standard/tests/http/newserver.inc + +diff --git a/ext/dom/tests/ghsa-p3x9-6h7p-cgfc_001.phpt b/ext/dom/tests/ghsa-p3x9-6h7p-cgfc_001.phpt +new file mode 100644 +index 00000000000..87cb2aa0b1f +--- /dev/null ++++ b/ext/dom/tests/ghsa-p3x9-6h7p-cgfc_001.phpt +@@ -0,0 +1,60 @@ ++--TEST-- ++GHSA-p3x9-6h7p-cgfc: libxml streams use wrong `content-type` header when requesting a redirected resource (Basic) ++--EXTENSIONS-- ++dom ++--SKIPIF-- ++<?php ++if (@!include "./ext/standard/tests/http/newserver.inc") die('skip server.inc not available'); ++http_server_skipif(); ++?> ++--FILE-- ++<?php ++require "./ext/standard/tests/http/newserver.inc"; ++ ++function genResponses($server) { ++ $uri = 'http://' . stream_socket_get_name($server, false); ++ yield "data://text/plain,HTTP/1.1 302 Moved Temporarily\r\nLocation: $uri/document.xml\r\nContent-Type: text/html;charset=utf-16\r\n\r\n"; ++ $xml = <<<'EOT' ++ <!doctype html> ++ <html> ++ <head> ++ <title>GHSA-p3x9-6h7p-cgfc</title> ++ ++ <meta charset="utf-8" /> ++ <meta http-equiv="Content-type" content="text/html; charset=utf-8" /> ++ </head> ++ ++ <body> ++ <h1>GHSA-p3x9-6h7p-cgfc</h1> ++ </body> ++ </html> ++ EOT; ++ // Intentionally using non-standard casing for content-type to verify it is matched not case sensitively. ++ yield "data://text/plain,HTTP/1.1 200 OK\r\nconteNt-tyPe: text/html; charset=utf-8\r\n\r\n{$xml}"; ++} ++ ++['pid' => $pid, 'uri' => $uri] = http_server('genResponses', $output); ++$document = new \DOMDocument(); ++$document->loadHTMLFile($uri); ++ ++$h1 = $document->getElementsByTagName('h1'); ++var_dump($h1->length); ++var_dump($document->saveHTML()); ++http_server_kill($pid); ++?> ++--EXPECT-- ++int(1) ++string(266) "<!DOCTYPE html> ++<html> ++ <head> ++ <title>GHSA-p3x9-6h7p-cgfc</title> ++ ++ <meta charset="utf-8"> ++ <meta http-equiv="Content-type" content="text/html; charset=utf-8"> ++ </head> ++ ++ <body> ++ <h1>GHSA-p3x9-6h7p-cgfc</h1> ++ </body> ++</html> ++" +diff --git a/ext/dom/tests/ghsa-p3x9-6h7p-cgfc_002.phpt b/ext/dom/tests/ghsa-p3x9-6h7p-cgfc_002.phpt +new file mode 100644 +index 00000000000..1ce468c3b19 +--- /dev/null ++++ b/ext/dom/tests/ghsa-p3x9-6h7p-cgfc_002.phpt +@@ -0,0 +1,60 @@ ++--TEST-- ++GHSA-p3x9-6h7p-cgfc: libxml streams use wrong `content-type` header when requesting a redirected resource (Missing content-type) ++--EXTENSIONS-- ++dom ++--SKIPIF-- ++<?php ++if (@!include "./ext/standard/tests/http/newserver.inc") die('skip server.inc not available'); ++http_server_skipif(); ++?> ++--FILE-- ++<?php ++require "./ext/standard/tests/http/newserver.inc"; ++ ++function genResponses($server) { ++ $uri = 'http://' . stream_socket_get_name($server, false); ++ yield "data://text/plain,HTTP/1.1 302 Moved Temporarily\r\nLocation: $uri/document.xml\r\nContent-Type: text/html;charset=utf-16\r\n\r\n"; ++ $xml = <<<'EOT' ++ <!doctype html> ++ <html> ++ <head> ++ <title>GHSA-p3x9-6h7p-cgfc</title> ++ ++ <meta charset="utf-8" /> ++ <meta http-equiv="Content-type" content="text/html; charset=utf-8" /> ++ </head> ++ ++ <body> ++ <h1>GHSA-p3x9-6h7p-cgfc</h1> ++ </body> ++ </html> ++ EOT; ++ // Missing content-type in actual response. ++ yield "data://text/plain,HTTP/1.1 200 OK\r\n\r\n{$xml}"; ++} ++ ++['pid' => $pid, 'uri' => $uri] = http_server('genResponses', $output); ++$document = new \DOMDocument(); ++$document->loadHTMLFile($uri); ++ ++$h1 = $document->getElementsByTagName('h1'); ++var_dump($h1->length); ++var_dump($document->saveHTML()); ++http_server_kill($pid); ++?> ++--EXPECT-- ++int(1) ++string(266) "<!DOCTYPE html> ++<html> ++ <head> ++ <title>GHSA-p3x9-6h7p-cgfc</title> ++ ++ <meta charset="utf-8"> ++ <meta http-equiv="Content-type" content="text/html; charset=utf-8"> ++ </head> ++ ++ <body> ++ <h1>GHSA-p3x9-6h7p-cgfc</h1> ++ </body> ++</html> ++" +diff --git a/ext/dom/tests/ghsa-p3x9-6h7p-cgfc_003.phpt b/ext/dom/tests/ghsa-p3x9-6h7p-cgfc_003.phpt +new file mode 100644 +index 00000000000..b8cac7e3247 +--- /dev/null ++++ b/ext/dom/tests/ghsa-p3x9-6h7p-cgfc_003.phpt +@@ -0,0 +1,60 @@ ++--TEST-- ++GHSA-p3x9-6h7p-cgfc: libxml streams use wrong `content-type` header when requesting a redirected resource (Reason with colon) ++--EXTENSIONS-- ++dom ++--SKIPIF-- ++<?php ++if (@!include "./ext/standard/tests/http/newserver.inc") die('skip server.inc not available'); ++http_server_skipif(); ++?> ++--FILE-- ++<?php ++require "./ext/standard/tests/http/newserver.inc"; ++ ++function genResponses($server) { ++ $uri = 'http://' . stream_socket_get_name($server, false); ++ yield "data://text/plain,HTTP/1.1 302 Moved Temporarily\r\nLocation: $uri/document.xml\r\nContent-Type: text/html;charset=utf-16\r\n\r\n"; ++ $xml = <<<'EOT' ++ <!doctype html> ++ <html> ++ <head> ++ <title>GHSA-p3x9-6h7p-cgfc</title> ++ ++ <meta charset="utf-8" /> ++ <meta http-equiv="Content-type" content="text/html; charset=utf-8" /> ++ </head> ++ ++ <body> ++ <h1>GHSA-p3x9-6h7p-cgfc</h1> ++ </body> ++ </html> ++ EOT; ++ // Missing content-type in actual response. ++ yield "data://text/plain,HTTP/1.1 200 OK: This is fine\r\n\r\n{$xml}"; ++} ++ ++['pid' => $pid, 'uri' => $uri] = http_server('genResponses', $output); ++$document = new \DOMDocument(); ++$document->loadHTMLFile($uri); ++ ++$h1 = $document->getElementsByTagName('h1'); ++var_dump($h1->length); ++var_dump($document->saveHTML()); ++http_server_kill($pid); ++?> ++--EXPECT-- ++int(1) ++string(266) "<!DOCTYPE html> ++<html> ++ <head> ++ <title>GHSA-p3x9-6h7p-cgfc</title> ++ ++ <meta charset="utf-8"> ++ <meta http-equiv="Content-type" content="text/html; charset=utf-8"> ++ </head> ++ ++ <body> ++ <h1>GHSA-p3x9-6h7p-cgfc</h1> ++ </body> ++</html> ++" +diff --git a/ext/libxml/libxml.c b/ext/libxml/libxml.c +index 4b9e6a918d4..1866b7b21f4 100644 +--- a/ext/libxml/libxml.c ++++ b/ext/libxml/libxml.c +@@ -420,42 +420,53 @@ php_libxml_input_buffer_create_filename(const char *URI, xmlCharEncoding enc) + if (Z_TYPE(s->wrapperdata) == IS_ARRAY) { + zval *header; + +- ZEND_HASH_FOREACH_VAL_IND(Z_ARRVAL(s->wrapperdata), header) { ++ /* Scan backwards: The header array might contain the headers for multiple responses, if ++ * a redirect was followed. ++ */ ++ ZEND_HASH_REVERSE_FOREACH_VAL_IND(Z_ARRVAL(s->wrapperdata), header) { + const char buf[] = "Content-Type:"; +- if (Z_TYPE_P(header) == IS_STRING && +- !zend_binary_strncasecmp(Z_STRVAL_P(header), Z_STRLEN_P(header), buf, sizeof(buf)-1, sizeof(buf)-1)) { +- char *needle = estrdup("charset="); +- char *haystack = estrndup(Z_STRVAL_P(header), Z_STRLEN_P(header)); +- char *encoding = php_stristr(haystack, needle, Z_STRLEN_P(header), sizeof("charset=")-1); +- +- if (encoding) { +- char *end; +- +- encoding += sizeof("charset=")-1; +- if (*encoding == '"') { +- encoding++; +- } +- end = strchr(encoding, ';'); +- if (end == NULL) { +- end = encoding + strlen(encoding); +- } +- end--; /* end == encoding-1 isn't a buffer underrun */ +- while (*end == ' ' || *end == '\t') { +- end--; +- } +- if (*end == '"') { +- end--; +- } +- if (encoding >= end) continue; +- *(end+1) = '\0'; +- enc = xmlParseCharEncoding(encoding); +- if (enc <= XML_CHAR_ENCODING_NONE) { +- enc = XML_CHAR_ENCODING_NONE; ++ if (Z_TYPE_P(header) == IS_STRING) { ++ /* If no colon is found in the header, we assume it's the HTTP status line and bail out. */ ++ char *colon = memchr(Z_STRVAL_P(header), ':', Z_STRLEN_P(header)); ++ char *space = memchr(Z_STRVAL_P(header), ' ', Z_STRLEN_P(header)); ++ if (colon == NULL || space < colon) { ++ break; ++ } ++ ++ if (!zend_binary_strncasecmp(Z_STRVAL_P(header), Z_STRLEN_P(header), buf, sizeof(buf)-1, sizeof(buf)-1)) { ++ char *needle = estrdup("charset="); ++ char *haystack = estrndup(Z_STRVAL_P(header), Z_STRLEN_P(header)); ++ char *encoding = php_stristr(haystack, needle, Z_STRLEN_P(header), sizeof("charset=")-1); ++ ++ if (encoding) { ++ char *end; ++ ++ encoding += sizeof("charset=")-1; ++ if (*encoding == '"') { ++ encoding++; ++ } ++ end = strchr(encoding, ';'); ++ if (end == NULL) { ++ end = encoding + strlen(encoding); ++ } ++ end--; /* end == encoding-1 isn't a buffer underrun */ ++ while (*end == ' ' || *end == '\t') { ++ end--; ++ } ++ if (*end == '"') { ++ end--; ++ } ++ if (encoding >= end) continue; ++ *(end+1) = '\0'; ++ enc = xmlParseCharEncoding(encoding); ++ if (enc <= XML_CHAR_ENCODING_NONE) { ++ enc = XML_CHAR_ENCODING_NONE; ++ } + } ++ efree(haystack); ++ efree(needle); ++ break; /* found content-type */ + } +- efree(haystack); +- efree(needle); +- break; /* found content-type */ + } + } ZEND_HASH_FOREACH_END(); + } +diff --git a/ext/standard/tests/http/newserver.inc b/ext/standard/tests/http/newserver.inc +new file mode 100644 +index 00000000000..5c636705e8c +--- /dev/null ++++ b/ext/standard/tests/http/newserver.inc +@@ -0,0 +1,124 @@ ++<?php declare(strict_types=1); ++ ++function http_server_skipif() { ++ ++ if (!function_exists('pcntl_fork')) die('skip pcntl_fork() not available'); ++ if (!function_exists('posix_kill')) die('skip posix_kill() not available'); ++ if (!stream_socket_server('tcp://localhost:0')) die('skip stream_socket_server() failed'); ++} ++ ++function http_server_init(&$output = null) { ++ pcntl_alarm(60); ++ ++ $server = stream_socket_server('tcp://localhost:0', $errno, $errstr); ++ if (!$server) { ++ return false; ++ } ++ ++ if ($output === null) { ++ $output = tmpfile(); ++ if ($output === false) { ++ return false; ++ } ++ } ++ ++ $pid = pcntl_fork(); ++ if ($pid == -1) { ++ die('could not fork'); ++ } else if ($pid) { ++ return [ ++ 'pid' => $pid, ++ 'uri' => 'http://' . stream_socket_get_name($server, false), ++ ]; ++ } ++ ++ return $server; ++} ++ ++/* Minimal HTTP server with predefined responses. ++ * ++ * $socket_string is the socket to create and listen on (e.g. tcp://127.0.0.1:1234) ++ * $files is an iterable of files or callable generator yielding files. ++ * containing N responses for N expected requests. Server dies after N requests. ++ * $output is a stream on which everything sent by clients is written to ++ */ ++function http_server($files, &$output = null) { ++ ++ if (!is_resource($server = http_server_init($output))) { ++ return $server; ++ } ++ ++ if (is_callable($files)) { ++ $files = $files($server); ++ } ++ ++ foreach($files as $file) { ++ ++ $sock = stream_socket_accept($server); ++ if (!$sock) { ++ exit(1); ++ } ++ ++ // read headers ++ ++ $content_length = 0; ++ ++ stream_set_blocking($sock, false); ++ while (!feof($sock)) { ++ ++ list($r, $w, $e) = array(array($sock), null, null); ++ if (!stream_select($r, $w, $e, 1)) continue; ++ ++ $line = stream_get_line($sock, 8192, "\r\n"); ++ if ($line === '') { ++ fwrite($output, "\r\n"); ++ break; ++ } else if ($line !== false) { ++ fwrite($output, "$line\r\n"); ++ ++ if (preg_match('#^Content-Length\s*:\s*([[:digit:]]+)\s*$#i', $line, $matches)) { ++ $content_length = (int) $matches[1]; ++ } ++ } ++ } ++ stream_set_blocking($sock, true); ++ ++ // read content ++ ++ if ($content_length > 0) { ++ stream_copy_to_stream($sock, $output, $content_length); ++ } ++ ++ // send response ++ ++ $fd = fopen($file, 'rb'); ++ stream_copy_to_stream($fd, $sock); ++ ++ fclose($sock); ++ } ++ ++ exit(0); ++} ++ ++function http_server_sleep($micro_seconds = 500000) ++{ ++ if (!is_resource($server = http_server_init($output))) { ++ return $server; ++ } ++ ++ $sock = stream_socket_accept($server); ++ if (!$sock) { ++ exit(1); ++ } ++ ++ usleep($micro_seconds); ++ ++ fclose($sock); ++ ++ exit(0); ++} ++ ++function http_server_kill(int $pid) { ++ posix_kill($pid, SIGTERM); ++ pcntl_waitpid($pid, $status); ++} +-- +2.48.1 + +From c5e836c5f98c6a01778595d448bb6a5b84eccec1 Mon Sep 17 00:00:00 2001 +From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> +Date: Wed, 18 Dec 2024 18:44:05 +0100 +Subject: [PATCH 09/11] Fix GHSA-wg4p-4hqh-c3g9 + +(cherry picked from commit 0e715e71d945b68f8ccedd62c5960df747af6625) +(cherry picked from commit 294140ee981fda6a38244215e4b16e53b7f5b2a6) +--- + ext/xml/tests/toffset_bounds.phpt | 42 +++++++++++++++++++++++++++++++ + ext/xml/xml.c | 12 ++++++--- + 2 files changed, 50 insertions(+), 4 deletions(-) + create mode 100644 ext/xml/tests/toffset_bounds.phpt + +diff --git a/ext/xml/tests/toffset_bounds.phpt b/ext/xml/tests/toffset_bounds.phpt +new file mode 100644 +index 00000000000..5a3fd22f86c +--- /dev/null ++++ b/ext/xml/tests/toffset_bounds.phpt +@@ -0,0 +1,42 @@ ++--TEST-- ++XML_OPTION_SKIP_TAGSTART bounds ++--EXTENSIONS-- ++xml ++--FILE-- ++<?php ++$sample = "<?xml version=\"1.0\"?><test><child/></test>"; ++$parser = xml_parser_create(); ++xml_parser_set_option($parser, XML_OPTION_SKIP_TAGSTART, 100); ++$res = xml_parse_into_struct($parser,$sample,$vals,$index); ++var_dump($vals); ++?> ++--EXPECT-- ++array(3) { ++ [0]=> ++ array(3) { ++ ["tag"]=> ++ string(0) "" ++ ["type"]=> ++ string(4) "open" ++ ["level"]=> ++ int(1) ++ } ++ [1]=> ++ array(3) { ++ ["tag"]=> ++ string(0) "" ++ ["type"]=> ++ string(8) "complete" ++ ["level"]=> ++ int(2) ++ } ++ [2]=> ++ array(3) { ++ ["tag"]=> ++ string(0) "" ++ ["type"]=> ++ string(5) "close" ++ ["level"]=> ++ int(1) ++ } ++} +diff --git a/ext/xml/xml.c b/ext/xml/xml.c +index 6fe6151c7a1..b56bf79f55d 100644 +--- a/ext/xml/xml.c ++++ b/ext/xml/xml.c +@@ -773,9 +773,11 @@ void _xml_startElementHandler(void *userData, const XML_Char *name, const XML_Ch + array_init(&tag); + array_init(&atr); + +- _xml_add_to_info(parser, ZSTR_VAL(tag_name) + parser->toffset); ++ char *skipped_tag_name = SKIP_TAGSTART(ZSTR_VAL(tag_name)); + +- add_assoc_string(&tag, "tag", SKIP_TAGSTART(ZSTR_VAL(tag_name))); /* cast to avoid gcc-warning */ ++ _xml_add_to_info(parser, skipped_tag_name); ++ ++ add_assoc_string(&tag, "tag", skipped_tag_name); + add_assoc_string(&tag, "type", "open"); + add_assoc_long(&tag, "level", parser->level); + +@@ -842,9 +844,11 @@ void _xml_endElementHandler(void *userData, const XML_Char *name) + } else { + array_init(&tag); + +- _xml_add_to_info(parser, ZSTR_VAL(tag_name) + parser->toffset); ++ char *skipped_tag_name = SKIP_TAGSTART(ZSTR_VAL(tag_name)); ++ ++ _xml_add_to_info(parser, skipped_tag_name); + +- add_assoc_string(&tag, "tag", SKIP_TAGSTART(ZSTR_VAL(tag_name))); /* cast to avoid gcc-warning */ ++ add_assoc_string(&tag, "tag", skipped_tag_name); + add_assoc_string(&tag, "type", "close"); + add_assoc_long(&tag, "level", parser->level); + +-- +2.48.1 + +From 3faf7b2017ccd1e7347c30cf64cddcb684300cba Mon Sep 17 00:00:00 2001 +From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> +Date: Fri, 17 Nov 2023 19:45:40 +0100 +Subject: [PATCH 10/11] Fix GH-12702: libxml2 2.12.0 issue building from src + +Fixes GH-12702. + +Co-authored-by: nono303 <github@nono303.net> +(cherry picked from commit 6a76e5d0a2dcf46b4ab74cc3ffcbfeb860c4fdb3) +(cherry picked from commit d7ab2bb9856d938fca7989575695c14c25892589) +--- + ext/dom/document.c | 1 + + ext/libxml/php_libxml.h | 1 + + 2 files changed, 2 insertions(+) + +diff --git a/ext/dom/document.c b/ext/dom/document.c +index af06fb41240..f8071774b92 100644 +--- a/ext/dom/document.c ++++ b/ext/dom/document.c +@@ -25,6 +25,7 @@ + #if HAVE_LIBXML && HAVE_DOM + #include "php_dom.h" + #include <libxml/SAX.h> ++#include <libxml/xmlsave.h> + #ifdef LIBXML_SCHEMAS_ENABLED + #include <libxml/relaxng.h> + #include <libxml/xmlschemas.h> +diff --git a/ext/libxml/php_libxml.h b/ext/libxml/php_libxml.h +index 92028d5703e..6f3295b5241 100644 +--- a/ext/libxml/php_libxml.h ++++ b/ext/libxml/php_libxml.h +@@ -37,6 +37,7 @@ extern zend_module_entry libxml_module_entry; + + #include "zend_smart_str.h" + #include <libxml/tree.h> ++#include <libxml/parser.h> + + #define LIBXML_SAVE_NOEMPTYTAG 1<<2 + +-- +2.48.1 + +From 8ab957ca87b42b808aec7fd472fbc4063073a119 Mon Sep 17 00:00:00 2001 +From: Remi Collet <remi@remirepo.net> +Date: Thu, 13 Mar 2025 09:39:19 +0100 +Subject: [PATCH 11/11] NEWS + +(cherry picked from commit adae2b8de8963ac6f92103803bf91a5174172f88) +--- + NEWS | 17 +++++++++++++++++ + 1 file changed, 17 insertions(+) + +diff --git a/NEWS b/NEWS +index 09cf2cfa0bb..fda646c7010 100644 +--- a/NEWS ++++ b/NEWS +@@ -1,6 +1,23 @@ + PHP NEWS + ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| + ++Backported from 8.1.32 ++ ++- LibXML: ++ . Fixed GHSA-wg4p-4hqh-c3g9 (Reocurrence of #72714). (nielsdos) ++ . Fixed GHSA-p3x9-6h7p-cgfc (libxml streams use wrong `content-type` header ++ when requesting a redirected resource). (CVE-2025-1219) (timwolla) ++ ++- Streams: ++ . Fixed GHSA-hgf54-96fm-v528 (Stream HTTP wrapper header check might omit ++ basic auth header). (CVE-2025-1736) (Jakub Zelenka) ++ . Fixed GHSA-52jp-hrpf-2jff (Stream HTTP wrapper truncate redirect location ++ to 1024 bytes). (CVE-2025-1861) (Jakub Zelenka) ++ . Fixed GHSA-pcmh-g36c-qc44 (Streams HTTP wrapper does not fail for headers ++ without colon). (CVE-2025-1734) (Jakub Zelenka) ++ . Fixed GHSA-v8xr-gpvj-cx9g (Header parser of `http` stream wrapper does not ++ handle folded headers). (CVE-2025-1217) (Jakub Zelenka) ++ + Backported from 8.1.31 + + - CLI: +-- +2.48.1 + diff --git a/php-cve-2025-1220.patch b/php-cve-2025-1220.patch new file mode 100644 index 0000000..25f3b61 --- /dev/null +++ b/php-cve-2025-1220.patch @@ -0,0 +1,154 @@ +From d407d8a8735ebf43bee3e6b49fb013b8aa4b6bfc Mon Sep 17 00:00:00 2001 +From: Jakub Zelenka <bukka@php.net> +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) +(cherry picked from commit 36150278addd8686a9899559241296094bd57282) +--- + 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 fe8fbea85ca..df6a74b078f 100644 +--- a/ext/standard/fsock.c ++++ b/ext/standard/fsock.c +@@ -25,6 +25,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) +@@ -59,11 +81,12 @@ static void php_fsockopen_stream(INTERNAL_FUNCTION_PARAMETERS, int persistent) + ZEND_PARSE_PARAMETERS_END_EX(RETURN_FALSE); + + 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..e16d3fa9060 +--- /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-- ++<?php ++ ++$server = stream_socket_server("tcp://localhost:0"); ++ ++if (preg_match('/:(\d+)$/', stream_socket_get_name($server, false), $m)) { ++ $client = fsockopen("localhost\0.example.com", intval($m[1])); ++ var_dump($client); ++ if ($client) { ++ fclose($client); ++ } ++} ++fclose($server); ++ ++?> ++--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..bc1f34eaf58 +--- /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-- ++<?php ++ ++$server = stream_socket_server("tcp://localhost:0"); ++$socket_name = stream_socket_get_name($server, false); ++ ++if (preg_match('/:(\d+)$/', $socket_name, $m)) { ++ $port = $m[1]; ++ $client = stream_socket_client("tcp://localhost\0.example.com:$port"); ++ var_dump($client); ++ if ($client) { ++ fclose($client); ++ } ++} else { ++ echo "Could not extract port from socket name: $socket_name\n"; ++} ++ ++fclose($server); ++ ++?> ++--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 46b23b63ada..7a192ea6c0b 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-1734.patch b/php-cve-2025-1734.patch new file mode 100644 index 0000000..6c9aa52 --- /dev/null +++ b/php-cve-2025-1734.patch @@ -0,0 +1,314 @@ +From 0b965cf85f512b1a7b87f100ac77e4aa13f7f421 Mon Sep 17 00:00:00 2001 +From: Jakub Zelenka <bukka@php.net> +Date: Sun, 19 Jan 2025 17:49:53 +0100 +Subject: [PATCH 02/11] Fix GHSA-pcmh-g36c-qc44: http headers without colon +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +The header line must contain colon otherwise it is invalid and it needs +to fail. + +Reviewed-by: Tim Düsterhus <tim@tideways-gmbh.com> +(cherry picked from commit 0548c4c1756724a89ef8310709419b08aadb2b3b) +(cherry picked from commit e81d0cd14bfeb17e899c73e3aece4991bbda76af) +--- + ext/standard/http_fopen_wrapper.c | 51 ++++++++++++++----- + ext/standard/tests/http/bug47021.phpt | 26 ++++++---- + ext/standard/tests/http/bug75535.phpt | 4 +- + .../tests/http/ghsa-pcmh-g36c-qc44-001.phpt | 51 +++++++++++++++++++ + .../tests/http/ghsa-pcmh-g36c-qc44-002.phpt | 51 +++++++++++++++++++ + 5 files changed, 156 insertions(+), 27 deletions(-) + create mode 100644 ext/standard/tests/http/ghsa-pcmh-g36c-qc44-001.phpt + create mode 100644 ext/standard/tests/http/ghsa-pcmh-g36c-qc44-002.phpt + +diff --git a/ext/standard/http_fopen_wrapper.c b/ext/standard/http_fopen_wrapper.c +index 08386cfafcd..071d6a4d119 100644 +--- a/ext/standard/http_fopen_wrapper.c ++++ b/ext/standard/http_fopen_wrapper.c +@@ -119,6 +119,7 @@ static zend_bool check_has_header(const char *headers, const char *header) { + typedef struct _php_stream_http_response_header_info { + php_stream_filter *transfer_encoding; + size_t file_size; ++ zend_bool error; + zend_bool follow_location; + char location[HTTP_HEADER_BLOCK_SIZE]; + } php_stream_http_response_header_info; +@@ -128,6 +129,7 @@ static void php_stream_http_response_header_info_init( + { + header_info->transfer_encoding = NULL; + header_info->file_size = 0; ++ header_info->error = 0; + header_info->follow_location = 1; + header_info->location[0] = '\0'; + } +@@ -165,10 +167,11 @@ static zend_bool php_stream_http_response_header_trim(char *http_header_line, + /* Process folding headers of the current line and if there are none, parse last full response + * header line. It returns NULL if the last header is finished, otherwise it returns updated + * last header line. */ +-static zend_string *php_stream_http_response_headers_parse(php_stream *stream, +- php_stream_context *context, int options, zend_string *last_header_line_str, +- char *header_line, size_t *header_line_length, int response_code, +- zval *response_header, php_stream_http_response_header_info *header_info) ++static zend_string *php_stream_http_response_headers_parse(php_stream_wrapper *wrapper, ++ php_stream *stream, php_stream_context *context, int options, ++ zend_string *last_header_line_str, char *header_line, size_t *header_line_length, ++ int response_code, zval *response_header, ++ php_stream_http_response_header_info *header_info) + { + char *last_header_line = ZSTR_VAL(last_header_line_str); + size_t last_header_line_length = ZSTR_LEN(last_header_line_str); +@@ -211,6 +214,19 @@ static zend_string *php_stream_http_response_headers_parse(php_stream *stream, + /* Find header separator position. */ + char *last_header_value = memchr(last_header_line, ':', last_header_line_length); + if (last_header_value) { ++ /* Verify there is no space in header name */ ++ char *last_header_name = last_header_line + 1; ++ while (last_header_name < last_header_value) { ++ if (*last_header_name == ' ' || *last_header_name == '\t') { ++ header_info->error = 1; ++ php_stream_wrapper_log_error(wrapper, options, ++ "HTTP invalid response format (space in header name)!"); ++ zend_string_efree(last_header_line_str); ++ return NULL; ++ } ++ ++last_header_name; ++ } ++ + last_header_value++; /* Skip ':'. */ + + /* Strip leading whitespace. */ +@@ -219,9 +235,12 @@ static zend_string *php_stream_http_response_headers_parse(php_stream *stream, + last_header_value++; + } + } else { +- /* There is no colon. Set the value to the end of the header line, which is effectively +- * an empty string. */ +- last_header_value = last_header_line_end; ++ /* There is no colon which means invalid response so error. */ ++ header_info->error = 1; ++ php_stream_wrapper_log_error(wrapper, options, ++ "HTTP invalid response format (no colon in header line)!"); ++ zend_string_efree(last_header_line_str); ++ return NULL; + } + + zend_bool store_header = 1; +@@ -927,10 +946,16 @@ finish: + + if (last_header_line_str != NULL) { + /* Parse last header line. */ +- last_header_line_str = php_stream_http_response_headers_parse(stream, context, +- options, last_header_line_str, http_header_line, &http_header_line_length, +- response_code, response_header, &header_info); +- if (last_header_line_str != NULL) { ++ last_header_line_str = php_stream_http_response_headers_parse(wrapper, stream, ++ context, options, last_header_line_str, http_header_line, ++ &http_header_line_length, response_code, response_header, &header_info); ++ if (EXPECTED(last_header_line_str == NULL)) { ++ if (UNEXPECTED(header_info.error)) { ++ php_stream_close(stream); ++ stream = NULL; ++ goto out; ++ } ++ } else { + /* Folding header present so continue. */ + continue; + } +@@ -960,8 +985,8 @@ finish: + + /* If the stream was closed early, we still want to process the last line to keep BC. */ + if (last_header_line_str != NULL) { +- php_stream_http_response_headers_parse(stream, context, options, last_header_line_str, +- NULL, NULL, response_code, response_header, &header_info); ++ php_stream_http_response_headers_parse(wrapper, stream, context, options, ++ last_header_line_str, NULL, NULL, response_code, response_header, &header_info); + } + + if (!reqok || (header_info.location[0] != '\0' && header_info.follow_location)) { +diff --git a/ext/standard/tests/http/bug47021.phpt b/ext/standard/tests/http/bug47021.phpt +index f3db3e1be23..a287b714c62 100644 +--- a/ext/standard/tests/http/bug47021.phpt ++++ b/ext/standard/tests/http/bug47021.phpt +@@ -47,9 +47,9 @@ function do_test($num_spaces, $leave_trailing_space=false) { + ]; + $pid = http_server('tcp://127.0.0.1:12342', $responses); + +- echo file_get_contents('http://127.0.0.1:12342/', false, $ctx); ++ echo file_get_contents('http://127.0.0.1:12342', false, $ctx); + echo "\n"; +- echo file_get_contents('http://127.0.0.1:12342/', false, $ctx); ++ echo file_get_contents('http://127.0.0.1:12342', false, $ctx); + echo "\n"; + + http_server_kill($pid); +@@ -70,23 +70,27 @@ do_test(1, true); + echo "\n"; + + ?> +---EXPECT-- ++--EXPECTF-- ++ + Type='text/plain' + Hello +-Size=5 +-World ++ ++Warning: file_get_contents(http://%s:%d): failed to open stream: HTTP invalid response format (no colon in header line)! in %s ++ + + Type='text/plain' + Hello +-Size=5 +-World ++ ++Warning: file_get_contents(http://%s:%d): failed to open stream: HTTP invalid response format (no colon in header line)! in %s ++ + + Type='text/plain' + Hello +-Size=5 +-World ++ ++Warning: file_get_contents(http://%s:%d): failed to open stream: HTTP invalid response format (no colon in header line)! in %s ++ + + Type='text/plain' + Hello +-Size=5 +-World ++ ++Warning: file_get_contents(http://%s:%d): failed to open stream: HTTP invalid response format (no colon in header line)! in %s +diff --git a/ext/standard/tests/http/bug75535.phpt b/ext/standard/tests/http/bug75535.phpt +index 9bf298cc065..e3757ba4f1d 100644 +--- a/ext/standard/tests/http/bug75535.phpt ++++ b/ext/standard/tests/http/bug75535.phpt +@@ -22,10 +22,8 @@ http_server_kill($pid); + ==DONE== + --EXPECT-- + string(0) "" +-array(2) { ++array(1) { + [0]=> + string(15) "HTTP/1.0 200 Ok" +- [1]=> +- string(14) "Content-Length" + } + ==DONE== +diff --git a/ext/standard/tests/http/ghsa-pcmh-g36c-qc44-001.phpt b/ext/standard/tests/http/ghsa-pcmh-g36c-qc44-001.phpt +new file mode 100644 +index 00000000000..53baa1c92d6 +--- /dev/null ++++ b/ext/standard/tests/http/ghsa-pcmh-g36c-qc44-001.phpt +@@ -0,0 +1,51 @@ ++--TEST-- ++GHSA-pcmh-g36c-qc44: Header parser of http stream wrapper does not verify header name and colon (colon) ++--FILE-- ++<?php ++$serverCode = <<<'CODE' ++ $ctxt = stream_context_create([ ++ "socket" => [ ++ "tcp_nodelay" => true ++ ] ++ ]); ++ ++ $server = stream_socket_server( ++ "tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt); ++ phpt_notify_server_start($server); ++ ++ $conn = stream_socket_accept($server); ++ ++ phpt_notify(WORKER_DEFAULT_NAME, "server-accepted"); ++ ++ fwrite($conn, "HTTP/1.0 200 Ok\r\nContent-Type: text/html\r\nWrong-Header\r\nGood-Header: test\r\n\r\nbody\r\n"); ++CODE; ++ ++$clientCode = <<<'CODE' ++ function stream_notification_callback($notification_code, $severity, $message, $message_code, $bytes_transferred, $bytes_max) { ++ switch($notification_code) { ++ case STREAM_NOTIFY_MIME_TYPE_IS: ++ echo "Found the mime-type: ", $message, PHP_EOL; ++ break; ++ } ++ } ++ ++ $ctx = stream_context_create(); ++ stream_context_set_params($ctx, array("notification" => "stream_notification_callback")); ++ var_dump(file_get_contents("http://{{ ADDR }}", false, $ctx)); ++ var_dump($http_response_header); ++CODE; ++ ++include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++ServerClientTestCase::getInstance()->run($clientCode, $serverCode); ++?> ++--EXPECTF-- ++Found the mime-type: text/html ++ ++Warning: file_get_contents(http://127.0.0.1:%d): failed to open stream: HTTP invalid response format (no colon in header line)! in %s ++bool(false) ++array(2) { ++ [0]=> ++ string(15) "HTTP/1.0 200 Ok" ++ [1]=> ++ string(23) "Content-Type: text/html" ++} +diff --git a/ext/standard/tests/http/ghsa-pcmh-g36c-qc44-002.phpt b/ext/standard/tests/http/ghsa-pcmh-g36c-qc44-002.phpt +new file mode 100644 +index 00000000000..5aa0ee00618 +--- /dev/null ++++ b/ext/standard/tests/http/ghsa-pcmh-g36c-qc44-002.phpt +@@ -0,0 +1,51 @@ ++--TEST-- ++GHSA-pcmh-g36c-qc44: Header parser of http stream wrapper does not verify header name and colon (name) ++--FILE-- ++<?php ++$serverCode = <<<'CODE' ++ $ctxt = stream_context_create([ ++ "socket" => [ ++ "tcp_nodelay" => true ++ ] ++ ]); ++ ++ $server = stream_socket_server( ++ "tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt); ++ phpt_notify_server_start($server); ++ ++ $conn = stream_socket_accept($server); ++ ++ phpt_notify(WORKER_DEFAULT_NAME, "server-accepted"); ++ ++ fwrite($conn, "HTTP/1.0 200 Ok\r\nContent-Type: text/html\r\nWrong-Header : test\r\nGood-Header: test\r\n\r\nbody\r\n"); ++CODE; ++ ++$clientCode = <<<'CODE' ++ function stream_notification_callback($notification_code, $severity, $message, $message_code, $bytes_transferred, $bytes_max) { ++ switch($notification_code) { ++ case STREAM_NOTIFY_MIME_TYPE_IS: ++ echo "Found the mime-type: ", $message, PHP_EOL; ++ break; ++ } ++ } ++ ++ $ctx = stream_context_create(); ++ stream_context_set_params($ctx, array("notification" => "stream_notification_callback")); ++ var_dump(file_get_contents("http://{{ ADDR }}", false, $ctx)); ++ var_dump($http_response_header); ++CODE; ++ ++include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++ServerClientTestCase::getInstance()->run($clientCode, $serverCode); ++?> ++--EXPECTF-- ++Found the mime-type: text/html ++ ++Warning: file_get_contents(http://127.0.0.1:%d): failed to open stream: HTTP invalid response format (space in header name)! in %s ++bool(false) ++array(2) { ++ [0]=> ++ string(15) "HTTP/1.0 200 Ok" ++ [1]=> ++ string(23) "Content-Type: text/html" ++} +-- +2.48.1 + diff --git a/php-cve-2025-1735.patch b/php-cve-2025-1735.patch new file mode 100644 index 0000000..e144cc4 --- /dev/null +++ b/php-cve-2025-1735.patch @@ -0,0 +1,492 @@ +From df2ecf34256c4a301e8959fe2eed0323f8b1b57a Mon Sep 17 00:00:00 2001 +From: Jakub Zelenka <bukka@php.net> +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) +(cherry picked from commit 7633d987cc11ee2601223e73cfdb8b31fed5980f) +--- + 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 e578bbc2720..021471cefc0 100644 +--- a/ext/pdo_pgsql/pgsql_driver.c ++++ b/ext/pdo_pgsql/pgsql_driver.c +@@ -323,11 +323,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); +@@ -339,7 +343,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-- ++<?php ++if (!extension_loaded('pdo') || !extension_loaded('pdo_pgsql')) die('skip not loaded'); ++require_once dirname(__FILE__) . '/../../../ext/pdo/tests/pdo_test.inc'; ++require_once dirname(__FILE__) . '/config.inc'; ++PDOTest::skip(); ++?> ++--FILE-- ++<?php ++require_once dirname(__FILE__) . '/../../../ext/pdo/tests/pdo_test.inc'; ++require_once dirname(__FILE__) . '/config.inc'; ++$db = PDOTest::test_factory(dirname(__FILE__) . '/common.phpt'); ++$db->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 7dcd56cf144..9e06497125c 100644 +--- a/ext/pgsql/pgsql.c ++++ b/ext/pgsql/pgsql.c +@@ -4393,10 +4393,16 @@ PHP_FUNCTION(pg_escape_string) + to = zend_string_safe_alloc(ZSTR_LEN(from), 2, 0, 0); + #ifdef HAVE_PQESCAPE_CONN + if (link) { ++ int err; + if ((pgsql = (PGconn *)zend_fetch_resource2(link, "PostgreSQL link", le_link, le_plink)) == NULL) { + RETURN_FALSE; + } +- 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_throw_exception(zend_ce_exception, "Escaping string failed", 0); ++ zend_string_efree(to); ++ return; ++ } + } else + #endif + { +@@ -4445,6 +4451,10 @@ PHP_FUNCTION(pg_escape_bytea) + } else + #endif + to = (char *)PQescapeBytea((unsigned char*)from, from_len, &to_len); ++ if (to == NULL) { ++ zend_throw_exception(zend_ce_exception, "Escape failure", 0); ++ return; ++ } + + RETVAL_STRINGL(to, to_len-1); /* to_len includes additional '\0' */ + PQfreemem(to); +@@ -5529,7 +5539,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; + + if (!*table_name) { +@@ -5570,7 +5580,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); + } +@@ -5578,7 +5595,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); + } +@@ -5850,7 +5874,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; + + assert(pg_link != NULL); +@@ -6101,10 +6125,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; + +@@ -6126,7 +6154,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; + +@@ -6406,6 +6442,11 @@ PHP_PGSQL_API int php_pgsql_convert(PGconn *pg_link, const char *table_name, con + #else + tmp = PQescapeBytea(Z_STRVAL_P(val), (unsigned char *)Z_STRLEN_P(val), &to_len); + #endif ++ 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); +@@ -6488,6 +6529,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 = PGSQLescapeIdentifier(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); + PGSQLfree(escaped); + } +@@ -6566,7 +6613,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 int build_tablename(smart_str *querystr, PGconn *pg_link, const char *table) /* {{{ */ + { + size_t table_len = strlen(table); + +@@ -6577,6 +6624,10 @@ static inline void build_tablename(smart_str *querystr, PGconn *pg_link, const c + smart_str_appendl(querystr, table, len); + } else { + char *escaped = PGSQLescapeIdentifier(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); + PGSQLfree(escaped); + } +@@ -6589,11 +6640,17 @@ static inline void build_tablename(smart_str *querystr, PGconn *pg_link, const c + smart_str_appendl(querystr, after_dot, len); + } else { + char *escaped = PGSQLescapeIdentifier(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); + PGSQLfree(escaped); + } + } ++ ++ return SUCCESS; + } + /* }}} */ + +@@ -6615,7 +6672,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; +@@ -6631,7 +6690,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) { +@@ -6641,6 +6702,10 @@ PHP_PGSQL_API int php_pgsql_insert(PGconn *pg_link, const char *table, zval *var + } + if (opt & PGSQL_DML_ESCAPE) { + tmp = PGSQLescapeIdentifier(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); + PGSQLfree(tmp); + } else { +@@ -6652,15 +6717,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, '\''); +@@ -6810,6 +6879,10 @@ static inline int build_assignment_string(PGconn *pg_link, smart_str *querystr, + } + if (opt & PGSQL_DML_ESCAPE) { + char *tmp = PGSQLescapeIdentifier(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); + PGSQLfree(tmp); + } else { +@@ -6824,8 +6897,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, '\''); +@@ -6894,7 +6973,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)) +@@ -6992,7 +7073,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)) +@@ -7130,7 +7213,9 @@ PHP_PGSQL_API int php_pgsql_result2array(PGresult *pg_result, zval *ret_array, l + } + + 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-- ++<?php include("skipif.inc"); ?> ++--FILE-- ++<?php ++ ++include 'config.inc'; ++define('FILE_NAME', __DIR__ . '/php.gif'); ++ ++$db = pg_connect($conn_str); ++pg_query($db, "DROP TABLE IF EXISTS ghsa_hrmw_9436_5mv3"); ++pg_query($db, "CREATE TABLE ghsa_hrmw_9436_5mv3 (bar text);"); ++ ++// pg_escape_literal/pg_escape_identifier ++ ++$invalid = "ABC\xff\x30';"; ++$flags = PGSQL_DML_NO_CONV | PGSQL_DML_ESCAPE; ++ ++var_dump(pg_insert($db, $invalid, ['bar' => '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 d52bcc1e66edd421dfea1698b1f897ad26c5f15f Mon Sep 17 00:00:00 2001 +From: Remi Collet <remi@remirepo.net> +Date: Thu, 3 Jul 2025 09:32:25 +0200 +Subject: [PATCH 4/4] NEWS + +(cherry picked from commit 970548b94b7f23be32154d05a9545b10c98bfd62) +--- + NEWS | 14 ++++++++++++++ + 1 file changed, 14 insertions(+) + +diff --git a/NEWS b/NEWS +index fda646c7010..a9dd716c003 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-1736.patch b/php-cve-2025-1736.patch new file mode 100644 index 0000000..eb33553 --- /dev/null +++ b/php-cve-2025-1736.patch @@ -0,0 +1,242 @@ +From 134f821622e2d2b68d66bea16e16c05b7b0f5114 Mon Sep 17 00:00:00 2001 +From: Jakub Zelenka <bukka@php.net> +Date: Fri, 14 Feb 2025 19:17:22 +0100 +Subject: [PATCH 04/11] Fix GHSA-hgf5-96fm-v528: http user header check of crlf + +(cherry picked from commit 41d49abbd99dab06cdae4834db664435f8177174) +(cherry picked from commit 8f65ef50929f6781f4973325f9b619f02cce19d8) +--- + ext/standard/http_fopen_wrapper.c | 2 +- + .../tests/http/ghsa-hgf5-96fm-v528-001.phpt | 65 +++++++++++++++++++ + .../tests/http/ghsa-hgf5-96fm-v528-002.phpt | 62 ++++++++++++++++++ + .../tests/http/ghsa-hgf5-96fm-v528-003.phpt | 64 ++++++++++++++++++ + 4 files changed, 192 insertions(+), 1 deletion(-) + create mode 100644 ext/standard/tests/http/ghsa-hgf5-96fm-v528-001.phpt + create mode 100644 ext/standard/tests/http/ghsa-hgf5-96fm-v528-002.phpt + create mode 100644 ext/standard/tests/http/ghsa-hgf5-96fm-v528-003.phpt + +diff --git a/ext/standard/http_fopen_wrapper.c b/ext/standard/http_fopen_wrapper.c +index b64a7c95446..46f7c7ebcee 100644 +--- a/ext/standard/http_fopen_wrapper.c ++++ b/ext/standard/http_fopen_wrapper.c +@@ -109,7 +109,7 @@ static inline void strip_header(char *header_bag, char *lc_header_bag, + static zend_bool check_has_header(const char *headers, const char *header) { + const char *s = headers; + while ((s = strstr(s, header))) { +- if (s == headers || *(s-1) == '\n') { ++ if (s == headers || (*(s-1) == '\n' && *(s-2) == '\r')) { + return 1; + } + s++; +diff --git a/ext/standard/tests/http/ghsa-hgf5-96fm-v528-001.phpt b/ext/standard/tests/http/ghsa-hgf5-96fm-v528-001.phpt +new file mode 100644 +index 00000000000..c8dcd47a4a4 +--- /dev/null ++++ b/ext/standard/tests/http/ghsa-hgf5-96fm-v528-001.phpt +@@ -0,0 +1,65 @@ ++--TEST-- ++GHSA-hgf5-96fm-v528: Stream HTTP wrapper header check might omit basic auth header (incorrect inside pos) ++--FILE-- ++<?php ++$serverCode = <<<'CODE' ++ $ctxt = stream_context_create([ ++ "socket" => [ ++ "tcp_nodelay" => true ++ ] ++ ]); ++ ++ $server = stream_socket_server( ++ "tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt); ++ phpt_notify_server_start($server); ++ ++ $conn = stream_socket_accept($server); ++ ++ phpt_notify(WORKER_DEFAULT_NAME, "server-accepted"); ++ ++ $result = fread($conn, 1024); ++ $encoded_result = base64_encode($result); ++ ++ fwrite($conn, "HTTP/1.0 200 Ok\r\nContent-Type: text/html; charset=utf-8\r\n\r\n$encoded_result\r\n"); ++ ++CODE; ++ ++$clientCode = <<<'CODE' ++ $opts = [ ++ "http" => [ ++ "method" => "GET", ++ "header" => "Cookie: foo=bar\nauthorization:x\r\n" ++ ] ++ ]; ++ $ctx = stream_context_create($opts); ++ var_dump(explode("\r\n", base64_decode(file_get_contents("http://user:pwd@{{ ADDR }}", false, $ctx)))); ++ var_dump($http_response_header); ++CODE; ++ ++include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++ServerClientTestCase::getInstance()->run($clientCode, $serverCode); ++?> ++--EXPECTF-- ++array(7) { ++ [0]=> ++ string(14) "GET / HTTP/1.%d" ++ [1]=> ++ string(33) "Authorization: Basic dXNlcjpwd2Q=" ++ [2]=> ++ string(21) "Host: 127.0.0.1:%d" ++ [3]=> ++ string(17) "Connection: close" ++ [4]=> ++ string(31) "Cookie: foo=bar ++authorization:x" ++ [5]=> ++ string(0) "" ++ [6]=> ++ string(0) "" ++} ++array(2) { ++ [0]=> ++ string(15) "HTTP/1.0 200 Ok" ++ [1]=> ++ string(38) "Content-Type: text/html; charset=utf-8" ++} +diff --git a/ext/standard/tests/http/ghsa-hgf5-96fm-v528-002.phpt b/ext/standard/tests/http/ghsa-hgf5-96fm-v528-002.phpt +new file mode 100644 +index 00000000000..ca8f75f0327 +--- /dev/null ++++ b/ext/standard/tests/http/ghsa-hgf5-96fm-v528-002.phpt +@@ -0,0 +1,62 @@ ++--TEST-- ++GHSA-hgf5-96fm-v528: Header parser of http stream wrapper does not handle folded headers (correct start pos) ++--FILE-- ++<?php ++$serverCode = <<<'CODE' ++ $ctxt = stream_context_create([ ++ "socket" => [ ++ "tcp_nodelay" => true ++ ] ++ ]); ++ ++ $server = stream_socket_server( ++ "tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt); ++ phpt_notify_server_start($server); ++ ++ $conn = stream_socket_accept($server); ++ ++ phpt_notify(WORKER_DEFAULT_NAME, "server-accepted"); ++ ++ $result = fread($conn, 1024); ++ $encoded_result = base64_encode($result); ++ ++ fwrite($conn, "HTTP/1.0 200 Ok\r\nContent-Type: text/html; charset=utf-8\r\n\r\n$encoded_result\r\n"); ++ ++CODE; ++ ++$clientCode = <<<'CODE' ++ $opts = [ ++ "http" => [ ++ "method" => "GET", ++ "header" => "Authorization: Bearer x\r\n" ++ ] ++ ]; ++ $ctx = stream_context_create($opts); ++ var_dump(explode("\r\n", base64_decode(file_get_contents("http://user:pwd@{{ ADDR }}", false, $ctx)))); ++ var_dump($http_response_header); ++CODE; ++ ++include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++ServerClientTestCase::getInstance()->run($clientCode, $serverCode); ++?> ++--EXPECTF-- ++array(6) { ++ [0]=> ++ string(14) "GET / HTTP/1.%d" ++ [1]=> ++ string(21) "Host: 127.0.0.1:%d" ++ [2]=> ++ string(17) "Connection: close" ++ [3]=> ++ string(23) "Authorization: Bearer x" ++ [4]=> ++ string(0) "" ++ [5]=> ++ string(0) "" ++} ++array(2) { ++ [0]=> ++ string(15) "HTTP/1.0 200 Ok" ++ [1]=> ++ string(38) "Content-Type: text/html; charset=utf-8" ++} +diff --git a/ext/standard/tests/http/ghsa-hgf5-96fm-v528-003.phpt b/ext/standard/tests/http/ghsa-hgf5-96fm-v528-003.phpt +new file mode 100644 +index 00000000000..4cfbc7ee804 +--- /dev/null ++++ b/ext/standard/tests/http/ghsa-hgf5-96fm-v528-003.phpt +@@ -0,0 +1,64 @@ ++--TEST-- ++GHSA-hgf5-96fm-v528: Header parser of http stream wrapper does not handle folded headers (correct middle pos) ++--FILE-- ++<?php ++$serverCode = <<<'CODE' ++ $ctxt = stream_context_create([ ++ "socket" => [ ++ "tcp_nodelay" => true ++ ] ++ ]); ++ ++ $server = stream_socket_server( ++ "tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt); ++ phpt_notify_server_start($server); ++ ++ $conn = stream_socket_accept($server); ++ ++ phpt_notify(WORKER_DEFAULT_NAME, "server-accepted"); ++ ++ $result = fread($conn, 1024); ++ $encoded_result = base64_encode($result); ++ ++ fwrite($conn, "HTTP/1.0 200 Ok\r\nContent-Type: text/html; charset=utf-8\r\n\r\n$encoded_result\r\n"); ++ ++CODE; ++ ++$clientCode = <<<'CODE' ++ $opts = [ ++ "http" => [ ++ "method" => "GET", ++ "header" => "Cookie: x=y\r\nAuthorization: Bearer x\r\n" ++ ] ++ ]; ++ $ctx = stream_context_create($opts); ++ var_dump(explode("\r\n", base64_decode(file_get_contents("http://user:pwd@{{ ADDR }}", false, $ctx)))); ++ var_dump($http_response_header); ++CODE; ++ ++include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++ServerClientTestCase::getInstance()->run($clientCode, $serverCode); ++?> ++--EXPECTF-- ++array(7) { ++ [0]=> ++ string(14) "GET / HTTP/1.%d" ++ [1]=> ++ string(21) "Host: 127.0.0.1:%d" ++ [2]=> ++ string(17) "Connection: close" ++ [3]=> ++ string(11) "Cookie: x=y" ++ [4]=> ++ string(23) "Authorization: Bearer x" ++ [5]=> ++ string(0) "" ++ [6]=> ++ string(0) "" ++} ++array(2) { ++ [0]=> ++ string(15) "HTTP/1.0 200 Ok" ++ [1]=> ++ string(38) "Content-Type: text/html; charset=utf-8" ++} +-- +2.48.1 + diff --git a/php-cve-2025-1861.patch b/php-cve-2025-1861.patch new file mode 100644 index 0000000..b31e74f --- /dev/null +++ b/php-cve-2025-1861.patch @@ -0,0 +1,349 @@ +From 5418040dcaaca46965ed6f8a4ad1541709c32e9f Mon Sep 17 00:00:00 2001 +From: Jakub Zelenka <bukka@php.net> +Date: Tue, 4 Mar 2025 09:01:34 +0100 +Subject: [PATCH 03/11] Fix GHSA-52jp-hrpf-2jff: http redirect location + truncation + +It converts the allocation of location to be on heap instead of stack +and errors if the location length is greater than 8086 bytes. + +(cherry picked from commit ac1a054bb3eb5994a199e8b18cca28cbabf5943e) +(cherry picked from commit adc7e9f20c9a9aab9cd23ca47ec3fb96287898ae) +--- + ext/standard/http_fopen_wrapper.c | 87 ++++++++++++------- + .../tests/http/ghsa-52jp-hrpf-2jff-001.phpt | 58 +++++++++++++ + .../tests/http/ghsa-52jp-hrpf-2jff-002.phpt | 55 ++++++++++++ + 3 files changed, 168 insertions(+), 32 deletions(-) + create mode 100644 ext/standard/tests/http/ghsa-52jp-hrpf-2jff-001.phpt + create mode 100644 ext/standard/tests/http/ghsa-52jp-hrpf-2jff-002.phpt + +diff --git a/ext/standard/http_fopen_wrapper.c b/ext/standard/http_fopen_wrapper.c +index 071d6a4d119..b64a7c95446 100644 +--- a/ext/standard/http_fopen_wrapper.c ++++ b/ext/standard/http_fopen_wrapper.c +@@ -69,15 +69,16 @@ + + #include "php_fopen_wrappers.h" + +-#define HTTP_HEADER_BLOCK_SIZE 1024 +-#define PHP_URL_REDIRECT_MAX 20 +-#define HTTP_HEADER_USER_AGENT 1 +-#define HTTP_HEADER_HOST 2 +-#define HTTP_HEADER_AUTH 4 +-#define HTTP_HEADER_FROM 8 +-#define HTTP_HEADER_CONTENT_LENGTH 16 +-#define HTTP_HEADER_TYPE 32 +-#define HTTP_HEADER_CONNECTION 64 ++#define HTTP_HEADER_BLOCK_SIZE 1024 ++#define HTTP_HEADER_MAX_LOCATION_SIZE 8182 /* 8192 - 10 (size of "Location: ") */ ++#define PHP_URL_REDIRECT_MAX 20 ++#define HTTP_HEADER_USER_AGENT 1 ++#define HTTP_HEADER_HOST 2 ++#define HTTP_HEADER_AUTH 4 ++#define HTTP_HEADER_FROM 8 ++#define HTTP_HEADER_CONTENT_LENGTH 16 ++#define HTTP_HEADER_TYPE 32 ++#define HTTP_HEADER_CONNECTION 64 + + #define HTTP_WRAPPER_HEADER_INIT 1 + #define HTTP_WRAPPER_REDIRECTED 2 +@@ -121,17 +122,15 @@ typedef struct _php_stream_http_response_header_info { + size_t file_size; + zend_bool error; + zend_bool follow_location; +- char location[HTTP_HEADER_BLOCK_SIZE]; ++ char *location; ++ size_t location_len; + } php_stream_http_response_header_info; + + static void php_stream_http_response_header_info_init( + php_stream_http_response_header_info *header_info) + { +- header_info->transfer_encoding = NULL; +- header_info->file_size = 0; +- header_info->error = 0; ++ memset(header_info, 0, sizeof(php_stream_http_response_header_info)); + header_info->follow_location = 1; +- header_info->location[0] = '\0'; + } + + /* Trim white spaces from response header line and update its length */ +@@ -258,7 +257,22 @@ static zend_string *php_stream_http_response_headers_parse(php_stream_wrapper *w + * RFC 7238 defines 308: http://tools.ietf.org/html/rfc7238 */ + header_info->follow_location = 0; + } +- strlcpy(header_info->location, last_header_value, sizeof(header_info->location)); ++ size_t last_header_value_len = strlen(last_header_value); ++ if (last_header_value_len > HTTP_HEADER_MAX_LOCATION_SIZE) { ++ header_info->error = 1; ++ php_stream_wrapper_log_error(wrapper, options, ++ "HTTP Location header size is over the limit of %d bytes", ++ HTTP_HEADER_MAX_LOCATION_SIZE); ++ zend_string_efree(last_header_line_str); ++ return NULL; ++ } ++ if (header_info->location_len == 0) { ++ header_info->location = emalloc(last_header_value_len + 1); ++ } else if (header_info->location_len <= last_header_value_len) { ++ header_info->location = erealloc(header_info->location, last_header_value_len + 1); ++ } ++ header_info->location_len = last_header_value_len; ++ memcpy(header_info->location, last_header_value, last_header_value_len + 1); + } else if (!strncasecmp(last_header_line, "Content-Type:", sizeof("Content-Type:")-1)) { + php_stream_notify_info(context, PHP_STREAM_NOTIFY_MIME_TYPE_IS, last_header_value, 0); + } else if (!strncasecmp(last_header_line, "Content-Length:", sizeof("Content-Length:")-1)) { +@@ -541,6 +555,8 @@ finish: + } + } + ++ php_stream_http_response_header_info_init(&header_info); ++ + if (stream == NULL) + goto out; + +@@ -918,8 +934,6 @@ finish: + } + } + +- php_stream_http_response_header_info_init(&header_info); +- + /* read past HTTP headers */ + while (!php_stream_eof(stream)) { + size_t http_header_line_length; +@@ -989,12 +1003,12 @@ finish: + last_header_line_str, NULL, NULL, response_code, response_header, &header_info); + } + +- if (!reqok || (header_info.location[0] != '\0' && header_info.follow_location)) { ++ if (!reqok || (header_info.location != NULL && header_info.follow_location)) { + if (!header_info.follow_location || (((options & STREAM_ONLY_GET_HEADERS) || ignore_errors) && redirect_max <= 1)) { + goto out; + } + +- if (header_info.location[0] != '\0') ++ if (header_info.location != NULL) + php_stream_notify_info(context, PHP_STREAM_NOTIFY_REDIRECTED, header_info.location, 0); + + php_stream_close(stream); +@@ -1005,18 +1019,17 @@ finish: + header_info.transfer_encoding = NULL; + } + +- if (header_info.location[0] != '\0') { ++ if (header_info.location != NULL) { + +- char new_path[HTTP_HEADER_BLOCK_SIZE]; +- char loc_path[HTTP_HEADER_BLOCK_SIZE]; ++ char *new_path = NULL; + +- *new_path='\0'; + if (strlen(header_info.location) < 8 || + (strncasecmp(header_info.location, "http://", sizeof("http://")-1) && + strncasecmp(header_info.location, "https://", sizeof("https://")-1) && + strncasecmp(header_info.location, "ftp://", sizeof("ftp://")-1) && + strncasecmp(header_info.location, "ftps://", sizeof("ftps://")-1))) + { ++ char *loc_path = NULL; + if (*header_info.location != '/') { + if (*(header_info.location+1) != '\0' && resource->path) { + char *s = strrchr(ZSTR_VAL(resource->path), '/'); +@@ -1034,31 +1047,35 @@ finish: + if (resource->path && + ZSTR_VAL(resource->path)[0] == '/' && + ZSTR_VAL(resource->path)[1] == '\0') { +- snprintf(loc_path, sizeof(loc_path) - 1, "%s%s", +- ZSTR_VAL(resource->path), header_info.location); ++ spprintf(&loc_path, 0, "%s%s", ZSTR_VAL(resource->path), header_info.location); + } else { +- snprintf(loc_path, sizeof(loc_path) - 1, "%s/%s", +- ZSTR_VAL(resource->path), header_info.location); ++ spprintf(&loc_path, 0, "%s/%s", ZSTR_VAL(resource->path), header_info.location); + } + } else { +- snprintf(loc_path, sizeof(loc_path) - 1, "/%s", header_info.location); ++ spprintf(&loc_path, 0, "/%s", header_info.location); + } + } else { +- strlcpy(loc_path, header_info.location, sizeof(loc_path)); ++ loc_path = header_info.location; ++ header_info.location = NULL; + } + if ((use_ssl && resource->port != 443) || (!use_ssl && resource->port != 80)) { +- snprintf(new_path, sizeof(new_path) - 1, "%s://%s:%d%s", ZSTR_VAL(resource->scheme), ZSTR_VAL(resource->host), resource->port, loc_path); ++ spprintf(&new_path, 0, "%s://%s:%d%s", ZSTR_VAL(resource->scheme), ++ ZSTR_VAL(resource->host), resource->port, loc_path); + } else { +- snprintf(new_path, sizeof(new_path) - 1, "%s://%s%s", ZSTR_VAL(resource->scheme), ZSTR_VAL(resource->host), loc_path); ++ spprintf(&new_path, 0, "%s://%s%s", ZSTR_VAL(resource->scheme), ++ ZSTR_VAL(resource->host), loc_path); + } ++ efree(loc_path); + } else { +- strlcpy(new_path, header_info.location, sizeof(new_path)); ++ new_path = header_info.location; ++ header_info.location = NULL; + } + + php_url_free(resource); + /* check for invalid redirection URLs */ + if ((resource = php_url_parse(new_path)) == NULL) { + php_stream_wrapper_log_error(wrapper, options, "Invalid redirect URL! %s", new_path); ++ efree(new_path); + goto out; + } + +@@ -1070,6 +1087,7 @@ finish: + while (s < e) { \ + if (iscntrl(*s)) { \ + php_stream_wrapper_log_error(wrapper, options, "Invalid redirect URL! %s", new_path); \ ++ efree(new_path); \ + goto out; \ + } \ + s++; \ +@@ -1085,6 +1103,7 @@ finish: + stream = php_stream_url_wrap_http_ex( + wrapper, new_path, mode, options, opened_path, context, + --redirect_max, HTTP_WRAPPER_REDIRECTED, response_header STREAMS_CC); ++ efree(new_path); + } else { + php_stream_wrapper_log_error(wrapper, options, "HTTP request failed! %s", tmp_line); + } +@@ -1097,6 +1116,10 @@ out: + efree(http_header_line); + } + ++ if (header_info.location != NULL) { ++ efree(header_info.location); ++ } ++ + if (resource) { + php_url_free(resource); + } +diff --git a/ext/standard/tests/http/ghsa-52jp-hrpf-2jff-001.phpt b/ext/standard/tests/http/ghsa-52jp-hrpf-2jff-001.phpt +new file mode 100644 +index 00000000000..46d77ec4aff +--- /dev/null ++++ b/ext/standard/tests/http/ghsa-52jp-hrpf-2jff-001.phpt +@@ -0,0 +1,58 @@ ++--TEST-- ++GHSA-52jp-hrpf-2jff: HTTP stream wrapper truncate redirect location to 1024 bytes (success) ++--FILE-- ++<?php ++$serverCode = <<<'CODE' ++$ctxt = stream_context_create([ ++ "socket" => [ ++ "tcp_nodelay" => true ++ ] ++ ]); ++ ++ $server = stream_socket_server( ++ "tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt); ++ phpt_notify_server_start($server); ++ ++ $conn = stream_socket_accept($server); ++ ++ phpt_notify(WORKER_DEFAULT_NAME, "server-accepted"); ++ ++ $loc = str_repeat("y", 8000); ++ fwrite($conn, "HTTP/1.0 301 Ok\r\nContent-Type: text/html;\r\nLocation: $loc\r\n\r\nbody\r\n"); ++CODE; ++ ++$clientCode = <<<'CODE' ++ function stream_notification_callback($notification_code, $severity, $message, $message_code, $bytes_transferred, $bytes_max) { ++ switch($notification_code) { ++ case STREAM_NOTIFY_MIME_TYPE_IS: ++ echo "Found the mime-type: ", $message, PHP_EOL; ++ break; ++ case STREAM_NOTIFY_REDIRECTED: ++ echo "Redirected: "; ++ var_dump($message); ++ } ++ } ++ ++ $ctx = stream_context_create(); ++ stream_context_set_params($ctx, array("notification" => "stream_notification_callback")); ++ var_dump(trim(file_get_contents("http://{{ ADDR }}", false, $ctx))); ++ var_dump($http_response_header); ++CODE; ++ ++include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++ServerClientTestCase::getInstance()->run($clientCode, $serverCode); ++?> ++--EXPECTF-- ++Found the mime-type: text/html; ++Redirected: string(8000) "%s" ++ ++Warning: file_get_contents(http://127.0.0.1:%d): failed to open stream: %s ++string(0) "" ++array(3) { ++ [0]=> ++ string(15) "HTTP/1.0 301 Ok" ++ [1]=> ++ string(24) "Content-Type: text/html;" ++ [2]=> ++ string(8010) "Location: %s" ++} +diff --git a/ext/standard/tests/http/ghsa-52jp-hrpf-2jff-002.phpt b/ext/standard/tests/http/ghsa-52jp-hrpf-2jff-002.phpt +new file mode 100644 +index 00000000000..d25c89d06e5 +--- /dev/null ++++ b/ext/standard/tests/http/ghsa-52jp-hrpf-2jff-002.phpt +@@ -0,0 +1,55 @@ ++--TEST-- ++GHSA-52jp-hrpf-2jff: HTTP stream wrapper truncate redirect location to 1024 bytes (over limit) ++--FILE-- ++<?php ++$serverCode = <<<'CODE' ++$ctxt = stream_context_create([ ++ "socket" => [ ++ "tcp_nodelay" => true ++ ] ++ ]); ++ ++ $server = stream_socket_server( ++ "tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt); ++ phpt_notify_server_start($server); ++ ++ $conn = stream_socket_accept($server); ++ ++ phpt_notify(WORKER_DEFAULT_NAME, "server-accepted"); ++ ++ $loc = str_repeat("y", 9000); ++ fwrite($conn, "HTTP/1.0 301 Ok\r\nContent-Type: text/html;\r\nLocation: $loc\r\n\r\nbody\r\n"); ++CODE; ++ ++$clientCode = <<<'CODE' ++ function stream_notification_callback($notification_code, $severity, $message, $message_code, $bytes_transferred, $bytes_max) { ++ switch($notification_code) { ++ case STREAM_NOTIFY_MIME_TYPE_IS: ++ echo "Found the mime-type: ", $message, PHP_EOL; ++ break; ++ case STREAM_NOTIFY_REDIRECTED: ++ echo "Redirected: "; ++ var_dump($message); ++ } ++ } ++ ++ $ctx = stream_context_create(); ++ stream_context_set_params($ctx, array("notification" => "stream_notification_callback")); ++ var_dump(trim(file_get_contents("http://{{ ADDR }}", false, $ctx))); ++ var_dump($http_response_header); ++CODE; ++ ++include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); ++ServerClientTestCase::getInstance()->run($clientCode, $serverCode); ++?> ++--EXPECTF-- ++Found the mime-type: text/html; ++ ++Warning: file_get_contents(http://127.0.0.1:%d): failed to open stream: HTTP Location header size is over the limit of 8182 bytes in %s ++string(0) "" ++array(2) { ++ [0]=> ++ string(15) "HTTP/1.0 301 Ok" ++ [1]=> ++ string(24) "Content-Type: text/html;" ++} +-- +2.48.1 + diff --git a/php-cve-2025-6491.patch b/php-cve-2025-6491.patch new file mode 100644 index 0000000..ec8ce61 --- /dev/null +++ b/php-cve-2025-6491.patch @@ -0,0 +1,103 @@ +From c13a3b2a3710c66231f0cad16ff74ef75c8672a7 Mon Sep 17 00:00:00 2001 +From: Ahmed Lekssays <lekssaysahmed@gmail.com> +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) +(cherry picked from commit 1b7410a57f8a5fd1dd43854bcf7b9200517c9fd2) +--- + 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 7429aebbf70..94f1db526c6 100644 +--- a/ext/soap/soap.c ++++ b/ext/soap/soap.c +@@ -4457,8 +4457,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..7a1bf026022 +--- /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-- ++<?php ++if (PHP_INT_SIZE != 8) die("skip: 64-bit only"); ++?> ++--INI-- ++memory_limit=8G ++--FILE-- ++<?php ++ ++class TestSoapClient extends SoapClient { ++ public function __doRequest( ++ $request, ++ $location, ++ $action, ++ $version, ++ $one_way = false ++ ): ?string { ++ die($request); ++ } ++} ++ ++$prefix = str_repeat("A", 2 * 1024 * 1024 * 1024); ++$qname = "{$prefix}:tag"; ++ ++echo "Attempting to create SoapVar with very large QName\n"; ++ ++$var = new SoapVar("value", XSD_QNAME, null, null, $qname); ++ ++echo "Attempting encoding\n"; ++ ++$options = [ ++ 'location' => '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 ++<?xml version="1.0" encoding="UTF-8"?> ++<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="urn:dummy" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><ns1:DummyFunction><param0 xsi:type="xsd:QName">value</param0></ns1:DummyFunction></SOAP-ENV:Body></SOAP-ENV:Envelope> +-- +2.50.0 + diff --git a/php-fpm.service b/php-fpm.service index 687dfc0..0712a11 100644 --- a/php-fpm.service +++ b/php-fpm.service @@ -4,7 +4,7 @@ [Unit] Description=The PHP FastCGI Process Manager -After=syslog.target network.target +After=network.target [Service] Type=notify diff --git a/php-ghsa-4w77-75f9-2c8w.patch b/php-ghsa-4w77-75f9-2c8w.patch new file mode 100644 index 0000000..4886973 --- /dev/null +++ b/php-ghsa-4w77-75f9-2c8w.patch @@ -0,0 +1,135 @@ +From 56488a8a4ec68e58eecc9e78dd75e41adf56984c Mon Sep 17 00:00:00 2001 +From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> +Date: Sat, 9 Nov 2024 15:29:52 +0100 +Subject: [PATCH 6/7] Fix GHSA-4w77-75f9-2c8w + +(cherry picked from commit 7dd336ae838bbf2c62dc47e3c900d657d3534c02) +(cherry picked from commit 462092a48aa0dbad24d9fa8a4a9d418faa14d309) +--- + sapi/cli/php_cli_server.c | 6 +--- + sapi/cli/tests/ghsa-4w77-75f9-2c8w.phpt | 41 +++++++++++++++++++++++++ + 2 files changed, 42 insertions(+), 5 deletions(-) + create mode 100644 sapi/cli/tests/ghsa-4w77-75f9-2c8w.phpt + +diff --git a/sapi/cli/php_cli_server.c b/sapi/cli/php_cli_server.c +index c3097861e3f..8717dc57418 100644 +--- a/sapi/cli/php_cli_server.c ++++ b/sapi/cli/php_cli_server.c +@@ -1923,8 +1923,6 @@ static size_t php_cli_server_client_send_through(php_cli_server_client *client, + + static void php_cli_server_client_populate_request_info(const php_cli_server_client *client, sapi_request_info *request_info) /* {{{ */ + { +- char *val; +- + request_info->request_method = php_http_method_str(client->request.request_method); + request_info->proto_num = client->request.protocol_version; + request_info->request_uri = client->request.request_uri; +@@ -1932,9 +1930,7 @@ static void php_cli_server_client_populate_request_info(const php_cli_server_cli + request_info->query_string = client->request.query_string; + request_info->content_length = client->request.content_len; + request_info->auth_user = request_info->auth_password = request_info->auth_digest = NULL; +- if (NULL != (val = zend_hash_str_find_ptr(&client->request.headers, "content-type", sizeof("content-type")-1))) { +- request_info->content_type = val; +- } ++ request_info->content_type = zend_hash_str_find_ptr(&client->request.headers, "content-type", sizeof("content-type")-1); + } /* }}} */ + + static void destroy_request_info(sapi_request_info *request_info) /* {{{ */ +diff --git a/sapi/cli/tests/ghsa-4w77-75f9-2c8w.phpt b/sapi/cli/tests/ghsa-4w77-75f9-2c8w.phpt +new file mode 100644 +index 00000000000..80944c3d14f +--- /dev/null ++++ b/sapi/cli/tests/ghsa-4w77-75f9-2c8w.phpt +@@ -0,0 +1,41 @@ ++--TEST-- ++GHSA-4w77-75f9-2c8w (Heap-Use-After-Free in sapi_read_post_data Processing in CLI SAPI Interface) ++--INI-- ++allow_url_fopen=1 ++--SKIPIF-- ++<?php ++include "skipif.inc"; ++?> ++--FILE-- ++<?php ++include "php_cli_server.inc"; ++ ++$serverCode = <<<'CODE' ++var_dump(file_get_contents('php://input')); ++CODE; ++ ++php_cli_server_start($serverCode, null, []); ++ ++$options = [ ++ "http" => [ ++ "method" => "POST", ++ "header" => "Content-Type: application/x-www-form-urlencoded", ++ "content" => "AAAAA", ++ ], ++]; ++$context = stream_context_create($options); ++ ++echo file_get_contents("http://" . PHP_CLI_SERVER_ADDRESS . "/", false, $context); ++ ++$options = [ ++ "http" => [ ++ "method" => "POST", ++ ], ++]; ++$context = stream_context_create($options); ++ ++echo file_get_contents("http://" . PHP_CLI_SERVER_ADDRESS . "/", false, $context); ++?> ++--EXPECT-- ++string(5) "AAAAA" ++string(0) "" +-- +2.47.0 + +From d8d682d3d6a4d027771806c8fc77128cae078d29 Mon Sep 17 00:00:00 2001 +From: Remi Collet <remi@remirepo.net> +Date: Fri, 22 Nov 2024 08:58:10 +0100 +Subject: [PATCH 7/7] NEWS for 8.1.31 backports + +(cherry picked from commit 22bdb43da0ecd6e72d63b63aa6c1f3a25d1bca3a) +--- + NEWS | 24 ++++++++++++++++++++++++ + 1 file changed, 24 insertions(+) + +diff --git a/NEWS b/NEWS +index 62616d6312d..f600d6aea65 100644 +--- a/NEWS ++++ b/NEWS +@@ -1,6 +1,30 @@ + PHP NEWS + ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| + ++Backported from 8.1.31 ++ ++- CLI: ++ . Fixed bug GHSA-4w77-75f9-2c8w (Heap-Use-After-Free in sapi_read_post_data ++ Processing in CLI SAPI Interface). (nielsdos) ++ ++- LDAP: ++ . Fixed bug GHSA-g665-fm4p-vhff (OOB access in ldap_escape). (CVE-2024-8932) ++ (nielsdos) ++ ++- PDO DBLIB: ++ . Fixed bug GHSA-5hqh-c84r-qjcv (Integer overflow in the dblib quoter causing ++ OOB writes). (CVE-2024-11236) (nielsdos) ++ ++- PDO Firebird: ++ . Fixed bug GHSA-5hqh-c84r-qjcv (Integer overflow in the firebird quoter ++ causing OOB writes). (CVE-2024-11236) (nielsdos) ++ ++- Streams: ++ . Fixed bug GHSA-c5f2-jwm7-mmq2 (Configuring a proxy in a stream context ++ might allow for CRLF injection in URIs). (CVE-2024-11234) (Jakub Zelenka) ++ . Fixed bug GHSA-r977-prxv-hc43 (Single byte overread with ++ convert.quoted-printable-decode filter). (CVE-2024-11233) (nielsdos) ++ + Backported from 8.1.30 + + - CGI: +-- +2.47.0 + @@ -49,17 +49,10 @@ %global mysql_sock %(mysql_config --socket 2>/dev/null || echo /var/lib/mysql/mysql.sock) -%ifarch aarch64 -%global oraclever 19.24 -%global oraclemax 20 -%global oraclelib 19.1 -%global oracledir 19.24 -%else -%global oraclever 23.6 +%global oraclever 23.8 %global oraclemax 24 %global oraclelib 23.1 %global oracledir 23 -%endif # Build for LiteSpeed Web Server (LSAPI) %global with_lsws 1 @@ -75,12 +68,18 @@ # Optional components; pass "--with mssql" etc to rpmbuild. %global with_oci8 %{?_with_oci8:1}%{!?_with_oci8:0} %global with_imap 1 -%global with_interbase 1 %global with_freetds 1 %global with_tidy 1 %global with_sqlite3 1 %global with_enchant 1 +# Build firebird extensions, you can disable using --without firebird +%if 0%{?rhel} == 10 +%bcond_with firebird +%else +%bcond_without firebird +%endif + %if 0%{?fedora} >= 27 || 0%{?rhel} >= 8 %global with_libpcre 1 %else @@ -119,7 +118,7 @@ Summary: PHP scripting language for creating dynamic web sites Name: %{?scl_prefix}php Version: %{upver}%{?rcver:~%{rcver}}%{?gh_date:.%{gh_date}} -Release: 18%{?dist} +Release: 24%{?dist} # All files licensed under PHP version 3.01, except # Zend is licensed under Zend # TSRM is licensed under BSD @@ -159,11 +158,12 @@ Source53: 20-ffi.ini Patch1: php-7.4.0-httpd.patch Patch5: php-7.2.0-includedir.patch Patch6: php-7.4.0-embed.patch -# For libxml 2.12 from 8.1 -Patch7: php-7.4.33-libxml212.patch Patch8: php-7.2.0-libdb.patch -Patch9: php-7.0.7-curl.patch Patch10: php-7.4.33-gcc14.patch +# For recent ICU from 8.2 +Patch11: php-7.4.33-icu.patch +# Fix strict prototypes from 8.1 +Patch12: php-7.4.33-proto.patch # Functional changes Patch42: php-7.3.3-systzdata-v19.patch @@ -201,6 +201,20 @@ Patch210: php-cve-2024-8925.patch Patch211: php-cve-2024-8926.patch Patch212: php-cve-2024-8927.patch Patch213: php-cve-2024-9026.patch +Patch214: php-cve-2024-11236.patch +Patch215: php-cve-2024-11234.patch +Patch216: php-cve-2024-8932.patch +Patch217: php-cve-2024-11233.patch +Patch218: php-ghsa-4w77-75f9-2c8w.patch +Patch219: php-cve-2024-8929.patch +Patch220: php-cve-2025-1217.patch +Patch221: php-cve-2025-1734.patch +Patch222: php-cve-2025-1861.patch +Patch223: php-cve-2025-1736.patch +Patch224: php-cve-2025-1219.patch +Patch225: php-cve-2025-6491.patch +Patch226: php-cve-2025-1220.patch +Patch227: php-cve-2025-1735.patch # Fixes for tests (300+) # Factory is droped from system tzdata @@ -209,6 +223,8 @@ Patch300: php-7.0.10-datetests.patch Patch301: php-7.4.33-tests.patch # For zlib-ng Patch302: php-7.4.33-zlib-tests.patch +# for pcre2 10.45 +Patch303: php-7.4.33-pcretests.patch # WIP @@ -621,7 +637,7 @@ BuildRequires: pkgconfig(libxml-2.0) The %{?scl_prefix}php-soap package contains a dynamic shared object that will add support to PHP for using the SOAP web services protocol. -%if %{with_interbase} +%if %{with firebird} %package pdo-firebird Summary: PDO driver for Interbase/Firebird databases Group: Development/Languages @@ -653,14 +669,7 @@ Summary: A module for PHP applications that use OCI8 databases Group: Development/Languages # All files licensed under PHP version 3.01 License: PHP -%ifarch aarch64 -BuildRequires: oracle-instantclient%{oraclever}-devel -# Should requires libclntsh.so.19.1()(aarch-64), but it's not provided by Oracle RPM. -Requires: libclntsh.so.%{oraclelib} -AutoReq: 0 -%else BuildRequires: (oracle-instantclient-devel >= %{oraclever} with oracle-instantclient-devel < %{oraclemax}) -%endif Requires: %{?scl_prefix}php-pdo%{?_isa} = %{version}-%{release} Provides: %{?scl_prefix}php_database Provides: %{?scl_prefix}php-pdo_oci @@ -968,12 +977,10 @@ in pure PHP. %patch -P1 -p1 -b .mpmcheck %patch -P5 -p1 -b .includedir %patch -P6 -p1 -b .embed -%patch -P7 -p1 -b .libxml212 %patch -P8 -p1 -b .libdb -%if 0%{?rhel} == 7 -%patch -P9 -p1 -b .curltls -%endif %patch -P10 -p1 -b .gcc14 +%patch -P11 -p1 -b .icu74 +%patch -P12 -p1 -b .proto %patch -P42 -p1 -b .systzdata %patch -P43 -p1 -b .headers @@ -1006,11 +1013,26 @@ rm ext/openssl/tests/p12_with_extra_certs.p12 %patch -P211 -p1 -b .cve8926 %patch -P212 -p1 -b .cve8927 %patch -P213 -p1 -b .cve9026 +%patch -P214 -p1 -b .cve11236 +%patch -P215 -p1 -b .cve11234 +%patch -P216 -p1 -b .cve8932 +%patch -P217 -p1 -b .cve11233 +%patch -P218 -p1 -b .ghsa4w77 +%patch -P219 -p1 -b .cve8929 +%patch -P220 -p1 -b .cve1217 +%patch -P221 -p1 -b .cve1734 +%patch -P222 -p1 -b .cve1861 +%patch -P223 -p1 -b .cve1736 +%patch -P224 -p1 -b .cve1219 +%patch -P225 -p1 -b .cve6491 +%patch -P226 -p1 -b .cve1220 +%patch -P227 -p1 -b .cve1735 # Fixes for tests %patch -P300 -p1 -b .datetests %patch -P301 -p1 -b .tests %patch -P302 -p1 -b .zlibng +%patch -P303 -p1 -b .pcretests # WIP patch @@ -1052,6 +1074,13 @@ rm Zend/tests/bug68412.phpt rm sapi/cli/tests/upload_2G.phpt # tar issue rm ext/zlib/tests/004-mb.phpt +# Known to fail +%if 0%{?rhel} == 8 +rm ext/openssl/tests/openssl_error_string_basic.phpt +rm ext/openssl/tests/openssl_open_basic.phpt +%endif +rm ext/openssl/tests/openssl_private_decrypt_basic.phpt +rm ext/openssl/tests/openssl_x509_parse_basic.phpt # Safety check for API version change. pver=$(sed -n '/#define PHP_VERSION /{s/.* "//;s/".*$//;p}' main/php_version.h) @@ -1279,7 +1308,7 @@ build --libdir=%{_libdir}/php \ --with-oci8=shared,instantclient,%{_root_prefix}/lib/oracle/%{oracledir}/client64/lib,%{oraclever} \ --with-pdo-oci=shared,instantclient,%{_root_prefix}/lib/oracle/%{oracledir}/client64/lib,%{oraclever} \ %endif -%if %{with_interbase} +%if %{with firebird} --with-pdo-firebird=shared \ %endif --enable-dom=shared \ @@ -1584,7 +1613,7 @@ for mod in pgsql odbc ldap snmp \ %if %{with_oci8} oci8 pdo_oci \ %endif -%if %{with_interbase} +%if %{with firebird} pdo_firebird \ %endif %if %{with_freetds} @@ -1875,7 +1904,7 @@ EOF %files pspell -f files.pspell %files intl -f files.intl %files process -f files.process -%if %{with_interbase} +%if %{with firebird} %files pdo-firebird -f files.pdo_firebird %endif %if %{with_enchant} @@ -1897,6 +1926,50 @@ EOF %changelog +* Thu Jul 3 2025 Remi Collet <remi@remirepo.net> - 7.4.33-24 +- 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 + +* Mon Mar 17 2025 Remi Collet <remi@remirepo.net> - 7.4.33-23 +- Fix libxml streams use wrong `content-type` header when requesting a redirected resource + CVE-2025-1219 +- Fix Stream HTTP wrapper header check might omit basic auth header + CVE-2025-1736 +- Fix Stream HTTP wrapper truncate redirect location to 1024 bytes + CVE-2025-1861 +- Fix Streams HTTP wrapper does not fail for headers without colon + CVE-2025-1734 +- Fix Header parser of `http` stream wrapper does not handle folded headers + CVE-2025-1217 +- use oracle client library version 23.7 on x86_64 and aarch64 + +* Thu Feb 13 2025 Remi Collet <remi@remirepo.net> - 7.4.33-22 +- backport fix for ICU 74+ +- backport fix strict prototypes + +* Wed Nov 27 2024 Remi Collet <remi@remirepo.net> - 7.4.33-21 +- Fix Leak partial content of the heap through heap buffer over-read + CVE-2024-8929 + +* Fri Nov 22 2024 Remi Collet <remi@remirepo.net> - 7.4.33-20 +- Fix Heap-Use-After-Free in sapi_read_post_data Processing in CLI SAPI Interface + GHSA-4w77-75f9-2c8w +- Fix OOB access in ldap_escape + CVE-2024-8932 +- Fix Integer overflow in the dblib/firebird quoter causing OOB writes + CVE-2024-11236 +- Fix Configuring a proxy in a stream context might allow for CRLF injection in URIs + CVE-2024-11234 +- Fix Single byte overread with convert.quoted-printable-decode filter + CVE-2024-11233 + +* Fri Nov 15 2024 Remi Collet <remi@remirepo.net> - 7.4.33-19 +- disable firebird on EL-10 + * Thu Sep 26 2024 Remi Collet <remi@remirepo.net> - 7.4.33-18 - Fix Bypass of CVE-2012-1823, Argument Injection in PHP-CGI CVE-2024-4577 |