diff options
45 files changed, 9141 insertions, 149 deletions
@@ -1,2 +1,3 @@ +clog php-*.tar.xz php70-php-*src.rpm @@ -1,19 +1,12 @@ -===== 7.0.33-16 (2020-01-21) +===== 7.0.33-46 (2026-05-13) $ grep -r 'Tests failed' /var/lib/mock/scl70*/build.log -/var/lib/mock/scl70el6x/build.log:Tests failed : 0 -/var/lib/mock/scl70el7x/build.log:Tests failed : 1 -/var/lib/mock/scl70el8x/build.log:Tests failed : 28 -/var/lib/mock/scl70fc29x/build.log:Tests failed : 1 -/var/lib/mock/scl70fc30x/build.log:Tests failed : 1 -/var/lib/mock/scl70fc31x/build.log:Tests failed : 1 +/var/lib/mock/scl70el8x/build.log:Tests failed : 33 -el7x: - Bug #75457 (heap-use-after-free in php7.0.25) [ext/pcre/tests/bug75457.phpt] -fc29x, fc30x, fc31x: - TLS server rate-limits client-initiated renegotiation [ext/openssl/tests/stream_server_reneg_limit.phpt] +el8x: + 2 related to tzdata, expired test cert and openssl policy 1 proc_open give erratic test results :( @@ -14,3 +14,7 @@ %@SCL@__php @BINDIR@/php +%@SCL@__phpize @BINDIR@/phpize + +%@SCL@__phpconfig @BINDIR@/php-config + diff --git a/php-5.6.3-oci8conf.patch b/php-5.6.3-oci8conf.patch index f2d8f99..0f923f7 100644 --- a/php-5.6.3-oci8conf.patch +++ b/php-5.6.3-oci8conf.patch @@ -10,28 +10,29 @@ diff -up php5.3-201104170830/ext/ldap/php_ldap.h.remi-oci8 php5.3-201104170830/e extern zend_module_entry ldap_module_entry; #define ldap_module_ptr &ldap_module_entry -diff -up php5.3-201104170830/ext/oci8/config.m4.remi-oci8 php5.3-201104170830/ext/oci8/config.m4 ---- php5.3-201104170830/ext/oci8/config.m4.remi-oci8 2011-03-30 00:35:22.000000000 +0200 -+++ php5.3-201104170830/ext/oci8/config.m4 2011-04-17 11:55:25.628871315 +0200 -@@ -376,6 +376,7 @@ if test "$PHP_OCI8" != "no"; then - - dnl Header directory for Instant Client SDK RPM install - OCISDKRPMINC=`echo "$PHP_OCI8_INSTANT_CLIENT" | $PHP_OCI8_SED -e 's!^/usr/lib/oracle/\(.*\)/client\('${PHP_OCI8_IC_LIBDIR_SUFFIX}'\)*/lib[/]*$!/usr/include/oracle/\1/client\2!'` -+ OCISDKRPMINC=`echo "$PHP_OCI8_INSTANT_CLIENT" | $PHP_OCI8_SED -e 's!^/usr/\(lib64\|lib\)/oracle/\(.*\)/\(client64\|client\)/lib[/]*$!/usr/include/oracle/\2/\3!'` - - dnl Header directory for Instant Client SDK zip file install - OCISDKZIPINC=$PHP_OCI8_INSTANT_CLIENT/sdk/include -diff -up php5.3-201104170830/ext/pdo_oci/config.m4.remi-oci8 php5.3-201104170830/ext/pdo_oci/config.m4 ---- php5.3-201104170830/ext/pdo_oci/config.m4.remi-oci8 2011-04-02 04:35:24.000000000 +0200 -+++ php5.3-201104170830/ext/pdo_oci/config.m4 2011-04-17 12:02:42.837194120 +0200 -@@ -104,8 +104,10 @@ You need to tell me where to find your O +diff -up ./ext/pdo_oci/config.m4.remi-oci8 ./ext/pdo_oci/config.m4 +--- ./ext/pdo_oci/config.m4.remi-oci8 2019-10-22 18:59:47.000000000 +0200 ++++ ./ext/pdo_oci/config.m4 2023-09-22 09:49:00.888471382 +0200 +@@ -104,7 +104,10 @@ You need to tell me where to find your O + fi + fi + AC_MSG_CHECKING([for oci.h]) +- if test -f $PDO_OCI_IC_PREFIX/include/oracle/$PDO_OCI_IC_VERS/$PDO_OCI_CLIENT_DIR/oci.h ; then ++ if test -f $PDO_OCI_IC_PREFIX/include/oracle/$PDO_OCI_IC_MAJ_VER/$PDO_OCI_CLIENT_DIR/oci.h ; then ++ PHP_ADD_INCLUDE($PDO_OCI_IC_PREFIX/include/oracle/$PDO_OCI_IC_MAJ_VER/$PDO_OCI_CLIENT_DIR) ++ AC_MSG_RESULT($PDO_OCI_IC_PREFIX/include/oracle/$PDO_OCI_IC_MAJ_VER/$PDO_OCI_CLIENT_DIR) ++ elif test -f $PDO_OCI_IC_PREFIX/include/oracle/$PDO_OCI_IC_VERS/$PDO_OCI_CLIENT_DIR/oci.h ; then + PHP_ADD_INCLUDE($PDO_OCI_IC_PREFIX/include/oracle/$PDO_OCI_IC_VERS/$PDO_OCI_CLIENT_DIR) + AC_MSG_RESULT($PDO_OCI_IC_PREFIX/include/oracle/$PDO_OCI_IC_VERS/$PDO_OCI_CLIENT_DIR) + elif test -f $PDO_OCI_IC_PREFIX/lib/oracle/$PDO_OCI_IC_VERS/$PDO_OCI_CLIENT_DIR/include/oci.h ; then +@@ -119,8 +122,10 @@ You need to tell me where to find your O else AC_MSG_ERROR([I'm too dumb to figure out where the include dir is in your Instant Client install]) fi - if test -f "$PDO_OCI_IC_PREFIX/lib/oracle/$PDO_OCI_IC_VERS/$PDO_OCI_CLIENT_DIR/lib/libclntsh.$SHLIB_SUFFIX_NAME" ; then - PDO_OCI_LIB_DIR="$PDO_OCI_IC_PREFIX/lib/oracle/$PDO_OCI_IC_VERS/$PDO_OCI_CLIENT_DIR/lib" -+ if test -f "$PDO_OCI_IC_PREFIX/lib64/oracle/$PDO_OCI_IC_VERS/$PDO_OCI_CLIENT_DIR/lib/libclntsh.$SHLIB_SUFFIX_NAME" ; then -+ PDO_OCI_LIB_DIR="$PDO_OCI_IC_PREFIX/lib64/oracle/$PDO_OCI_IC_VERS/$PDO_OCI_CLIENT_DIR/lib" ++ if test -f "$PDO_OCI_IC_PREFIX/lib/oracle/$PDO_OCI_IC_MAJ_VER/$PDO_OCI_CLIENT_DIR/lib/libclntsh.$SHLIB_SUFFIX_NAME" ; then ++ PDO_OCI_LIB_DIR="$PDO_OCI_IC_PREFIX/lib/oracle/$PDO_OCI_IC_MAJ_VER/$PDO_OCI_CLIENT_DIR/lib" + elif test -f "$PDO_OCI_IC_PREFIX/lib/oracle/$PDO_OCI_IC_VERS/$PDO_OCI_CLIENT_DIR/lib/libclntsh.$SHLIB_SUFFIX_NAME" ; then + PDO_OCI_LIB_DIR="$PDO_OCI_IC_PREFIX/lib/oracle/$PDO_OCI_IC_VERS/$PDO_OCI_CLIENT_DIR/lib" elif test -f "$PDO_OCI_IC_PREFIX/$PDO_OCI_CLIENT_DIR/lib/libclntsh.$SHLIB_SUFFIX_NAME" ; then diff --git a/php-7.0.0-systzdata-v14.patch b/php-7.0.0-systzdata-v14.patch index dd6b784..bbf65ed 100644 --- a/php-7.0.0-systzdata-v14.patch +++ b/php-7.0.0-systzdata-v14.patch @@ -392,7 +392,7 @@ diff -up php-7.0.12RC1/ext/date/lib/parse_tz.c.systzdata php-7.0.12RC1/ext/date/ + size_t n; + char *data, *p; + -+ data = malloc(3 * sysdb->index_size + 7); ++ data = malloc(3 * sysdb->index_size + sizeof(FAKE_HEADER) - 1); + + p = mempcpy(data, FAKE_HEADER, sizeof(FAKE_HEADER) - 1); + diff --git a/php-7.0.31-icu62.patch b/php-7.0.31-icu62.patch index 50ec038..f65271c 100644 --- a/php-7.0.31-icu62.patch +++ b/php-7.0.31-icu62.patch @@ -571,7 +571,7 @@ index e8428e1cbbe8..52408f8e9183 100644 PHP_SETUP_ICU(INTL_SHARED_LIBADD) PHP_SUBST(INTL_SHARED_LIBADD) PHP_REQUIRE_CXX() -+ INTL_COMMON_FLAGS="$ICU_INCS -Wno-write-strings -D__STDC_LIMIT_MACROS -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1" ++ INTL_COMMON_FLAGS="$ICU_INCS -Wno-write-strings -DU_DEFINE_FALSE_AND_TRUE=1 -D__STDC_LIMIT_MACROS -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1" if test "$icu_version" -ge "4002"; then icu_spoof_src=" spoofchecker/spoofchecker_class.c \ spoofchecker/spoofchecker.c\ diff --git a/php-bug76450.patch b/php-bug76450.patch new file mode 100644 index 0000000..e745e42 --- /dev/null +++ b/php-bug76450.patch @@ -0,0 +1,278 @@ +From b671a8dd887ae7f661f6233e734179e8bca3daf6 Mon Sep 17 00:00:00 2001 +From: sim1984 <sim-mail@list.ru> +Date: Mon, 25 Jun 2018 21:35:51 +0300 +Subject: [PATCH 3/8] Fix bug #76488 Memory leak when fetching a BLOB field + +Add a phpt test + +(cherry picked from commit 3847a6fcb63c362548e9434b195232f2dcf7a6c7) +--- + ext/pdo_firebird/firebird_statement.c | 2 +- + ext/pdo_firebird/tests/bug_76488.phpt | 32 +++++++++++++++++++++++++++ + 2 files changed, 33 insertions(+), 1 deletion(-) + create mode 100644 ext/pdo_firebird/tests/bug_76488.phpt + +diff --git a/ext/pdo_firebird/firebird_statement.c b/ext/pdo_firebird/firebird_statement.c +index 3feeedf39f..88be6da369 100644 +--- a/ext/pdo_firebird/firebird_statement.c ++++ b/ext/pdo_firebird/firebird_statement.c +@@ -294,7 +294,7 @@ static int firebird_fetch_blob(pdo_stmt_t *stmt, int colno, char **ptr, /* {{{ * + unsigned short seg_len; + ISC_STATUS stat; + +- *ptr = S->fetch_buf[colno] = erealloc(*ptr, *len+1); ++ *ptr = S->fetch_buf[colno] = erealloc(S->fetch_buf[colno], *len+1); + + for (cur_len = stat = 0; (!stat || stat == isc_segment) && cur_len < *len; cur_len += seg_len) { + +diff --git a/ext/pdo_firebird/tests/bug_76488.phpt b/ext/pdo_firebird/tests/bug_76488.phpt +new file mode 100644 +index 0000000000..dba6734c28 +--- /dev/null ++++ b/ext/pdo_firebird/tests/bug_76488.phpt +@@ -0,0 +1,32 @@ ++--TEST-- ++PDO_Firebird: Bug #76488 Memory leak when fetching a BLOB field ++--SKIPIF-- ++<?php if (!extension_loaded('interbase') || !extension_loaded('pdo_firebird')) die('skip'); ?> ++--FILE-- ++<?php ++require 'testdb.inc'; ++$dbh = new PDO('firebird:dbname='.$test_base, $user, $password) or die; ++ ++$sql = ' ++with recursive r(n) as ( ++ select 1 from rdb$database ++ union all ++ select n+1 from r where n < 1000 ++) ++select n, ++ cast(lpad(\'A\', 8000, \'A\') as BLOB sub_type TEXT) as SRC ++from r ++'; ++ ++ for ($i = 0; $i < 10; $i++) { ++ $sth = $dbh->prepare($sql); ++ $sth->execute(); ++ $rows = $sth->fetchAll(); ++ unset($rows); ++ unset($sth); ++ } ++ unset($dbh); ++ echo "OK"; ++?> ++--EXPECT-- ++OK +\ No newline at end of file +-- +2.31.1 + +From 9a58cd2ba3d99f5312966566a3632b197b676830 Mon Sep 17 00:00:00 2001 +From: "Christoph M. Becker" <cmbecker69@gmx.de> +Date: Wed, 5 May 2021 12:42:17 +0200 +Subject: [PATCH 4/8] Fix #76452: Crash while parsing blob data in + firebird_fetch_blob + +We need to prevent integer overflow when calling `erealloc()` with +`len+1`. + +(cherry picked from commit 286162e9b03071c4308e7e92597bca4239f49d89) +--- + ext/pdo_firebird/firebird_statement.c | 5 +++++ + ext/pdo_firebird/tests/bug_76452.data | Bin 0 -> 856 bytes + ext/pdo_firebird/tests/bug_76452.phpt | 31 ++++++++++++++++++++++++++ + 3 files changed, 36 insertions(+) + create mode 100644 ext/pdo_firebird/tests/bug_76452.data + create mode 100644 ext/pdo_firebird/tests/bug_76452.phpt + +diff --git a/ext/pdo_firebird/firebird_statement.c b/ext/pdo_firebird/firebird_statement.c +index 88be6da369..fde897186d 100644 +--- a/ext/pdo_firebird/firebird_statement.c ++++ b/ext/pdo_firebird/firebird_statement.c +@@ -294,6 +294,11 @@ static int firebird_fetch_blob(pdo_stmt_t *stmt, int colno, char **ptr, /* {{{ * + unsigned short seg_len; + ISC_STATUS stat; + ++ /* prevent overflow */ ++ if (*len == ZEND_ULONG_MAX) { ++ result = 0; ++ goto fetch_blob_end; ++ } + *ptr = S->fetch_buf[colno] = erealloc(S->fetch_buf[colno], *len+1); + + for (cur_len = stat = 0; (!stat || stat == isc_segment) && cur_len < *len; cur_len += seg_len) { + +From 7b11e10fa679a575ca61e5bcd66db68193a3b2fb Mon Sep 17 00:00:00 2001 +From: "Christoph M. Becker" <cmbecker69@gmx.de> +Date: Fri, 30 Apr 2021 14:10:50 +0200 +Subject: [PATCH 5/8] Fix #76450: SIGSEGV in firebird_stmt_execute + +We need to verify that the `result_size` is not larger than our buffer, +and also should make sure that the `len` which is passed to +`isc_vax_integer()` has a permissible value; otherwise we bail out. + +(cherry picked from commit bcbf8aa0c96d8d9e81ec3428232485555fae0b37) +--- + ext/pdo_firebird/firebird_statement.c | 7 +++++++ + ext/pdo_firebird/tests/bug_76450.data | Bin 0 -> 464 bytes + ext/pdo_firebird/tests/bug_76450.phpt | 29 ++++++++++++++++++++++++++ + 3 files changed, 36 insertions(+) + create mode 100644 ext/pdo_firebird/tests/bug_76450.data + create mode 100644 ext/pdo_firebird/tests/bug_76450.phpt + +diff --git a/ext/pdo_firebird/firebird_statement.c b/ext/pdo_firebird/firebird_statement.c +index fde897186d..537f6f4038 100644 +--- a/ext/pdo_firebird/firebird_statement.c ++++ b/ext/pdo_firebird/firebird_statement.c +@@ -133,8 +133,14 @@ static int firebird_stmt_execute(pdo_stmt_t *stmt) /* {{{ */ + } + if (result[0] == isc_info_sql_records) { + unsigned i = 3, result_size = isc_vax_integer(&result[1], 2); ++ if (result_size > sizeof(result)) { ++ goto error; ++ } + while (result[i] != isc_info_end && i < result_size) { + short len = (short) isc_vax_integer(&result[i + 1], 2); ++ if (len != 1 && len != 2 && len != 4) { ++ goto error; ++ } + if (result[i] != isc_info_req_select_count) { + affected_rows += isc_vax_integer(&result[i + 3], len); + } +@@ -158,6 +164,7 @@ static int firebird_stmt_execute(pdo_stmt_t *stmt) /* {{{ */ + return 1; + } while (0); + ++error: + RECORD_ERROR(stmt); + + return 0; + +From 5952ac3d2e87d5f00a32f28d6f66209955e6e777 Mon Sep 17 00:00:00 2001 +From: "Christoph M. Becker" <cmbecker69@gmx.de> +Date: Fri, 30 Apr 2021 13:53:21 +0200 +Subject: [PATCH 6/8] Fix #76449: SIGSEGV in firebird_handle_doer + +We need to verify that the `result_size` is not larger than our buffer, +and also should make sure that the `len` which is passed to +`isc_vax_integer()` has a permissible value; otherwise we bail out. + +(cherry picked from commit 08da7c73726f7b86b67d6f0ff87c73c585a7834a) +--- + ext/pdo_firebird/firebird_driver.c | 9 +++++++++ + ext/pdo_firebird/tests/bug_76449.data | Bin 0 -> 464 bytes + ext/pdo_firebird/tests/bug_76449.phpt | 23 +++++++++++++++++++++++ + 3 files changed, 32 insertions(+) + create mode 100644 ext/pdo_firebird/tests/bug_76449.data + create mode 100644 ext/pdo_firebird/tests/bug_76449.phpt + +diff --git a/ext/pdo_firebird/firebird_driver.c b/ext/pdo_firebird/firebird_driver.c +index 166fb13d43..46699d51d4 100644 +--- a/ext/pdo_firebird/firebird_driver.c ++++ b/ext/pdo_firebird/firebird_driver.c +@@ -253,8 +253,17 @@ static zend_long firebird_handle_doer(pdo_dbh_t *dbh, const char *sql, size_t sq + if (result[0] == isc_info_sql_records) { + unsigned i = 3, result_size = isc_vax_integer(&result[1],2); + ++ if (result_size > sizeof(result)) { ++ ret = -1; ++ goto free_statement; ++ } + while (result[i] != isc_info_end && i < result_size) { + short len = (short)isc_vax_integer(&result[i+1],2); ++ /* bail out on bad len */ ++ if (len != 1 && len != 2 && len != 4) { ++ ret = -1; ++ goto free_statement; ++ } + if (result[i] != isc_info_req_select_count) { + ret += isc_vax_integer(&result[i+3],len); + } + +From a1793d85e89a0c33b5496beec204ef3491af4bdc Mon Sep 17 00:00:00 2001 +From: "Christoph M. Becker" <cmbecker69@gmx.de> +Date: Thu, 29 Apr 2021 15:26:22 +0200 +Subject: [PATCH 7/8] Fix #76448: Stack buffer overflow in firebird_info_cb + +We ensure not to overflow the stack allocated buffer by using `strlcat`. + +(cherry picked from commit 67afa32541ebc4abbf633cb1e7e879b2fbb616ad) +--- + ext/pdo_firebird/firebird_driver.c | 8 +++++--- + ext/pdo_firebird/tests/bug_76448.data | Bin 0 -> 749 bytes + ext/pdo_firebird/tests/bug_76448.phpt | 23 +++++++++++++++++++++++ + 3 files changed, 28 insertions(+), 3 deletions(-) + create mode 100644 ext/pdo_firebird/tests/bug_76448.data + create mode 100644 ext/pdo_firebird/tests/bug_76448.phpt + +diff --git a/ext/pdo_firebird/firebird_driver.c b/ext/pdo_firebird/firebird_driver.c +index 46699d51d4..48808d6f3d 100644 +--- a/ext/pdo_firebird/firebird_driver.c ++++ b/ext/pdo_firebird/firebird_driver.c +@@ -556,14 +556,16 @@ static int firebird_handle_set_attribute(pdo_dbh_t *dbh, zend_long attr, zval *v + } + /* }}} */ + ++#define INFO_BUF_LEN 512 ++ + /* callback to used to report database server info */ + static void firebird_info_cb(void *arg, char const *s) /* {{{ */ + { + if (arg) { + if (*(char*)arg) { /* second call */ +- strcat(arg, " "); ++ strlcat(arg, " ", INFO_BUF_LEN); + } +- strcat(arg, s); ++ strlcat(arg, s, INFO_BUF_LEN); + } + } + /* }}} */ +@@ -574,7 +576,7 @@ static int firebird_handle_get_attribute(pdo_dbh_t *dbh, zend_long attr, zval *v + pdo_firebird_db_handle *H = (pdo_firebird_db_handle *)dbh->driver_data; + + switch (attr) { +- char tmp[512]; ++ char tmp[INFO_BUF_LEN]; + + case PDO_ATTR_AUTOCOMMIT: + ZVAL_LONG(val,dbh->auto_commit); + +From 4faea6cc54c742245639ba2736b199c711f2a77b Mon Sep 17 00:00:00 2001 +From: Stanislav Malyshev <stas@php.net> +Date: Sun, 20 Jun 2021 22:20:38 -0700 +Subject: [PATCH 8/8] Update NEWS + +(cherry picked from commit c68a687566591e2268f35d124a90c7d556ce968b) +(cherry picked from commit 7598733c51af30611aa64e456c9a777069d2efb9) +--- + NEWS | 13 +++++++++++++ + 1 file changed, 13 insertions(+) + +diff --git a/NEWS b/NEWS +index 9eff4bd7ae..861dbd7dd5 100644 +--- a/NEWS ++++ b/NEWS +@@ -1,6 +1,19 @@ + PHP NEWS + ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| + ++Backported from 7.3.29 ++ ++- Core: ++ . Fixed #81122: SSRF bypass in FILTER_VALIDATE_URL. (CVE-2021-21705) (cmb) ++ ++- PDO_Firebird: ++ . Fixed #76448: Stack buffer overflow in firebird_info_cb. (CVE-2021-21704) ++ (cmb) ++ . Fixed #76449: SIGSEGV in firebird_handle_doer. (CVE-2021-21704) (cmb) ++ . Fixed #76450: SIGSEGV in firebird_stmt_execute. (CVE-2021-21704) (cmb) ++ . Fixed #76452: Crash while parsing blob data in firebird_fetch_blob. ++ (CVE-2021-21704) (cmb) ++ + Backported from 7.3.28 + + - Imap: +-- +2.31.1 + diff --git a/php-bug77423.patch b/php-bug77423.patch new file mode 100644 index 0000000..cbe84df --- /dev/null +++ b/php-bug77423.patch @@ -0,0 +1,431 @@ +From 6d88ee38ec98c500c4a596307ce6b3e83becd0e9 Mon Sep 17 00:00:00 2001 +From: "Christoph M. Becker" <cmbecker69@gmx.de> +Date: Wed, 13 May 2020 09:36:52 +0200 +Subject: [PATCH 1/2] Fix #77423: parse_url() will deliver a wrong host to user + +To avoid that `parse_url()` returns an erroneous host, which would be +valid for `FILTER_VALIDATE_URL`, we make sure that only userinfo which +is valid according to RFC 3986 is treated as such. + +For consistency with the existing url parsing code, we use ctype +functions, although that is not necessarily correct. + +(cherry picked from commit 2d3d72412a6734e19a38ed10f385227a6238e4a6) +(cherry picked from commit 31459f94f2780e748e15d5c2951ba20adbba2366) +--- + ext/standard/tests/strings/url_t.phpt | 6 ++-- + ext/standard/tests/url/bug77423.phpt | 30 +++++++++++++++++++ + .../tests/url/parse_url_basic_001.phpt | 6 ++-- + .../tests/url/parse_url_basic_003.phpt | 2 +- + .../tests/url/parse_url_basic_005.phpt | 2 +- + ext/standard/url.c | 21 +++++++++++++ + 6 files changed, 57 insertions(+), 10 deletions(-) + create mode 100644 ext/standard/tests/url/bug77423.phpt + +diff --git a/ext/standard/tests/strings/url_t.phpt b/ext/standard/tests/strings/url_t.phpt +index e172061ec2..80e164a08e 100644 +--- a/ext/standard/tests/strings/url_t.phpt ++++ b/ext/standard/tests/strings/url_t.phpt +@@ -575,15 +575,13 @@ $sample_urls = array ( + string(16) "some_page_ref123" + } + +---> http://secret@hideout@www.php.net:80/index.php?test=1&test2=char&test3=mixesCI#some_page_ref123: array(7) { ++--> http://secret@hideout@www.php.net:80/index.php?test=1&test2=char&test3=mixesCI#some_page_ref123: array(6) { + ["scheme"]=> + string(4) "http" + ["host"]=> +- string(11) "www.php.net" ++ string(26) "secret@hideout@www.php.net" + ["port"]=> + int(80) +- ["user"]=> +- string(14) "secret@hideout" + ["path"]=> + string(10) "/index.php" + ["query"]=> +diff --git a/ext/standard/tests/url/bug77423.phpt b/ext/standard/tests/url/bug77423.phpt +new file mode 100644 +index 0000000000..be03fe95e2 +--- /dev/null ++++ b/ext/standard/tests/url/bug77423.phpt +@@ -0,0 +1,30 @@ ++--TEST-- ++Bug #77423 (parse_url() will deliver a wrong host to user) ++--FILE-- ++<?php ++$urls = array( ++ "http://php.net\@aliyun.com/aaa.do", ++ "https://example.com\uFF03@bing.com", ++); ++foreach ($urls as $url) { ++ var_dump(filter_var($url, FILTER_VALIDATE_URL)); ++ var_dump(parse_url($url)); ++} ++?> ++--EXPECT-- ++bool(false) ++array(3) { ++ ["scheme"]=> ++ string(4) "http" ++ ["host"]=> ++ string(19) "php.net\@aliyun.com" ++ ["path"]=> ++ string(7) "/aaa.do" ++} ++bool(false) ++array(2) { ++ ["scheme"]=> ++ string(5) "https" ++ ["host"]=> ++ string(26) "example.com\uFF03@bing.com" ++} +diff --git a/ext/standard/tests/url/parse_url_basic_001.phpt b/ext/standard/tests/url/parse_url_basic_001.phpt +index e468066a42..c9e9d32de0 100644 +--- a/ext/standard/tests/url/parse_url_basic_001.phpt ++++ b/ext/standard/tests/url/parse_url_basic_001.phpt +@@ -507,15 +507,13 @@ echo "Done"; + string(16) "some_page_ref123" + } + +---> http://secret@hideout@www.php.net:80/index.php?test=1&test2=char&test3=mixesCI#some_page_ref123: array(7) { ++--> http://secret@hideout@www.php.net:80/index.php?test=1&test2=char&test3=mixesCI#some_page_ref123: array(6) { + ["scheme"]=> + string(4) "http" + ["host"]=> +- string(11) "www.php.net" ++ string(26) "secret@hideout@www.php.net" + ["port"]=> + int(80) +- ["user"]=> +- string(14) "secret@hideout" + ["path"]=> + string(10) "/index.php" + ["query"]=> +diff --git a/ext/standard/tests/url/parse_url_basic_003.phpt b/ext/standard/tests/url/parse_url_basic_003.phpt +index 70dc4bb90b..431de27009 100644 +--- a/ext/standard/tests/url/parse_url_basic_003.phpt ++++ b/ext/standard/tests/url/parse_url_basic_003.phpt +@@ -68,7 +68,7 @@ echo "Done"; + --> http://secret:@www.php.net/index.php?test=1&test2=char&test3=mixesCI#some_page_ref123 : string(11) "www.php.net" + --> http://:hideout@www.php.net:80/index.php?test=1&test2=char&test3=mixesCI#some_page_ref123 : string(11) "www.php.net" + --> http://secret:hideout@www.php.net/index.php?test=1&test2=char&test3=mixesCI#some_page_ref123 : string(11) "www.php.net" +---> http://secret@hideout@www.php.net:80/index.php?test=1&test2=char&test3=mixesCI#some_page_ref123 : string(11) "www.php.net" ++--> http://secret@hideout@www.php.net:80/index.php?test=1&test2=char&test3=mixesCI#some_page_ref123 : string(26) "secret@hideout@www.php.net" + --> http://secret:hid:out@www.php.net:80/index.php?test=1&test2=char&test3=mixesCI#some_page_ref123 : string(11) "www.php.net" + --> nntp://news.php.net : string(12) "news.php.net" + --> ftp://ftp.gnu.org/gnu/glic/glibc.tar.gz : string(11) "ftp.gnu.org" +diff --git a/ext/standard/tests/url/parse_url_basic_005.phpt b/ext/standard/tests/url/parse_url_basic_005.phpt +index b2ca06ff96..b2c1a1d6dd 100644 +--- a/ext/standard/tests/url/parse_url_basic_005.phpt ++++ b/ext/standard/tests/url/parse_url_basic_005.phpt +@@ -68,7 +68,7 @@ echo "Done"; + --> http://secret:@www.php.net/index.php?test=1&test2=char&test3=mixesCI#some_page_ref123 : string(6) "secret" + --> http://:hideout@www.php.net:80/index.php?test=1&test2=char&test3=mixesCI#some_page_ref123 : string(0) "" + --> http://secret:hideout@www.php.net/index.php?test=1&test2=char&test3=mixesCI#some_page_ref123 : string(6) "secret" +---> http://secret@hideout@www.php.net:80/index.php?test=1&test2=char&test3=mixesCI#some_page_ref123 : string(14) "secret@hideout" ++--> http://secret@hideout@www.php.net:80/index.php?test=1&test2=char&test3=mixesCI#some_page_ref123 : NULL + --> http://secret:hid:out@www.php.net:80/index.php?test=1&test2=char&test3=mixesCI#some_page_ref123 : string(6) "secret" + --> nntp://news.php.net : NULL + --> ftp://ftp.gnu.org/gnu/glic/glibc.tar.gz : NULL +diff --git a/ext/standard/url.c b/ext/standard/url.c +index a9cc06b1c0..3bb62c7da3 100644 +--- a/ext/standard/url.c ++++ b/ext/standard/url.c +@@ -92,6 +92,22 @@ PHPAPI php_url *php_url_parse(char const *str) + return php_url_parse_ex(str, strlen(str)); + } + ++static int is_userinfo_valid(const char *str, size_t len) ++{ ++ char *valid = "-._~!$&'()*+,;=:"; ++ char *p = str; ++ while (p - str < len) { ++ if (isalpha(*p) || isdigit(*p) || strchr(valid, *p)) { ++ p++; ++ } else if (*p == '%' && p - str <= len - 3 && isdigit(*(p+1)) && isxdigit(*(p+2))) { ++ p += 3; ++ } else { ++ return 0; ++ } ++ } ++ return 1; ++} ++ + /* {{{ php_url_parse + */ + PHPAPI php_url *php_url_parse_ex(char const *str, size_t length) +@@ -235,13 +251,18 @@ PHPAPI php_url *php_url_parse_ex(char const *str, size_t length) + ret->pass = estrndup(pp, (p-pp)); + php_replace_controlchars_ex(ret->pass, (p-pp)); + } else { ++ if (!is_userinfo_valid(s, p-s)) { ++ goto check_port; ++ } + ret->user = estrndup(s, (p-s)); + php_replace_controlchars_ex(ret->user, (p-s)); ++ + } + + s = p + 1; + } + ++check_port: + /* check for port */ + if (s < ue && *s == '[' && *(e-1) == ']') { + /* Short circuit portscan, +-- +2.29.2 + +From 745ba68440670440bdddd6cfb7e0f02eacef0f29 Mon Sep 17 00:00:00 2001 +From: Remi Collet <remi@remirepo.net> +Date: Mon, 4 Jan 2021 14:20:55 +0100 +Subject: [PATCH 2/2] NEWS + +(cherry picked from commit c784479182b92b9b3b96a7be42aa86a6c6d0b693) +--- + NEWS | 6 ++++++ + 1 file changed, 6 insertions(+) + +diff --git a/NEWS b/NEWS +index 47848d24b7..e328fd39c0 100644 +--- a/NEWS ++++ b/NEWS +@@ -1,6 +1,12 @@ + PHP NEWS + ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| + ++Backported from 7.3.26 ++ ++- Standard: ++ . Fixed bug #77423 (FILTER_VALIDATE_URL accepts URLs with invalid userinfo). ++ (CVE-2020-7071) (cmb) ++ + Backported from 7.2.34 + + - Core: +-- +2.29.2 + +From efb6c49f08314aca84733b0e83d72cd20c8e0015 Mon Sep 17 00:00:00 2001 +From: "Christoph M. Becker" <cmbecker69@gmx.de> +Date: Tue, 19 Jan 2021 11:23:25 +0100 +Subject: [PATCH] Alternative fix for bug 77423 + +That bug report originally was about `parse_url()` misbehaving, but the +security aspect was actually only regarding `FILTER_VALIDATE_URL`. +Since the changes to `parse_url_ex()` apparently affect userland code +which is relying on the sloppy URL parsing[1], this alternative +restores the old parsing behavior, but ensures that the userinfo is +checked for correctness for `FILTER_VALIDATE_URL`. + +[1] <https://github.com/php/php-src/commit/5174de7cd33c3d4fa591c9c93859ff9989b07e8c#commitcomment-45967652> + +(cherry picked from commit 4a89e726bd4d0571991dc22a9a1ad4509e8fe347) +(cherry picked from commit 9c673083cd46ee2a954a62156acbe4b6e657c048) +(cherry picked from commit 356f7008f36da60ec9794d48c55d117f1dd31903) +(cherry picked from commit b5d4f109bab648c0d07273d2a52a5f2560e7832b) +--- + ext/filter/logical_filters.c | 25 +++++++++++++++++++ + .../tests/url => filter/tests}/bug77423.phpt | 15 ----------- + ext/standard/tests/strings/url_t.phpt | 6 +++-- + .../tests/url/parse_url_basic_001.phpt | 6 +++-- + .../tests/url/parse_url_basic_003.phpt | 2 +- + .../tests/url/parse_url_basic_005.phpt | 2 +- + ext/standard/url.c | 21 ---------------- + 7 files changed, 35 insertions(+), 42 deletions(-) + rename ext/{standard/tests/url => filter/tests}/bug77423.phpt (53%) + +diff --git a/ext/filter/logical_filters.c b/ext/filter/logical_filters.c +index a0fed76fce..22868fd8c1 100644 +--- a/ext/filter/logical_filters.c ++++ b/ext/filter/logical_filters.c +@@ -514,6 +514,24 @@ void php_filter_validate_domain(PHP_INPUT_FILTER_PARAM_DECL) /* {{{ */ + } + /* }}} */ + ++static int is_userinfo_valid(char *str) ++{ ++ const char *valid = "-._~!$&'()*+,;=:"; ++ const char *p = str; ++ size_t len = strlen(str); ++ ++ while (p - str < len) { ++ if (isalpha(*p) || isdigit(*p) || strchr(valid, *p)) { ++ p++; ++ } else if (*p == '%' && p - str <= len - 3 && isdigit(*(p+1)) && isxdigit(*(p+2))) { ++ p += 3; ++ } else { ++ return 0; ++ } ++ } ++ return 1; ++} ++ + void php_filter_validate_url(PHP_INPUT_FILTER_PARAM_DECL) /* {{{ */ + { + php_url *url; +@@ -568,6 +586,13 @@ bad_url: + php_url_free(url); + RETURN_VALIDATION_FAILED + } ++ ++ if (url->user != NULL && !is_userinfo_valid(url->user)) { ++ php_url_free(url); ++ RETURN_VALIDATION_FAILED ++ ++ } ++ + php_url_free(url); + } + /* }}} */ +diff --git a/ext/standard/tests/url/bug77423.phpt b/ext/filter/tests/bug77423.phpt +similarity index 53% +rename from ext/standard/tests/url/bug77423.phpt +rename to ext/filter/tests/bug77423.phpt +index be03fe95e2..761c7c359a 100644 +--- a/ext/standard/tests/url/bug77423.phpt ++++ b/ext/filter/tests/bug77423.phpt +@@ -8,23 +8,8 @@ $urls = array( + ); + foreach ($urls as $url) { + var_dump(filter_var($url, FILTER_VALIDATE_URL)); +- var_dump(parse_url($url)); + } + ?> + --EXPECT-- + bool(false) +-array(3) { +- ["scheme"]=> +- string(4) "http" +- ["host"]=> +- string(19) "php.net\@aliyun.com" +- ["path"]=> +- string(7) "/aaa.do" +-} + bool(false) +-array(2) { +- ["scheme"]=> +- string(5) "https" +- ["host"]=> +- string(26) "example.com\uFF03@bing.com" +-} +diff --git a/ext/standard/tests/strings/url_t.phpt b/ext/standard/tests/strings/url_t.phpt +index 80e164a08e..e172061ec2 100644 +--- a/ext/standard/tests/strings/url_t.phpt ++++ b/ext/standard/tests/strings/url_t.phpt +@@ -575,13 +575,15 @@ $sample_urls = array ( + string(16) "some_page_ref123" + } + +---> http://secret@hideout@www.php.net:80/index.php?test=1&test2=char&test3=mixesCI#some_page_ref123: array(6) { ++--> http://secret@hideout@www.php.net:80/index.php?test=1&test2=char&test3=mixesCI#some_page_ref123: array(7) { + ["scheme"]=> + string(4) "http" + ["host"]=> +- string(26) "secret@hideout@www.php.net" ++ string(11) "www.php.net" + ["port"]=> + int(80) ++ ["user"]=> ++ string(14) "secret@hideout" + ["path"]=> + string(10) "/index.php" + ["query"]=> +diff --git a/ext/standard/tests/url/parse_url_basic_001.phpt b/ext/standard/tests/url/parse_url_basic_001.phpt +index c9e9d32de0..e468066a42 100644 +--- a/ext/standard/tests/url/parse_url_basic_001.phpt ++++ b/ext/standard/tests/url/parse_url_basic_001.phpt +@@ -507,13 +507,15 @@ echo "Done"; + string(16) "some_page_ref123" + } + +---> http://secret@hideout@www.php.net:80/index.php?test=1&test2=char&test3=mixesCI#some_page_ref123: array(6) { ++--> http://secret@hideout@www.php.net:80/index.php?test=1&test2=char&test3=mixesCI#some_page_ref123: array(7) { + ["scheme"]=> + string(4) "http" + ["host"]=> +- string(26) "secret@hideout@www.php.net" ++ string(11) "www.php.net" + ["port"]=> + int(80) ++ ["user"]=> ++ string(14) "secret@hideout" + ["path"]=> + string(10) "/index.php" + ["query"]=> +diff --git a/ext/standard/tests/url/parse_url_basic_003.phpt b/ext/standard/tests/url/parse_url_basic_003.phpt +index 431de27009..70dc4bb90b 100644 +--- a/ext/standard/tests/url/parse_url_basic_003.phpt ++++ b/ext/standard/tests/url/parse_url_basic_003.phpt +@@ -68,7 +68,7 @@ echo "Done"; + --> http://secret:@www.php.net/index.php?test=1&test2=char&test3=mixesCI#some_page_ref123 : string(11) "www.php.net" + --> http://:hideout@www.php.net:80/index.php?test=1&test2=char&test3=mixesCI#some_page_ref123 : string(11) "www.php.net" + --> http://secret:hideout@www.php.net/index.php?test=1&test2=char&test3=mixesCI#some_page_ref123 : string(11) "www.php.net" +---> http://secret@hideout@www.php.net:80/index.php?test=1&test2=char&test3=mixesCI#some_page_ref123 : string(26) "secret@hideout@www.php.net" ++--> http://secret@hideout@www.php.net:80/index.php?test=1&test2=char&test3=mixesCI#some_page_ref123 : string(11) "www.php.net" + --> http://secret:hid:out@www.php.net:80/index.php?test=1&test2=char&test3=mixesCI#some_page_ref123 : string(11) "www.php.net" + --> nntp://news.php.net : string(12) "news.php.net" + --> ftp://ftp.gnu.org/gnu/glic/glibc.tar.gz : string(11) "ftp.gnu.org" +diff --git a/ext/standard/tests/url/parse_url_basic_005.phpt b/ext/standard/tests/url/parse_url_basic_005.phpt +index b2c1a1d6dd..b2ca06ff96 100644 +--- a/ext/standard/tests/url/parse_url_basic_005.phpt ++++ b/ext/standard/tests/url/parse_url_basic_005.phpt +@@ -68,7 +68,7 @@ echo "Done"; + --> http://secret:@www.php.net/index.php?test=1&test2=char&test3=mixesCI#some_page_ref123 : string(6) "secret" + --> http://:hideout@www.php.net:80/index.php?test=1&test2=char&test3=mixesCI#some_page_ref123 : string(0) "" + --> http://secret:hideout@www.php.net/index.php?test=1&test2=char&test3=mixesCI#some_page_ref123 : string(6) "secret" +---> http://secret@hideout@www.php.net:80/index.php?test=1&test2=char&test3=mixesCI#some_page_ref123 : NULL ++--> http://secret@hideout@www.php.net:80/index.php?test=1&test2=char&test3=mixesCI#some_page_ref123 : string(14) "secret@hideout" + --> http://secret:hid:out@www.php.net:80/index.php?test=1&test2=char&test3=mixesCI#some_page_ref123 : string(6) "secret" + --> nntp://news.php.net : NULL + --> ftp://ftp.gnu.org/gnu/glic/glibc.tar.gz : NULL +diff --git a/ext/standard/url.c b/ext/standard/url.c +index 3bb62c7da3..a9cc06b1c0 100644 +--- a/ext/standard/url.c ++++ b/ext/standard/url.c +@@ -92,22 +92,6 @@ PHPAPI php_url *php_url_parse(char const *str) + return php_url_parse_ex(str, strlen(str)); + } + +-static int is_userinfo_valid(const char *str, size_t len) +-{ +- char *valid = "-._~!$&'()*+,;=:"; +- char *p = str; +- while (p - str < len) { +- if (isalpha(*p) || isdigit(*p) || strchr(valid, *p)) { +- p++; +- } else if (*p == '%' && p - str <= len - 3 && isdigit(*(p+1)) && isxdigit(*(p+2))) { +- p += 3; +- } else { +- return 0; +- } +- } +- return 1; +-} +- + /* {{{ php_url_parse + */ + PHPAPI php_url *php_url_parse_ex(char const *str, size_t length) +@@ -251,18 +235,13 @@ PHPAPI php_url *php_url_parse_ex(char const *str, size_t length) + ret->pass = estrndup(pp, (p-pp)); + php_replace_controlchars_ex(ret->pass, (p-pp)); + } else { +- if (!is_userinfo_valid(s, p-s)) { +- goto check_port; +- } + ret->user = estrndup(s, (p-s)); + php_replace_controlchars_ex(ret->user, (p-s)); +- + } + + s = p + 1; + } + +-check_port: + /* check for port */ + if (s < ue && *s == '[' && *(e-1) == ']') { + /* Short circuit portscan, +-- +2.29.2 + diff --git a/php-bug79699.patch b/php-bug79699.patch new file mode 100644 index 0000000..b37cbbf --- /dev/null +++ b/php-bug79699.patch @@ -0,0 +1,142 @@ +From 33a0a05b0995907eb1b2b922676ab765ac6fcac2 Mon Sep 17 00:00:00 2001 +From: Stanislav Malyshev <stas@php.net> +Date: Sun, 20 Sep 2020 18:08:55 -0700 +Subject: [PATCH] Do not decode cookie names anymore + +(cherry picked from commit 6559fe912661ca5ce5f0eeeb591d928451428ed0) +--- + main/php_variables.c | 8 ++++++-- + tests/basic/022.phpt | 10 +++++++--- + tests/basic/023.phpt | 4 +++- + tests/basic/bug79699.phpt | 22 ++++++++++++++++++++++ + 4 files changed, 38 insertions(+), 6 deletions(-) + create mode 100644 tests/basic/bug79699.phpt + +diff --git a/main/php_variables.c b/main/php_variables.c +index d3cfb7f737..50ecc663bd 100644 +--- a/main/php_variables.c ++++ b/main/php_variables.c +@@ -464,7 +464,9 @@ SAPI_API SAPI_TREAT_DATA_FUNC(php_default_treat_data) + size_t new_val_len; + + *val++ = '\0'; +- php_url_decode(var, strlen(var)); ++ if (arg != PARSE_COOKIE) { ++ php_url_decode(var, strlen(var)); ++ } + val_len = php_url_decode(val, strlen(val)); + val = estrndup(val, val_len); + if (sapi_module.input_filter(arg, var, &val, val_len, &new_val_len)) { +@@ -475,7 +477,9 @@ SAPI_API SAPI_TREAT_DATA_FUNC(php_default_treat_data) + size_t val_len; + size_t new_val_len; + +- php_url_decode(var, strlen(var)); ++ if (arg != PARSE_COOKIE) { ++ php_url_decode(var, strlen(var)); ++ } + val_len = 0; + val = estrndup("", val_len); + if (sapi_module.input_filter(arg, var, &val, val_len, &new_val_len)) { +diff --git a/tests/basic/022.phpt b/tests/basic/022.phpt +index 0ab70d4be7..bd1db13701 100644 +--- a/tests/basic/022.phpt ++++ b/tests/basic/022.phpt +@@ -10,7 +10,7 @@ cookie1=val1 ; cookie2=val2%20; cookie3=val 3.; cookie 4= value 4 %3B; cookie1= + var_dump($_COOKIE); + ?> + --EXPECT-- +-array(10) { ++array(12) { + ["cookie1"]=> + string(6) "val1 " + ["cookie2"]=> +@@ -19,11 +19,15 @@ array(10) { + string(6) "val 3." + ["cookie_4"]=> + string(10) " value 4 ;" ++ ["%20cookie1"]=> ++ string(6) "ignore" ++ ["+cookie1"]=> ++ string(6) "ignore" + ["cookie__5"]=> + string(7) " value" +- ["cookie_6"]=> ++ ["cookie%206"]=> + string(3) "þæö" +- ["cookie_7"]=> ++ ["cookie+7"]=> + string(0) "" + ["$cookie_8"]=> + string(0) "" +diff --git a/tests/basic/023.phpt b/tests/basic/023.phpt +index ca5f1dcfbb..0e2e0ac669 100644 +--- a/tests/basic/023.phpt ++++ b/tests/basic/023.phpt +@@ -10,9 +10,11 @@ c o o k i e=value; c o o k i e= v a l u e ;;c%20o+o k+i%20e=v;name="value","valu + var_dump($_COOKIE); + ?> + --EXPECT-- +-array(3) { ++array(4) { + ["c_o_o_k_i_e"]=> + string(5) "value" ++ ["c%20o+o_k+i%20e"]=> ++ string(1) "v" + ["name"]=> + string(24) ""value","value",UEhQIQ==" + ["UEhQIQ"]=> +diff --git a/tests/basic/bug79699.phpt b/tests/basic/bug79699.phpt +new file mode 100644 +index 0000000000..fc3d3fedb0 +--- /dev/null ++++ b/tests/basic/bug79699.phpt +@@ -0,0 +1,22 @@ ++--TEST-- ++Cookies Security Bug ++--INI-- ++max_input_vars=1000 ++filter.default=unsafe_raw ++--COOKIE-- ++__%48ost-evil=evil; __Host-evil=good; %66oo=baz;foo=bar ++--FILE-- ++<?php ++var_dump($_COOKIE); ++?> ++--EXPECT-- ++array(4) { ++ ["__%48ost-evil"]=> ++ string(4) "evil" ++ ["__Host-evil"]=> ++ string(4) "good" ++ ["%66oo"]=> ++ string(3) "baz" ++ ["foo"]=> ++ string(3) "bar" ++} +From 4248ab3d8ef089f23b93cdf979ce7a5690f8bf9d Mon Sep 17 00:00:00 2001 +From: Remi Collet <remi@remirepo.net> +Date: Tue, 29 Sep 2020 09:11:38 +0200 +Subject: [PATCH] NEWS + +--- + NEWS | 6 ++++++ + 1 file changed, 6 insertions(+) + +diff --git a/NEWS b/NEWS +index d826960c11..47848d24b7 100644 +--- a/NEWS ++++ b/NEWS +@@ -1,6 +1,12 @@ + PHP NEWS + ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| + ++Backported from 7.2.34 ++ ++- Core: ++ . Fixed bug #79699 (PHP parses encoded cookie names so malicious `__Host-` ++ cookies can be sent). (CVE-2020-7070) (Stas) ++ + Backported from 7.2.33 + + - Core: diff --git a/php-bug79971.patch b/php-bug79971.patch new file mode 100644 index 0000000..28e7889 --- /dev/null +++ b/php-bug79971.patch @@ -0,0 +1,208 @@ +From 9e9a4876bb9cafe4d4ef20a469ffd4124d8f0ef1 Mon Sep 17 00:00:00 2001 +From: "Christoph M. Becker" <cmbecker69@gmx.de> +Date: Tue, 1 Sep 2020 10:04:28 +0200 +Subject: [PATCH 1/3] Fix #79971: special character is breaking the path in xml + function + +The libxml based XML functions accepting a filename actually accept +URIs with possibly percent-encoded characters. Percent-encoded NUL +bytes lead to truncation, like non-encoded NUL bytes would. We catch +those, and let the functions fail with a respective warning. + +(cherry picked from commit f15f8fc573eb38c3c73e23e0930063a6f6409ed4) +--- + ext/dom/domimplementation.c | 5 +++++ + ext/dom/tests/bug79971_2.phpt | 20 ++++++++++++++++++++ + ext/libxml/libxml.c | 9 +++++++++ + ext/simplexml/tests/bug79971_1.phpt | 27 +++++++++++++++++++++++++++ + ext/simplexml/tests/bug79971_1.xml | 2 ++ + 5 files changed, 63 insertions(+) + create mode 100644 ext/dom/tests/bug79971_2.phpt + create mode 100644 ext/simplexml/tests/bug79971_1.phpt + create mode 100644 ext/simplexml/tests/bug79971_1.xml + +diff --git a/ext/dom/domimplementation.c b/ext/dom/domimplementation.c +index ee050e21fd..486a49d52b 100644 +--- a/ext/dom/domimplementation.c ++++ b/ext/dom/domimplementation.c +@@ -114,6 +114,11 @@ PHP_METHOD(domimplementation, createDocumentType) + pch2 = (xmlChar *) systemid; + } + ++ if (strstr(name, "%00")) { ++ php_error_docref(NULL, E_WARNING, "URI must not contain percent-encoded NUL bytes"); ++ RETURN_FALSE; ++ } ++ + uri = xmlParseURI(name); + if (uri != NULL && uri->opaque != NULL) { + localname = xmlStrdup((xmlChar *) uri->opaque); +diff --git a/ext/dom/tests/bug79971_2.phpt b/ext/dom/tests/bug79971_2.phpt +new file mode 100644 +index 0000000000..c4e6b1e4e0 +--- /dev/null ++++ b/ext/dom/tests/bug79971_2.phpt +@@ -0,0 +1,20 @@ ++--TEST-- ++Bug #79971 (special character is breaking the path in xml function) ++--SKIPIF-- ++<?php ++if (!extension_loaded('dom')) die('skip dom extension not available'); ++?> ++--FILE-- ++<?php ++$imp = new DOMImplementation; ++if (PHP_OS_FAMILY === 'Windows') { ++ $path = '/' . str_replace('\\', '/', __DIR__); ++} else { ++ $path = __DIR__; ++} ++$uri = "file://$path/bug79971_2.xml"; ++var_dump($imp->createDocumentType("$uri%00foo")); ++?> ++--EXPECTF-- ++Warning: DOMImplementation::createDocumentType(): URI must not contain percent-encoded NUL bytes in %s on line %d ++bool(false) +diff --git a/ext/libxml/libxml.c b/ext/libxml/libxml.c +index da30004f36..f481353683 100644 +--- a/ext/libxml/libxml.c ++++ b/ext/libxml/libxml.c +@@ -308,6 +308,10 @@ static void *php_libxml_streams_IO_open_wrapper(const char *filename, const char + int isescaped=0; + xmlURI *uri; + ++ if (strstr(filename, "%00")) { ++ php_error_docref(NULL, E_WARNING, "URI must not contain percent-encoded NUL bytes"); ++ return NULL; ++ } + + uri = xmlParseURI(filename); + if (uri && (uri->scheme == NULL || +@@ -438,6 +442,11 @@ php_libxml_output_buffer_create_filename(const char *URI, + if (URI == NULL) + return(NULL); + ++ if (strstr(URI, "%00")) { ++ php_error_docref(NULL, E_WARNING, "URI must not contain percent-encoded NUL bytes"); ++ return NULL; ++ } ++ + puri = xmlParseURI(URI); + if (puri != NULL) { + if (puri->scheme != NULL) +diff --git a/ext/simplexml/tests/bug79971_1.phpt b/ext/simplexml/tests/bug79971_1.phpt +new file mode 100644 +index 0000000000..197776d82d +--- /dev/null ++++ b/ext/simplexml/tests/bug79971_1.phpt +@@ -0,0 +1,27 @@ ++--TEST-- ++Bug #79971 (special character is breaking the path in xml function) ++--SKIPIF-- ++<?php ++if (!extension_loaded('simplexml')) die('skip simplexml extension not available'); ++?> ++--FILE-- ++<?php ++if (PHP_OS_FAMILY === 'Windows') { ++ $path = '/' . str_replace('\\', '/', __DIR__); ++} else { ++ $path = __DIR__; ++} ++$uri = "file://$path/bug79971_1.xml"; ++var_dump(simplexml_load_file("$uri%00foo")); ++ ++$sxe = simplexml_load_file($uri); ++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 ++bool(false) ++ ++Warning: SimpleXMLElement::asXML(): URI must not contain percent-encoded NUL bytes in %s on line %d ++bool(false) +diff --git a/ext/simplexml/tests/bug79971_1.xml b/ext/simplexml/tests/bug79971_1.xml +new file mode 100644 +index 0000000000..912bb76d9d +--- /dev/null ++++ b/ext/simplexml/tests/bug79971_1.xml +@@ -0,0 +1,2 @@ ++<?xml version="1.0"?> ++<root></root> +-- +2.31.1 + +From 927082f30d8bfb1434df25494e804bdc3d13ca5b Mon Sep 17 00:00:00 2001 +From: Remi Collet <remi@remirepo.net> +Date: Mon, 15 Nov 2021 09:05:33 +0100 +Subject: [PATCH 2/3] NEWS + +(cherry picked from commit c032381da0bfb6457aa9cfa7a430790f6eab8178) +--- + NEWS | 8 +++++++- + 1 file changed, 7 insertions(+), 1 deletion(-) + +diff --git a/NEWS b/NEWS +index fe2c75f2cf..0207f4caed 100644 +--- a/NEWS ++++ b/NEWS +@@ -1,7 +1,13 @@ + PHP NEWS + ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| + +-Backported from 7.4.25 ++Backported from 7.3.33 ++ ++- XML: ++ . Fix #79971: special character is breaking the path in xml function. ++ (CVE-2021-21707) (cmb) ++ ++Backported from 7.3.32 + + - FPM: + . Fixed bug #81026 (PHP-FPM oob R/W in root process leading to privilege +-- +2.31.1 + +From 271e8b9203ba752de436cb090e3fe8f27c792de4 Mon Sep 17 00:00:00 2001 +From: Remi Collet <remi@remirepo.net> +Date: Mon, 15 Nov 2021 09:57:10 +0100 +Subject: [PATCH 3/3] fix new tests + +(cherry picked from commit b21524ff3db15da5a7779cba73e3774eb5404d40) +--- + ext/dom/tests/bug79971_2.phpt | 2 +- + ext/simplexml/tests/bug79971_1.phpt | 2 +- + 2 files changed, 2 insertions(+), 2 deletions(-) + +diff --git a/ext/dom/tests/bug79971_2.phpt b/ext/dom/tests/bug79971_2.phpt +index c4e6b1e4e0..01cd123541 100644 +--- a/ext/dom/tests/bug79971_2.phpt ++++ b/ext/dom/tests/bug79971_2.phpt +@@ -7,7 +7,7 @@ if (!extension_loaded('dom')) die('skip dom extension not available'); + --FILE-- + <?php + $imp = new DOMImplementation; +-if (PHP_OS_FAMILY === 'Windows') { ++if (DIRECTORY_SEPARATOR !== '/') { + $path = '/' . str_replace('\\', '/', __DIR__); + } else { + $path = __DIR__; +diff --git a/ext/simplexml/tests/bug79971_1.phpt b/ext/simplexml/tests/bug79971_1.phpt +index 197776d82d..464112c99e 100644 +--- a/ext/simplexml/tests/bug79971_1.phpt ++++ b/ext/simplexml/tests/bug79971_1.phpt +@@ -6,7 +6,7 @@ if (!extension_loaded('simplexml')) die('skip simplexml extension not available' + ?> + --FILE-- + <?php +-if (PHP_OS_FAMILY === 'Windows') { ++if (DIRECTORY_SEPARATOR !== '/') { + $path = '/' . str_replace('\\', '/', __DIR__); + } else { + $path = __DIR__; +-- +2.31.1 + diff --git a/php-bug80672.patch b/php-bug80672.patch new file mode 100644 index 0000000..cfc39d6 --- /dev/null +++ b/php-bug80672.patch @@ -0,0 +1,239 @@ +From 59fbaa328950cc73b47aaa975b53dc8ca423a440 Mon Sep 17 00:00:00 2001 +From: Stanislav Malyshev <stas@php.net> +Date: Sun, 31 Jan 2021 21:15:23 -0800 +Subject: [PATCH 1/2] Fix bug #80672 - Null Dereference in SoapClient + +(cherry picked from commit 3c939e3f69955d087e0bb671868f7267dfb2a502) +(cherry picked from commit f1e2cfa008d1596251968d13eb9a8539dba6879f) +--- + NEWS | 5 +++++ + ext/soap/php_sdl.c | 26 ++++++++++++++------------ + ext/soap/php_xml.c | 4 ++-- + ext/soap/tests/bug80672.phpt | 15 +++++++++++++++ + ext/soap/tests/bug80672.xml | 6 ++++++ + 5 files changed, 42 insertions(+), 14 deletions(-) + create mode 100644 ext/soap/tests/bug80672.phpt + create mode 100644 ext/soap/tests/bug80672.xml + +diff --git a/NEWS b/NEWS +index e328fd39c0..fe5564de15 100644 +--- a/NEWS ++++ b/NEWS +@@ -1,6 +1,11 @@ + PHP NEWS + ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| + ++Backported from 7.3.27 ++ ++- SOAP: ++ . Fixed bug #80672 (Null Dereference in SoapClient). (CVE-2021-21702) (cmb, Stas) ++ + Backported from 7.3.26 + + - Standard: +diff --git a/ext/soap/php_sdl.c b/ext/soap/php_sdl.c +index c53fa8a758..c15b7b4323 100644 +--- a/ext/soap/php_sdl.c ++++ b/ext/soap/php_sdl.c +@@ -314,6 +314,8 @@ void sdl_restore_uri_credentials(sdlCtx *ctx) + ctx->context = NULL; + } + ++#define SAFE_STR(a) ((a)?a:"") ++ + static void load_wsdl_ex(zval *this_ptr, char *struri, sdlCtx *ctx, int include) + { + sdlPtr tmpsdl = ctx->sdl; +@@ -375,7 +377,7 @@ static void load_wsdl_ex(zval *this_ptr, char *struri, sdlCtx *ctx, int include) + if (node_is_equal_ex(trav2, "schema", XSD_NAMESPACE)) { + load_schema(ctx, trav2); + } else if (is_wsdl_element(trav2) && !node_is_equal(trav2,"documentation")) { +- soap_error1(E_ERROR, "Parsing WSDL: Unexpected WSDL element <%s>", trav2->name); ++ soap_error1(E_ERROR, "Parsing WSDL: Unexpected WSDL element <%s>", SAFE_STR(trav2->name)); + } + trav2 = trav2->next; + } +@@ -436,7 +438,7 @@ static void load_wsdl_ex(zval *this_ptr, char *struri, sdlCtx *ctx, int include) + soap_error0(E_ERROR, "Parsing WSDL: <service> has no name attribute"); + } + } else if (!node_is_equal(trav,"documentation")) { +- soap_error1(E_ERROR, "Parsing WSDL: Unexpected WSDL element <%s>", trav->name); ++ soap_error1(E_ERROR, "Parsing WSDL: Unexpected WSDL element <%s>", SAFE_STR(trav->name)); + } + trav = trav->next; + } +@@ -546,7 +548,7 @@ static sdlSoapBindingFunctionHeaderPtr wsdl_soap_binding_header(sdlCtx* ctx, xml + } + smart_str_free(&key); + } else if (is_wsdl_element(trav) && !node_is_equal(trav,"documentation")) { +- soap_error1(E_ERROR, "Parsing WSDL: Unexpected WSDL element <%s>", trav->name); ++ soap_error1(E_ERROR, "Parsing WSDL: Unexpected WSDL element <%s>", SAFE_STR(trav->name)); + } + trav = trav->next; + } +@@ -648,7 +650,7 @@ static void wsdl_soap_binding_body(sdlCtx* ctx, xmlNodePtr node, char* wsdl_soap + } + smart_str_free(&key); + } else if (is_wsdl_element(trav) && !node_is_equal(trav,"documentation")) { +- soap_error1(E_ERROR, "Parsing WSDL: Unexpected WSDL element <%s>", trav->name); ++ soap_error1(E_ERROR, "Parsing WSDL: Unexpected WSDL element <%s>", SAFE_STR(trav->name)); + } + trav = trav->next; + } +@@ -680,14 +682,14 @@ static HashTable* wsdl_message(sdlCtx *ctx, xmlChar* message_name) + sdlParamPtr param; + + if (trav->ns != NULL && strcmp((char*)trav->ns->href, WSDL_NAMESPACE) != 0) { +- soap_error1(E_ERROR, "Parsing WSDL: Unexpected extensibility element <%s>", trav->name); ++ soap_error1(E_ERROR, "Parsing WSDL: Unexpected extensibility element <%s>", SAFE_STR(trav->name)); + } + if (node_is_equal(trav,"documentation")) { + trav = trav->next; + continue; + } + if (!node_is_equal(trav,"part")) { +- soap_error1(E_ERROR, "Parsing WSDL: Unexpected WSDL element <%s>", trav->name); ++ soap_error1(E_ERROR, "Parsing WSDL: Unexpected WSDL element <%s>", SAFE_STR(trav->name)); + } + part = trav; + param = emalloc(sizeof(sdlParam)); +@@ -696,7 +698,7 @@ static HashTable* wsdl_message(sdlCtx *ctx, xmlChar* message_name) + + name = get_attribute(part->properties, "name"); + if (name == NULL) { +- soap_error1(E_ERROR, "Parsing WSDL: No name associated with <part> '%s'", message->name); ++ soap_error1(E_ERROR, "Parsing WSDL: No name associated with <part> '%s'", SAFE_STR(message->name)); + } + + param->paramName = estrdup((char*)name->children->content); +@@ -765,7 +767,7 @@ static sdlPtr load_wsdl(zval *this_ptr, char *struri) + continue; + } + if (!node_is_equal(trav,"port")) { +- soap_error1(E_ERROR, "Parsing WSDL: Unexpected WSDL element <%s>", trav->name); ++ soap_error1(E_ERROR, "Parsing WSDL: Unexpected WSDL element <%s>", SAFE_STR(trav->name)); + } + + port = trav; +@@ -804,7 +806,7 @@ static sdlPtr load_wsdl(zval *this_ptr, char *struri) + } + } + if (trav2 != address && is_wsdl_element(trav2) && !node_is_equal(trav2,"documentation")) { +- soap_error1(E_ERROR, "Parsing WSDL: Unexpected WSDL element <%s>", trav2->name); ++ soap_error1(E_ERROR, "Parsing WSDL: Unexpected WSDL element <%s>", SAFE_STR(trav2->name)); + } + trav2 = trav2->next; + } +@@ -906,7 +908,7 @@ static sdlPtr load_wsdl(zval *this_ptr, char *struri) + continue; + } + if (!node_is_equal(trav2,"operation")) { +- soap_error1(E_ERROR, "Parsing WSDL: Unexpected WSDL element <%s>", trav2->name); ++ soap_error1(E_ERROR, "Parsing WSDL: Unexpected WSDL element <%s>", SAFE_STR(trav2->name)); + } + + operation = trav2; +@@ -925,7 +927,7 @@ static sdlPtr load_wsdl(zval *this_ptr, char *struri) + !node_is_equal(trav3,"output") && + !node_is_equal(trav3,"fault") && + !node_is_equal(trav3,"documentation")) { +- soap_error1(E_ERROR, "Parsing WSDL: Unexpected WSDL element <%s>", trav3->name); ++ soap_error1(E_ERROR, "Parsing WSDL: Unexpected WSDL element <%s>", SAFE_STR(trav3->name)); + } + trav3 = trav3->next; + } +@@ -1103,7 +1105,7 @@ static sdlPtr load_wsdl(zval *this_ptr, char *struri) + } + } + } else if (is_wsdl_element(trav) && !node_is_equal(trav,"documentation")) { +- soap_error1(E_ERROR, "Parsing WSDL: Unexpected WSDL element <%s>", trav->name); ++ soap_error1(E_ERROR, "Parsing WSDL: Unexpected WSDL element <%s>", SAFE_STR(trav->name)); + } + trav = trav->next; + } +diff --git a/ext/soap/php_xml.c b/ext/soap/php_xml.c +index fb00c420a6..a9c6a56858 100644 +--- a/ext/soap/php_xml.c ++++ b/ext/soap/php_xml.c +@@ -204,7 +204,7 @@ xmlNsPtr node_find_ns(xmlNodePtr node) + + int attr_is_equal_ex(xmlAttrPtr node, char *name, char *ns) + { +- if (name == NULL || strcmp((char*)node->name, name) == 0) { ++ if (name == NULL || ((node->name) && strcmp((char*)node->name, name) == 0)) { + if (ns) { + xmlNsPtr nsPtr = attr_find_ns(node); + if (nsPtr) { +@@ -220,7 +220,7 @@ int attr_is_equal_ex(xmlAttrPtr node, char *name, char *ns) + + int node_is_equal_ex(xmlNodePtr node, char *name, char *ns) + { +- if (name == NULL || strcmp((char*)node->name, name) == 0) { ++ if (name == NULL || ((node->name) && strcmp((char*)node->name, name) == 0)) { + if (ns) { + xmlNsPtr nsPtr = node_find_ns(node); + if (nsPtr) { +diff --git a/ext/soap/tests/bug80672.phpt b/ext/soap/tests/bug80672.phpt +new file mode 100644 +index 0000000000..71e2b1d841 +--- /dev/null ++++ b/ext/soap/tests/bug80672.phpt +@@ -0,0 +1,15 @@ ++--TEST-- ++Bug #80672 Null Dereference in SoapClient ++--SKIPIF-- ++<?php require_once('skipif.inc'); ?> ++--FILE-- ++<?php ++try { ++ $client = new SoapClient(__DIR__ . "/bug80672.xml"); ++ $query = $soap->query(array('sXML' => 'something')); ++} catch(SoapFault $e) { ++ print $e->getMessage(); ++} ++?> ++--EXPECTF-- ++SOAP-ERROR: Parsing WSDL: Unexpected WSDL element <> +\ No newline at end of file +diff --git a/ext/soap/tests/bug80672.xml b/ext/soap/tests/bug80672.xml +new file mode 100644 +index 0000000000..0fa185bf1e +--- /dev/null ++++ b/ext/soap/tests/bug80672.xml +@@ -0,0 +1,6 @@ ++<?xml version="1.0" encoding="ISO-8859-1"?> ++<soap:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ++ xmlns:xsd="http://www.w3.org/2001/XMLSchema" ++ xmlns:soap="http://schemas.xmlsoap.org/wsdl/"> ++<![CDATA[test]]> ++</soap:definitions> +-- +2.29.2 + +From e031e2f5eeb29881947899378d70318bca46249c Mon Sep 17 00:00:00 2001 +From: Nikita Popov <nikita.ppv@gmail.com> +Date: Mon, 1 Feb 2021 09:46:17 +0100 +Subject: [PATCH 2/2] Fix build + +(cherry picked from commit e5d767d27f94895e09f0321562fd3774d4656164) +(cherry picked from commit 02352d5acc1896756dcb4645f54689ffdcc4ca52) +--- + ext/soap/php_sdl.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/ext/soap/php_sdl.c b/ext/soap/php_sdl.c +index c15b7b4323..4cc1ee69e7 100644 +--- a/ext/soap/php_sdl.c ++++ b/ext/soap/php_sdl.c +@@ -314,7 +314,7 @@ void sdl_restore_uri_credentials(sdlCtx *ctx) + ctx->context = NULL; + } + +-#define SAFE_STR(a) ((a)?a:"") ++#define SAFE_STR(a) ((a)?((const char *)a):"") + + static void load_wsdl_ex(zval *this_ptr, char *struri, sdlCtx *ctx, int include) + { +-- +2.29.2 + diff --git a/php-bug80710.patch b/php-bug80710.patch new file mode 100644 index 0000000..d66dd07 --- /dev/null +++ b/php-bug80710.patch @@ -0,0 +1,373 @@ +From bfa81ac72836e53a75665eb1f78a6d67489da2e3 Mon Sep 17 00:00:00 2001 +From: "Christoph M. Becker" <cmbecker69@gmx.de> +Date: Fri, 5 Feb 2021 22:51:41 +0100 +Subject: [PATCH 1/2] Fix #80710: imap_mail_compose() header injection + +Like `mail()` and `mb_send_mail()`, `imap_mail_compose()` must prevent +header injection. For maximum backward compatibility, we still allow +header folding for general headers, and still accept trailing line +breaks for address lists. + +(cherry picked from commit 37962c61d29794645ec45d45d78123382d82c2e5) +(cherry picked from commit 9017896cccefe000938f80b49361b1c183849922) +--- + ext/imap/php_imap.c | 56 ++++++++++++++++++++++++++++++++++ + ext/imap/tests/bug80710_1.phpt | 37 ++++++++++++++++++++++ + ext/imap/tests/bug80710_2.phpt | 37 ++++++++++++++++++++++ + 3 files changed, 130 insertions(+) + create mode 100644 ext/imap/tests/bug80710_1.phpt + create mode 100644 ext/imap/tests/bug80710_2.phpt + +diff --git a/ext/imap/php_imap.c b/ext/imap/php_imap.c +index 011cbc0dfd..5f8c0da79a 100644 +--- a/ext/imap/php_imap.c ++++ b/ext/imap/php_imap.c +@@ -3531,6 +3531,23 @@ PHP_FUNCTION(imap_fetch_overview) + } + /* }}} */ + ++static zend_bool header_injection(zend_string *str, zend_bool adrlist) ++{ ++ char *p = ZSTR_VAL(str); ++ ++ while ((p = strpbrk(p, "\r\n")) != NULL) { ++ if (!(p[0] == '\r' && p[1] == '\n') ++ /* adrlists do not support folding, but swallow trailing line breaks */ ++ && !((adrlist && p[1] == '\0') ++ /* other headers support folding */ ++ || !adrlist && (p[1] == ' ' || p[1] == '\t'))) { ++ return 1; ++ } ++ p++; ++ } ++ return 0; ++} ++ + /* {{{ proto string imap_mail_compose(array envelope, array body) + Create a MIME message based on given envelope and body sections */ + PHP_FUNCTION(imap_mail_compose) +@@ -3551,6 +3568,13 @@ PHP_FUNCTION(imap_mail_compose) + return; + } + ++#define CHECK_HEADER_INJECTION(zstr, adrlist, header) \ ++ if (header_injection(zstr, adrlist)) { \ ++ php_error_docref(NULL, E_WARNING, "header injection attempt in " header); \ ++ RETVAL_FALSE; \ ++ goto done; \ ++ } ++ + #define PHP_RFC822_PARSE_ADRLIST(target, value) \ + str_copy = estrndup(Z_STRVAL_P(value), Z_STRLEN_P(value)); \ + rfc822_parse_adrlist(target, str_copy, "NO HOST"); \ +@@ -3559,46 +3583,57 @@ PHP_FUNCTION(imap_mail_compose) + env = mail_newenvelope(); + if ((pvalue = zend_hash_str_find(Z_ARRVAL_P(envelope), "remail", sizeof("remail") - 1)) != NULL) { + convert_to_string_ex(pvalue); ++ CHECK_HEADER_INJECTION(Z_STR_P(pvalue), 0, "remail"); + env->remail = cpystr(Z_STRVAL_P(pvalue)); + } + if ((pvalue = zend_hash_str_find(Z_ARRVAL_P(envelope), "return_path", sizeof("return_path") - 1)) != NULL) { + convert_to_string_ex(pvalue); ++ CHECK_HEADER_INJECTION(Z_STR_P(pvalue), 1, "return_path"); + PHP_RFC822_PARSE_ADRLIST(&env->return_path, pvalue); + } + if ((pvalue = zend_hash_str_find(Z_ARRVAL_P(envelope), "date", sizeof("date") - 1)) != NULL) { + convert_to_string_ex(pvalue); ++ CHECK_HEADER_INJECTION(Z_STR_P(pvalue), 0, "date"); + env->date = (unsigned char*)cpystr(Z_STRVAL_P(pvalue)); + } + if ((pvalue = zend_hash_str_find(Z_ARRVAL_P(envelope), "from", sizeof("from") - 1)) != NULL) { + convert_to_string_ex(pvalue); ++ CHECK_HEADER_INJECTION(Z_STR_P(pvalue), 1, "from"); + PHP_RFC822_PARSE_ADRLIST(&env->from, pvalue); + } + if ((pvalue = zend_hash_str_find(Z_ARRVAL_P(envelope), "reply_to", sizeof("reply_to") - 1)) != NULL) { + convert_to_string_ex(pvalue); ++ CHECK_HEADER_INJECTION(Z_STR_P(pvalue), 1, "reply_to"); + PHP_RFC822_PARSE_ADRLIST(&env->reply_to, pvalue); + } + if ((pvalue = zend_hash_str_find(Z_ARRVAL_P(envelope), "in_reply_to", sizeof("in_reply_to") - 1)) != NULL) { + convert_to_string_ex(pvalue); ++ CHECK_HEADER_INJECTION(Z_STR_P(pvalue), 0, "in_reply_to"); + env->in_reply_to = cpystr(Z_STRVAL_P(pvalue)); + } + if ((pvalue = zend_hash_str_find(Z_ARRVAL_P(envelope), "subject", sizeof("subject") - 1)) != NULL) { + convert_to_string_ex(pvalue); ++ CHECK_HEADER_INJECTION(Z_STR_P(pvalue), 0, "subject"); + env->subject = cpystr(Z_STRVAL_P(pvalue)); + } + if ((pvalue = zend_hash_str_find(Z_ARRVAL_P(envelope), "to", sizeof("to") - 1)) != NULL) { + convert_to_string_ex(pvalue); ++ CHECK_HEADER_INJECTION(Z_STR_P(pvalue), 1, "to"); + PHP_RFC822_PARSE_ADRLIST(&env->to, pvalue); + } + if ((pvalue = zend_hash_str_find(Z_ARRVAL_P(envelope), "cc", sizeof("cc") - 1)) != NULL) { + convert_to_string_ex(pvalue); ++ CHECK_HEADER_INJECTION(Z_STR_P(pvalue), 1, "cc"); + PHP_RFC822_PARSE_ADRLIST(&env->cc, pvalue); + } + if ((pvalue = zend_hash_str_find(Z_ARRVAL_P(envelope), "bcc", sizeof("bcc") - 1)) != NULL) { + convert_to_string_ex(pvalue); ++ CHECK_HEADER_INJECTION(Z_STR_P(pvalue), 1, "bcc"); + PHP_RFC822_PARSE_ADRLIST(&env->bcc, pvalue); + } + if ((pvalue = zend_hash_str_find(Z_ARRVAL_P(envelope), "message_id", sizeof("message_id") - 1)) != NULL) { + convert_to_string_ex(pvalue); ++ CHECK_HEADER_INJECTION(Z_STR_P(pvalue), 0, "message_id"); + env->message_id=cpystr(Z_STRVAL_P(pvalue)); + } + +@@ -3608,6 +3643,7 @@ PHP_FUNCTION(imap_mail_compose) + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(pvalue), env_data) { + custom_headers_param = mail_newbody_parameter(); + convert_to_string_ex(env_data); ++ CHECK_HEADER_INJECTION(Z_STR_P(env_data), 0, "custom_headers"); + custom_headers_param->value = (char *) fs_get(Z_STRLEN_P(env_data) + 1); + custom_headers_param->attribute = NULL; + memcpy(custom_headers_param->value, Z_STRVAL_P(env_data), Z_STRLEN_P(env_data) + 1); +@@ -3640,6 +3676,7 @@ PHP_FUNCTION(imap_mail_compose) + } + if ((pvalue = zend_hash_str_find(Z_ARRVAL_P(data), "charset", sizeof("charset") - 1)) != NULL) { + convert_to_string_ex(pvalue); ++ CHECK_HEADER_INJECTION(Z_STR_P(pvalue), 0, "body charset"); + tmp_param = mail_newbody_parameter(); + tmp_param->value = cpystr(Z_STRVAL_P(pvalue)); + tmp_param->attribute = cpystr("CHARSET"); +@@ -3650,9 +3687,11 @@ PHP_FUNCTION(imap_mail_compose) + if(Z_TYPE_P(pvalue) == IS_ARRAY) { + disp_param = tmp_param = NULL; + ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(pvalue), key, disp_data) { ++ CHECK_HEADER_INJECTION(key, 0, "body disposition key"); + disp_param = mail_newbody_parameter(); + disp_param->attribute = cpystr(ZSTR_VAL(key)); + convert_to_string_ex(disp_data); ++ CHECK_HEADER_INJECTION(Z_STR_P(disp_data), 0, "body disposition value"); + disp_param->value = (char *) fs_get(Z_STRLEN_P(disp_data) + 1); + memcpy(disp_param->value, Z_STRVAL_P(disp_data), Z_STRLEN_P(disp_data) + 1); + disp_param->next = tmp_param; +@@ -3663,18 +3702,22 @@ PHP_FUNCTION(imap_mail_compose) + } + if ((pvalue = zend_hash_str_find(Z_ARRVAL_P(data), "subtype", sizeof("subtype") - 1)) != NULL) { + convert_to_string_ex(pvalue); ++ CHECK_HEADER_INJECTION(Z_STR_P(pvalue), 0, "body subtype"); + bod->subtype = cpystr(Z_STRVAL_P(pvalue)); + } + if ((pvalue = zend_hash_str_find(Z_ARRVAL_P(data), "id", sizeof("id") - 1)) != NULL) { + convert_to_string_ex(pvalue); ++ CHECK_HEADER_INJECTION(Z_STR_P(pvalue), 0, "body id"); + bod->id = cpystr(Z_STRVAL_P(pvalue)); + } + if ((pvalue = zend_hash_str_find(Z_ARRVAL_P(data), "description", sizeof("description") - 1)) != NULL) { + convert_to_string_ex(pvalue); ++ CHECK_HEADER_INJECTION(Z_STR_P(pvalue), 0, "body description"); + bod->description = cpystr(Z_STRVAL_P(pvalue)); + } + if ((pvalue = zend_hash_str_find(Z_ARRVAL_P(data), "disposition.type", sizeof("disposition.type") - 1)) != NULL) { + convert_to_string_ex(pvalue); ++ CHECK_HEADER_INJECTION(Z_STR_P(pvalue), 0, "body disposition.type"); + bod->disposition.type = (char *) fs_get(Z_STRLEN_P(pvalue) + 1); + memcpy(bod->disposition.type, Z_STRVAL_P(pvalue), Z_STRLEN_P(pvalue)+1); + } +@@ -3682,9 +3725,11 @@ PHP_FUNCTION(imap_mail_compose) + if (Z_TYPE_P(pvalue) == IS_ARRAY) { + disp_param = tmp_param = NULL; + ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(pvalue), key, disp_data) { ++ CHECK_HEADER_INJECTION(key, 0, "body type.parameters key"); + disp_param = mail_newbody_parameter(); + disp_param->attribute = cpystr(ZSTR_VAL(key)); + convert_to_string_ex(disp_data); ++ CHECK_HEADER_INJECTION(Z_STR_P(disp_data), 0, "body type.parameters value"); + disp_param->value = (char *) fs_get(Z_STRLEN_P(disp_data) + 1); + memcpy(disp_param->value, Z_STRVAL_P(disp_data), Z_STRLEN_P(disp_data) + 1); + disp_param->next = tmp_param; +@@ -3713,6 +3758,7 @@ PHP_FUNCTION(imap_mail_compose) + } + if ((pvalue = zend_hash_str_find(Z_ARRVAL_P(data), "md5", sizeof("md5") - 1)) != NULL) { + convert_to_string_ex(pvalue); ++ CHECK_HEADER_INJECTION(Z_STR_P(pvalue), 0, "body md5"); + bod->md5 = cpystr(Z_STRVAL_P(pvalue)); + } + } else if (Z_TYPE_P(data) == IS_ARRAY) { +@@ -3743,6 +3789,7 @@ PHP_FUNCTION(imap_mail_compose) + } + if ((pvalue = zend_hash_str_find(Z_ARRVAL_P(data), "charset", sizeof("charset") - 1)) != NULL) { + convert_to_string_ex(pvalue); ++ CHECK_HEADER_INJECTION(Z_STR_P(pvalue), 0, "body charset"); + tmp_param = mail_newbody_parameter(); + tmp_param->value = (char *) fs_get(Z_STRLEN_P(pvalue) + 1); + memcpy(tmp_param->value, Z_STRVAL_P(pvalue), Z_STRLEN_P(pvalue) + 1); +@@ -3754,9 +3801,11 @@ PHP_FUNCTION(imap_mail_compose) + if (Z_TYPE_P(pvalue) == IS_ARRAY) { + disp_param = tmp_param = NULL; + ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(pvalue), key, disp_data) { ++ CHECK_HEADER_INJECTION(key, 0, "body type.parameters key"); + disp_param = mail_newbody_parameter(); + disp_param->attribute = cpystr(ZSTR_VAL(key)); + convert_to_string_ex(disp_data); ++ CHECK_HEADER_INJECTION(Z_STR_P(disp_data), 0, "body type.parameters value"); + disp_param->value = (char *)fs_get(Z_STRLEN_P(disp_data) + 1); + memcpy(disp_param->value, Z_STRVAL_P(disp_data), Z_STRLEN_P(disp_data) + 1); + disp_param->next = tmp_param; +@@ -3767,18 +3816,22 @@ PHP_FUNCTION(imap_mail_compose) + } + if ((pvalue = zend_hash_str_find(Z_ARRVAL_P(data), "subtype", sizeof("subtype") - 1)) != NULL) { + convert_to_string_ex(pvalue); ++ CHECK_HEADER_INJECTION(Z_STR_P(pvalue), 0, "body subtype"); + bod->subtype = cpystr(Z_STRVAL_P(pvalue)); + } + if ((pvalue = zend_hash_str_find(Z_ARRVAL_P(data), "id", sizeof("id") - 1)) != NULL) { + convert_to_string_ex(pvalue); ++ CHECK_HEADER_INJECTION(Z_STR_P(pvalue), 0, "body id"); + bod->id = cpystr(Z_STRVAL_P(pvalue)); + } + if ((pvalue = zend_hash_str_find(Z_ARRVAL_P(data), "description", sizeof("description") - 1)) != NULL) { + convert_to_string_ex(pvalue); ++ CHECK_HEADER_INJECTION(Z_STR_P(pvalue), 0, "body description"); + bod->description = cpystr(Z_STRVAL_P(pvalue)); + } + if ((pvalue = zend_hash_str_find(Z_ARRVAL_P(data), "disposition.type", sizeof("disposition.type") - 1)) != NULL) { + convert_to_string_ex(pvalue); ++ CHECK_HEADER_INJECTION(Z_STR_P(pvalue), 0, "body disposition.type"); + bod->disposition.type = (char *) fs_get(Z_STRLEN_P(pvalue) + 1); + memcpy(bod->disposition.type, Z_STRVAL_P(pvalue), Z_STRLEN_P(pvalue)+1); + } +@@ -3786,9 +3839,11 @@ PHP_FUNCTION(imap_mail_compose) + if (Z_TYPE_P(pvalue) == IS_ARRAY) { + disp_param = tmp_param = NULL; + ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(pvalue), key, disp_data) { ++ CHECK_HEADER_INJECTION(key, 0, "body disposition key"); + disp_param = mail_newbody_parameter(); + disp_param->attribute = cpystr(ZSTR_VAL(key)); + convert_to_string_ex(disp_data); ++ CHECK_HEADER_INJECTION(Z_STR_P(disp_data), 0, "body disposition value"); + disp_param->value = (char *) fs_get(Z_STRLEN_P(disp_data) + 1); + memcpy(disp_param->value, Z_STRVAL_P(disp_data), Z_STRLEN_P(disp_data) + 1); + disp_param->next = tmp_param; +@@ -3817,6 +3872,7 @@ PHP_FUNCTION(imap_mail_compose) + } + if ((pvalue = zend_hash_str_find(Z_ARRVAL_P(data), "md5", sizeof("md5") - 1)) != NULL) { + convert_to_string_ex(pvalue); ++ CHECK_HEADER_INJECTION(Z_STR_P(pvalue), 0, "body md5"); + bod->md5 = cpystr(Z_STRVAL_P(pvalue)); + } + } +diff --git a/ext/imap/tests/bug80710_1.phpt b/ext/imap/tests/bug80710_1.phpt +new file mode 100644 +index 0000000000..5cdee03401 +--- /dev/null ++++ b/ext/imap/tests/bug80710_1.phpt +@@ -0,0 +1,37 @@ ++--TEST-- ++Bug #80710 (imap_mail_compose() header injection) - MIME Splitting Attack ++--SKIPIF-- ++<?php ++if (!extension_loaded("imap")) die("skip imap extension not available"); ++?> ++--FILE-- ++<?php ++$envelope["from"]= "joe@example.com\n From : X-INJECTED"; ++$envelope["to"] = "foo@example.com\nFrom: X-INJECTED"; ++$envelope["cc"] = "bar@example.com\nFrom: X-INJECTED"; ++$envelope["subject"] = "bar@example.com\n\n From : X-INJECTED"; ++$envelope["x-remail"] = "bar@example.com\nFrom: X-INJECTED"; ++$envelope["something"] = "bar@example.com\nFrom: X-INJECTED"; ++ ++$part1["type"] = TYPEMULTIPART; ++$part1["subtype"] = "mixed"; ++ ++$part2["type"] = TYPEAPPLICATION; ++$part2["encoding"] = ENCBINARY; ++$part2["subtype"] = "octet-stream\nContent-Type: X-INJECTED"; ++$part2["description"] = "some file\nContent-Type: X-INJECTED"; ++$part2["contents.data"] = "ABC\nContent-Type: X-INJECTED"; ++ ++$part3["type"] = TYPETEXT; ++$part3["subtype"] = "plain"; ++$part3["description"] = "description3"; ++$part3["contents.data"] = "contents.data3\n\n\n\t"; ++ ++$body[1] = $part1; ++$body[2] = $part2; ++$body[3] = $part3; ++ ++echo imap_mail_compose($envelope, $body); ++?> ++--EXPECTF-- ++Warning: imap_mail_compose(): header injection attempt in from in %s on line %d +diff --git a/ext/imap/tests/bug80710_2.phpt b/ext/imap/tests/bug80710_2.phpt +new file mode 100644 +index 0000000000..b9f2fa8544 +--- /dev/null ++++ b/ext/imap/tests/bug80710_2.phpt +@@ -0,0 +1,37 @@ ++--TEST-- ++Bug #80710 (imap_mail_compose() header injection) - Remail ++--SKIPIF-- ++<?php ++if (!extension_loaded("imap")) die("skip imap extension not available"); ++?> ++--FILE-- ++<?php ++$envelope["from"]= "joe@example.com\n From : X-INJECTED"; ++$envelope["to"] = "foo@example.com\nFrom: X-INJECTED"; ++$envelope["cc"] = "bar@example.com\nFrom: X-INJECTED"; ++$envelope["subject"] = "bar@example.com\n\n From : X-INJECTED"; ++$envelope["remail"] = "X-INJECTED-REMAIL: X-INJECTED\nFrom: X-INJECTED-REMAIL-FROM"; //<--- Injected as first hdr ++$envelope["something"] = "bar@example.com\nFrom: X-INJECTED"; ++ ++$part1["type"] = TYPEMULTIPART; ++$part1["subtype"] = "mixed"; ++ ++$part2["type"] = TYPEAPPLICATION; ++$part2["encoding"] = ENCBINARY; ++$part2["subtype"] = "octet-stream\nContent-Type: X-INJECTED"; ++$part2["description"] = "some file\nContent-Type: X-INJECTED"; ++$part2["contents.data"] = "ABC\nContent-Type: X-INJECTED"; ++ ++$part3["type"] = TYPETEXT; ++$part3["subtype"] = "plain"; ++$part3["description"] = "description3"; ++$part3["contents.data"] = "contents.data3\n\n\n\t"; ++ ++$body[1] = $part1; ++$body[2] = $part2; ++$body[3] = $part3; ++ ++echo imap_mail_compose($envelope, $body); ++?> ++--EXPECTF-- ++Warning: imap_mail_compose(): header injection attempt in remail in %s on line %d +-- +2.30.2 + +From 5a31584debadbb8e7195d768edece32b249e14bb Mon Sep 17 00:00:00 2001 +From: "Christoph M. Becker" <cmbecker69@gmx.de> +Date: Tue, 27 Apr 2021 13:38:39 +0200 +Subject: [PATCH 2/2] Add missing NEWS entry for #80710 + +(cherry picked from commit 60a68a45c3e9f63585151221e7fe9ddff78bd71f) +(cherry picked from commit f16c623ec8ae3f3cdc73ab3fa05ae6bb0a77d1f3) +--- + NEWS | 5 +++++ + 1 file changed, 5 insertions(+) + +diff --git a/NEWS b/NEWS +index fe5564de15..9eff4bd7ae 100644 +--- a/NEWS ++++ b/NEWS +@@ -1,6 +1,11 @@ + PHP NEWS + ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| + ++Backported from 7.3.28 ++ ++- Imap: ++ . Fixed bug #80710 (imap_mail_compose() header injection). (cmb, Stas) ++ + Backported from 7.3.27 + + - SOAP: +-- +2.30.2 + diff --git a/php-bug81026.patch b/php-bug81026.patch new file mode 100644 index 0000000..7338dde --- /dev/null +++ b/php-bug81026.patch @@ -0,0 +1,430 @@ +From 4699cc1b1b957c843c71a79fa816446b622d4278 Mon Sep 17 00:00:00 2001 +From: Jakub Zelenka <bukka@php.net> +Date: Sat, 2 Oct 2021 22:53:41 +0100 +Subject: [PATCH 1/2] Fix bug #81026 (PHP-FPM oob R/W in root process leading + to priv escalation) + +The main change is to store scoreboard procs directly to the variable sized +array rather than indirectly through the pointer. + +Signed-off-by: Stanislav Malyshev <stas@php.net> +(cherry picked from commit cb2021e5f69da5e2868130a05bb53db0f9f89e4b) +--- + sapi/fpm/fpm/fpm_children.c | 14 ++--- + sapi/fpm/fpm/fpm_request.c | 4 +- + sapi/fpm/fpm/fpm_scoreboard.c | 107 +++++++++++++++++++-------------- + sapi/fpm/fpm/fpm_scoreboard.h | 11 ++-- + sapi/fpm/fpm/fpm_status.c | 4 +- + sapi/fpm/fpm/fpm_worker_pool.c | 2 +- + 6 files changed, 81 insertions(+), 61 deletions(-) + +diff --git a/sapi/fpm/fpm/fpm_children.c b/sapi/fpm/fpm/fpm_children.c +index b48fa54f53..c7f97fd490 100644 +--- a/sapi/fpm/fpm/fpm_children.c ++++ b/sapi/fpm/fpm/fpm_children.c +@@ -239,7 +239,7 @@ void fpm_children_bury() /* {{{ */ + + fpm_child_unlink(child); + +- fpm_scoreboard_proc_free(wp->scoreboard, child->scoreboard_i); ++ fpm_scoreboard_proc_free(child); + + fpm_clock_get(&tv1); + +@@ -249,9 +249,9 @@ void fpm_children_bury() /* {{{ */ + if (!fpm_pctl_can_spawn_children()) { + severity = ZLOG_DEBUG; + } +- zlog(severity, "[pool %s] child %d exited %s after %ld.%06d seconds from start", child->wp->config->name, (int) pid, buf, tv2.tv_sec, (int) tv2.tv_usec); ++ zlog(severity, "[pool %s] child %d exited %s after %ld.%06d seconds from start", wp->config->name, (int) pid, buf, tv2.tv_sec, (int) tv2.tv_usec); + } else { +- zlog(ZLOG_DEBUG, "[pool %s] child %d has been killed by the process management after %ld.%06d seconds from start", child->wp->config->name, (int) pid, tv2.tv_sec, (int) tv2.tv_usec); ++ zlog(ZLOG_DEBUG, "[pool %s] child %d has been killed by the process management after %ld.%06d seconds from start", wp->config->name, (int) pid, tv2.tv_sec, (int) tv2.tv_usec); + } + + fpm_child_close(child, 1 /* in event_loop */); +@@ -317,7 +317,7 @@ static struct fpm_child_s *fpm_resources_prepare(struct fpm_worker_pool_s *wp) / + return 0; + } + +- if (0 > fpm_scoreboard_proc_alloc(wp->scoreboard, &c->scoreboard_i)) { ++ if (0 > fpm_scoreboard_proc_alloc(c)) { + fpm_stdio_discard_pipes(c); + fpm_child_free(c); + return 0; +@@ -329,7 +329,7 @@ static struct fpm_child_s *fpm_resources_prepare(struct fpm_worker_pool_s *wp) / + + static void fpm_resources_discard(struct fpm_child_s *child) /* {{{ */ + { +- fpm_scoreboard_proc_free(child->wp->scoreboard, child->scoreboard_i); ++ fpm_scoreboard_proc_free(child); + fpm_stdio_discard_pipes(child); + fpm_child_free(child); + } +@@ -342,10 +342,10 @@ static void fpm_child_resources_use(struct fpm_child_s *child) /* {{{ */ + if (wp == child->wp) { + continue; + } +- fpm_scoreboard_free(wp->scoreboard); ++ fpm_scoreboard_free(wp); + } + +- fpm_scoreboard_child_use(child->wp->scoreboard, child->scoreboard_i, getpid()); ++ fpm_scoreboard_child_use(child, getpid()); + fpm_stdio_child_use_pipes(child); + fpm_child_free(child); + } +diff --git a/sapi/fpm/fpm/fpm_request.c b/sapi/fpm/fpm/fpm_request.c +index 3f82a7d4f7..a707fd29f6 100644 +--- a/sapi/fpm/fpm/fpm_request.c ++++ b/sapi/fpm/fpm/fpm_request.c +@@ -287,7 +287,7 @@ int fpm_request_is_idle(struct fpm_child_s *child) /* {{{ */ + struct fpm_scoreboard_proc_s *proc; + + /* no need in atomicity here */ +- proc = fpm_scoreboard_proc_get(child->wp->scoreboard, child->scoreboard_i); ++ proc = fpm_scoreboard_proc_get_from_child(child); + if (!proc) { + return 0; + } +@@ -302,7 +302,7 @@ int fpm_request_last_activity(struct fpm_child_s *child, struct timeval *tv) /* + + if (!tv) return -1; + +- proc = fpm_scoreboard_proc_get(child->wp->scoreboard, child->scoreboard_i); ++ proc = fpm_scoreboard_proc_get_from_child(child); + if (!proc) { + return -1; + } +diff --git a/sapi/fpm/fpm/fpm_scoreboard.c b/sapi/fpm/fpm/fpm_scoreboard.c +index 5693ce4e49..45d44f4bbb 100644 +--- a/sapi/fpm/fpm/fpm_scoreboard.c ++++ b/sapi/fpm/fpm/fpm_scoreboard.c +@@ -8,6 +8,7 @@ + #include <time.h> + + #include "fpm_config.h" ++#include "fpm_children.h" + #include "fpm_scoreboard.h" + #include "fpm_shm.h" + #include "fpm_sockets.h" +@@ -25,7 +26,6 @@ static float fpm_scoreboard_tick; + int fpm_scoreboard_init_main() /* {{{ */ + { + struct fpm_worker_pool_s *wp; +- unsigned int i; + + #ifdef HAVE_TIMES + #if (defined(HAVE_SYSCONF) && defined(_SC_CLK_TCK)) +@@ -42,7 +42,7 @@ int fpm_scoreboard_init_main() /* {{{ */ + + + for (wp = fpm_worker_all_pools; wp; wp = wp->next) { +- size_t scoreboard_size, scoreboard_nprocs_size; ++ size_t scoreboard_procs_size; + void *shm_mem; + + if (wp->config->pm_max_children < 1) { +@@ -55,22 +55,15 @@ int fpm_scoreboard_init_main() /* {{{ */ + return -1; + } + +- scoreboard_size = sizeof(struct fpm_scoreboard_s) + (wp->config->pm_max_children) * sizeof(struct fpm_scoreboard_proc_s *); +- scoreboard_nprocs_size = sizeof(struct fpm_scoreboard_proc_s) * wp->config->pm_max_children; +- shm_mem = fpm_shm_alloc(scoreboard_size + scoreboard_nprocs_size); ++ scoreboard_procs_size = sizeof(struct fpm_scoreboard_proc_s) * wp->config->pm_max_children; ++ shm_mem = fpm_shm_alloc(sizeof(struct fpm_scoreboard_s) + scoreboard_procs_size); + + if (!shm_mem) { + return -1; + } +- wp->scoreboard = shm_mem; ++ wp->scoreboard = shm_mem; ++ wp->scoreboard->pm = wp->config->pm; + wp->scoreboard->nprocs = wp->config->pm_max_children; +- shm_mem += scoreboard_size; +- +- for (i = 0; i < wp->scoreboard->nprocs; i++, shm_mem += sizeof(struct fpm_scoreboard_proc_s)) { +- wp->scoreboard->procs[i] = shm_mem; +- } +- +- wp->scoreboard->pm = wp->config->pm; + wp->scoreboard->start_epoch = time(NULL); + strlcpy(wp->scoreboard->pool, wp->config->name, sizeof(wp->scoreboard->pool)); + } +@@ -164,28 +157,47 @@ struct fpm_scoreboard_s *fpm_scoreboard_get() /* {{{*/ + } + /* }}} */ + +-struct fpm_scoreboard_proc_s *fpm_scoreboard_proc_get(struct fpm_scoreboard_s *scoreboard, int child_index) /* {{{*/ ++static inline struct fpm_scoreboard_proc_s *fpm_scoreboard_proc_get_ex( ++ struct fpm_scoreboard_s *scoreboard, int child_index, unsigned int nprocs) /* {{{*/ + { + if (!scoreboard) { +- scoreboard = fpm_scoreboard; ++ return NULL; + } + +- if (!scoreboard) { ++ if (child_index < 0 || (unsigned int)child_index >= nprocs) { + return NULL; + } + ++ return &scoreboard->procs[child_index]; ++} ++/* }}} */ ++ ++struct fpm_scoreboard_proc_s *fpm_scoreboard_proc_get( ++ struct fpm_scoreboard_s *scoreboard, int child_index) /* {{{*/ ++{ ++ if (!scoreboard) { ++ scoreboard = fpm_scoreboard; ++ } ++ + if (child_index < 0) { + child_index = fpm_scoreboard_i; + } + +- if (child_index < 0 || child_index >= scoreboard->nprocs) { +- return NULL; +- } ++ return fpm_scoreboard_proc_get_ex(scoreboard, child_index, scoreboard->nprocs); ++} ++ ++struct fpm_scoreboard_proc_s *fpm_scoreboard_proc_get_from_child(struct fpm_child_s *child) /* {{{*/ ++{ ++ struct fpm_worker_pool_s *wp = child->wp; ++ unsigned int nprocs = wp->config->pm_max_children; ++ struct fpm_scoreboard_s *scoreboard = wp->scoreboard; ++ int child_index = child->scoreboard_i; + +- return scoreboard->procs[child_index]; ++ return fpm_scoreboard_proc_get_ex(scoreboard, child_index, nprocs); + } + /* }}} */ + ++ + struct fpm_scoreboard_s *fpm_scoreboard_acquire(struct fpm_scoreboard_s *scoreboard, int nohang) /* {{{ */ + { + struct fpm_scoreboard_s *s; +@@ -236,28 +248,28 @@ void fpm_scoreboard_proc_release(struct fpm_scoreboard_proc_s *proc) /* {{{ */ + proc->lock = 0; + } + +-void fpm_scoreboard_free(struct fpm_scoreboard_s *scoreboard) /* {{{ */ ++void fpm_scoreboard_free(struct fpm_worker_pool_s *wp) /* {{{ */ + { +- size_t scoreboard_size, scoreboard_nprocs_size; ++ size_t scoreboard_procs_size; ++ struct fpm_scoreboard_s *scoreboard = wp->scoreboard; + + if (!scoreboard) { + zlog(ZLOG_ERROR, "**scoreboard is NULL"); + return; + } + +- scoreboard_size = sizeof(struct fpm_scoreboard_s) + (scoreboard->nprocs) * sizeof(struct fpm_scoreboard_proc_s *); +- scoreboard_nprocs_size = sizeof(struct fpm_scoreboard_proc_s) * scoreboard->nprocs; +- +- fpm_shm_free(scoreboard, scoreboard_size + scoreboard_nprocs_size); ++ scoreboard_procs_size = sizeof(struct fpm_scoreboard_proc_s) * wp->config->pm_max_children; ++ ++ fpm_shm_free(scoreboard, sizeof(struct fpm_scoreboard_s) + scoreboard_procs_size); + } + /* }}} */ + +-void fpm_scoreboard_child_use(struct fpm_scoreboard_s *scoreboard, int child_index, pid_t pid) /* {{{ */ ++void fpm_scoreboard_child_use(struct fpm_child_s *child, pid_t pid) /* {{{ */ + { + struct fpm_scoreboard_proc_s *proc; +- fpm_scoreboard = scoreboard; +- fpm_scoreboard_i = child_index; +- proc = fpm_scoreboard_proc_get(scoreboard, child_index); ++ fpm_scoreboard = child->wp->scoreboard; ++ fpm_scoreboard_i = child->scoreboard_i; ++ proc = fpm_scoreboard_proc_get_from_child(child); + if (!proc) { + return; + } +@@ -266,18 +278,22 @@ void fpm_scoreboard_child_use(struct fpm_scoreboard_s *scoreboard, int child_ind + } + /* }}} */ + +-void fpm_scoreboard_proc_free(struct fpm_scoreboard_s *scoreboard, int child_index) /* {{{ */ ++void fpm_scoreboard_proc_free(struct fpm_child_s *child) /* {{{ */ + { ++ struct fpm_worker_pool_s *wp = child->wp; ++ struct fpm_scoreboard_s *scoreboard = wp->scoreboard; ++ int child_index = child->scoreboard_i; ++ + if (!scoreboard) { + return; + } + +- if (child_index < 0 || child_index >= scoreboard->nprocs) { ++ if (child_index < 0 || child_index >= wp->config->pm_max_children) { + return; + } + +- if (scoreboard->procs[child_index] && scoreboard->procs[child_index]->used > 0) { +- memset(scoreboard->procs[child_index], 0, sizeof(struct fpm_scoreboard_proc_s)); ++ if (scoreboard->procs[child_index].used > 0) { ++ memset(&scoreboard->procs[child_index], 0, sizeof(struct fpm_scoreboard_proc_s)); + } + + /* set this slot as free to avoid search on next alloc */ +@@ -285,41 +301,44 @@ void fpm_scoreboard_proc_free(struct fpm_scoreboard_s *scoreboard, int child_ind + } + /* }}} */ + +-int fpm_scoreboard_proc_alloc(struct fpm_scoreboard_s *scoreboard, int *child_index) /* {{{ */ ++int fpm_scoreboard_proc_alloc(struct fpm_child_s *child) /* {{{ */ + { + int i = -1; ++ struct fpm_worker_pool_s *wp = child->wp; ++ struct fpm_scoreboard_s *scoreboard = wp->scoreboard; ++ int nprocs = wp->config->pm_max_children; + +- if (!scoreboard || !child_index) { ++ if (!scoreboard) { + return -1; + } + + /* first try the slot which is supposed to be free */ +- if (scoreboard->free_proc >= 0 && scoreboard->free_proc < scoreboard->nprocs) { +- if (scoreboard->procs[scoreboard->free_proc] && !scoreboard->procs[scoreboard->free_proc]->used) { ++ if (scoreboard->free_proc >= 0 && scoreboard->free_proc < nprocs) { ++ if (!scoreboard->procs[scoreboard->free_proc].used) { + i = scoreboard->free_proc; + } + } + + if (i < 0) { /* the supposed free slot is not, let's search for a free slot */ + zlog(ZLOG_DEBUG, "[pool %s] the proc->free_slot was not free. Let's search", scoreboard->pool); +- for (i = 0; i < scoreboard->nprocs; i++) { +- if (scoreboard->procs[i] && !scoreboard->procs[i]->used) { /* found */ ++ for (i = 0; i < nprocs; i++) { ++ if (!scoreboard->procs[i].used) { /* found */ + break; + } + } + } + + /* no free slot */ +- if (i < 0 || i >= scoreboard->nprocs) { ++ if (i < 0 || i >= nprocs) { + zlog(ZLOG_ERROR, "[pool %s] no free scoreboard slot", scoreboard->pool); + return -1; + } + +- scoreboard->procs[i]->used = 1; +- *child_index = i; ++ scoreboard->procs[i].used = 1; ++ child->scoreboard_i = i; + + /* supposed next slot is free */ +- if (i + 1 >= scoreboard->nprocs) { ++ if (i + 1 >= nprocs) { + scoreboard->free_proc = 0; + } else { + scoreboard->free_proc = i + 1; +diff --git a/sapi/fpm/fpm/fpm_scoreboard.h b/sapi/fpm/fpm/fpm_scoreboard.h +index f58a28737d..a0cc093b8c 100644 +--- a/sapi/fpm/fpm/fpm_scoreboard.h ++++ b/sapi/fpm/fpm/fpm_scoreboard.h +@@ -65,7 +65,7 @@ struct fpm_scoreboard_s { + unsigned int nprocs; + int free_proc; + unsigned long int slow_rq; +- struct fpm_scoreboard_proc_s *procs[]; ++ struct fpm_scoreboard_proc_s procs[]; + }; + + int fpm_scoreboard_init_main(); +@@ -74,18 +74,19 @@ int fpm_scoreboard_init_child(struct fpm_worker_pool_s *wp); + void fpm_scoreboard_update(int idle, int active, int lq, int lq_len, int requests, int max_children_reached, int slow_rq, int action, struct fpm_scoreboard_s *scoreboard); + struct fpm_scoreboard_s *fpm_scoreboard_get(); + struct fpm_scoreboard_proc_s *fpm_scoreboard_proc_get(struct fpm_scoreboard_s *scoreboard, int child_index); ++struct fpm_scoreboard_proc_s *fpm_scoreboard_proc_get_from_child(struct fpm_child_s *child); + + struct fpm_scoreboard_s *fpm_scoreboard_acquire(struct fpm_scoreboard_s *scoreboard, int nohang); + void fpm_scoreboard_release(struct fpm_scoreboard_s *scoreboard); + struct fpm_scoreboard_proc_s *fpm_scoreboard_proc_acquire(struct fpm_scoreboard_s *scoreboard, int child_index, int nohang); + void fpm_scoreboard_proc_release(struct fpm_scoreboard_proc_s *proc); + +-void fpm_scoreboard_free(struct fpm_scoreboard_s *scoreboard); ++void fpm_scoreboard_free(struct fpm_worker_pool_s *wp); + +-void fpm_scoreboard_child_use(struct fpm_scoreboard_s *scoreboard, int child_index, pid_t pid); ++void fpm_scoreboard_child_use(struct fpm_child_s *child, pid_t pid); + +-void fpm_scoreboard_proc_free(struct fpm_scoreboard_s *scoreboard, int child_index); +-int fpm_scoreboard_proc_alloc(struct fpm_scoreboard_s *scoreboard, int *child_index); ++void fpm_scoreboard_proc_free(struct fpm_child_s *child); ++int fpm_scoreboard_proc_alloc(struct fpm_child_s *child); + + #ifdef HAVE_TIMES + float fpm_scoreboard_get_tick(); +diff --git a/sapi/fpm/fpm/fpm_status.c b/sapi/fpm/fpm/fpm_status.c +index 3e82face3c..666a1d9aba 100644 +--- a/sapi/fpm/fpm/fpm_status.c ++++ b/sapi/fpm/fpm/fpm_status.c +@@ -402,10 +402,10 @@ int fpm_status_handle_request(void) /* {{{ */ + + first = 1; + for (i=0; i<scoreboard_p->nprocs; i++) { +- if (!scoreboard_p->procs[i] || !scoreboard_p->procs[i]->used) { ++ if (!scoreboard_p->procs[i].used) { + continue; + } +- proc = *scoreboard_p->procs[i]; ++ proc = scoreboard_p->procs[i]; + + if (first) { + first = 0; +diff --git a/sapi/fpm/fpm/fpm_worker_pool.c b/sapi/fpm/fpm/fpm_worker_pool.c +index a0022915cd..c778b33c8c 100644 +--- a/sapi/fpm/fpm/fpm_worker_pool.c ++++ b/sapi/fpm/fpm/fpm_worker_pool.c +@@ -44,7 +44,7 @@ static void fpm_worker_pool_cleanup(int which, void *arg) /* {{{ */ + fpm_worker_pool_config_free(wp->config); + fpm_children_free(wp->children); + if ((which & FPM_CLEANUP_CHILD) == 0 && fpm_globals.parent_pid == getpid()) { +- fpm_scoreboard_free(wp->scoreboard); ++ fpm_scoreboard_free(wp); + } + fpm_worker_pool_free(wp); + } +-- +2.31.1 + +From 9e0b951aee92deb470e31bd9f0e14f1434861b6a Mon Sep 17 00:00:00 2001 +From: Remi Collet <remi@remirepo.net> +Date: Wed, 20 Oct 2021 14:11:20 +0200 +Subject: [PATCH 2/2] NEWS + +(cherry picked from commit 28c4ee7da2cf3428113e07326dbd46550c50c2bd) +--- + NEWS | 6 ++++++ + 1 file changed, 6 insertions(+) + +diff --git a/NEWS b/NEWS +index 8167eb8552..26c5f70de9 100644 +--- a/NEWS ++++ b/NEWS +@@ -1,6 +1,12 @@ + PHP NEWS + ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| + ++Backported from 7.4.25 ++ ++- FPM: ++ . Fixed bug #81026 (PHP-FPM oob R/W in root process leading to privilege ++ escalation) (CVE-2021-21703). (Jakub Zelenka) ++ + Backported from 7.3.30 + + - Phar: +-- +2.31.1 + diff --git a/php-bug81122.patch b/php-bug81122.patch new file mode 100644 index 0000000..3d200d6 --- /dev/null +++ b/php-bug81122.patch @@ -0,0 +1,88 @@ +From bb524cfbd498cebedd9f866c51cef97b69f59644 Mon Sep 17 00:00:00 2001 +From: "Christoph M. Becker" <cmbecker69@gmx.de> +Date: Mon, 14 Jun 2021 13:22:27 +0200 +Subject: [PATCH 1/8] Fix #81122: SSRF bypass in FILTER_VALIDATE_URL + +We need to ensure that the password detected by parse_url() is actually +a valid password; we can re-use is_userinfo_valid() for that. + +(cherry picked from commit a5538c62293fa782fcc382d0635cfc0c8b9190e3) +--- + ext/filter/logical_filters.c | 4 +++- + ext/filter/tests/bug81122.phpt | 21 +++++++++++++++++++++ + 2 files changed, 24 insertions(+), 1 deletion(-) + create mode 100644 ext/filter/tests/bug81122.phpt + +diff --git a/ext/filter/logical_filters.c b/ext/filter/logical_filters.c +index 22868fd8c1..357f7f2f43 100644 +--- a/ext/filter/logical_filters.c ++++ b/ext/filter/logical_filters.c +@@ -587,7 +587,9 @@ bad_url: + RETURN_VALIDATION_FAILED + } + +- if (url->user != NULL && !is_userinfo_valid(url->user)) { ++ if (url->user != NULL && !is_userinfo_valid(url->user) ++ || url->pass != NULL && !is_userinfo_valid(url->pass) ++ ) { + php_url_free(url); + RETURN_VALIDATION_FAILED + +diff --git a/ext/filter/tests/bug81122.phpt b/ext/filter/tests/bug81122.phpt +new file mode 100644 +index 0000000000..d89d4114a5 +--- /dev/null ++++ b/ext/filter/tests/bug81122.phpt +@@ -0,0 +1,21 @@ ++--TEST-- ++Bug #81122 (SSRF bypass in FILTER_VALIDATE_URL) ++--SKIPIF-- ++<?php ++if (!extension_loaded('filter')) die("skip filter extension not available"); ++?> ++--FILE-- ++<?php ++$urls = [ ++ "https://example.com:\\@test.com/", ++ "https://user:\\epass@test.com", ++ "https://user:\\@test.com", ++]; ++foreach ($urls as $url) { ++ var_dump(filter_var($url, FILTER_VALIDATE_URL)); ++} ++?> ++--EXPECT-- ++bool(false) ++bool(false) ++bool(false) +-- +2.31.1 + +From db917b17e82344db82a67c4c68f627246b484ac4 Mon Sep 17 00:00:00 2001 +From: Stanislav Malyshev <stas@php.net> +Date: Sun, 27 Jun 2021 21:57:58 -0700 +Subject: [PATCH 2/8] Fix warning + +(cherry picked from commit 190013787bbc424c240413d914e3a038f974ccef) +--- + ext/filter/logical_filters.c | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/ext/filter/logical_filters.c b/ext/filter/logical_filters.c +index 357f7f2f43..7a37f80e46 100644 +--- a/ext/filter/logical_filters.c ++++ b/ext/filter/logical_filters.c +@@ -587,8 +587,8 @@ bad_url: + RETURN_VALIDATION_FAILED + } + +- if (url->user != NULL && !is_userinfo_valid(url->user) +- || url->pass != NULL && !is_userinfo_valid(url->pass) ++ if ((url->user != NULL && !is_userinfo_valid(url->user)) ++ || (url->pass != NULL && !is_userinfo_valid(url->pass)) + ) { + php_url_free(url); + RETURN_VALIDATION_FAILED +-- +2.31.1 + diff --git a/php-bug81211.patch b/php-bug81211.patch new file mode 100644 index 0000000..2df28c6 --- /dev/null +++ b/php-bug81211.patch @@ -0,0 +1,164 @@ +From bc182403e00c9da5a51f52a443047946266ca5d8 Mon Sep 17 00:00:00 2001 +From: "Christoph M. Becker" <cmbecker69@gmx.de> +Date: Mon, 23 Aug 2021 13:42:17 +0200 +Subject: [PATCH 1/3] Fix #81211: Symlinks are followed when creating PHAR + archive + +It is insufficient to check whether the `base` is contained in `fname`; +we also need to ensure that `fname` is properly separated. And of +course, `fname` has to start with `base`. + +(cherry picked from commit 2ff853aa113e52637c85e28d1a03df1aa2d747b5) +--- + ext/phar/phar_object.c | 3 +- + ext/phar/tests/bug81211.phpt | 45 +++++++++++++++++++ + .../tests/file/windows_links/common.inc | 9 +++- + 3 files changed, 55 insertions(+), 2 deletions(-) + create mode 100644 ext/phar/tests/bug81211.phpt + +diff --git a/ext/phar/phar_object.c b/ext/phar/phar_object.c +index 8a0d369287..c8898a4a9d 100644 +--- a/ext/phar/phar_object.c ++++ b/ext/phar/phar_object.c +@@ -1452,6 +1452,7 @@ static int phar_build(zend_object_iterator *iter, void *puser) /* {{{ */ + zend_class_entry *ce = p_obj->c; + phar_archive_object *phar_obj = p_obj->p; + php_stream_statbuf ssb; ++ char ch; + + value = iter->funcs->get_current_data(iter); + +@@ -1581,7 +1582,7 @@ phar_spl_fileinfo: + base = temp; + base_len = (int)strlen(base); + +- if (strstr(fname, base)) { ++ if (fname_len >= base_len && strncmp(fname, base, base_len) == 0 && ((ch = fname[base_len - IS_SLASH(base[base_len - 1])]) == '\0' || IS_SLASH(ch))) { + str_key_len = fname_len - base_len; + + if (str_key_len <= 0) { +diff --git a/ext/phar/tests/bug81211.phpt b/ext/phar/tests/bug81211.phpt +new file mode 100644 +index 0000000000..43d82143f2 +--- /dev/null ++++ b/ext/phar/tests/bug81211.phpt +@@ -0,0 +1,45 @@ ++--TEST-- ++Bug #81211 (Symlinks are followed when creating PHAR archive) ++--SKIPIF-- ++<?php ++if (!extension_loaded('phar')) die('skip phar extension is not available'); ++if (PHP_OS_FAMILY === 'Windows') { ++ if (false === include __DIR__ . '/../../standard/tests/file/windows_links/common.inc') { ++ die('skip windows_links/common.inc is not available'); ++ } ++ skipIfSeCreateSymbolicLinkPrivilegeIsDisabled(__FILE__); ++} ++?> ++--FILE-- ++<?php ++mkdir(__DIR__ . '/bug81211'); ++mkdir(__DIR__ . '/bug81211/foobar'); ++mkdir(__DIR__ . '/bug81211/foo'); ++ ++file_put_contents(__DIR__ . '/bug81211/foobar/file', 'this file should NOT be included in the archive!'); ++symlink(__DIR__ . '/bug81211/foobar/file', __DIR__ . '/bug81211/foo/symlink'); ++ ++$archive = new PharData(__DIR__ . '/bug81211/archive.tar'); ++try { ++ $archive->buildFromDirectory(__DIR__ . '/bug81211/foo'); ++} catch (UnexpectedValueException $ex) { ++ echo $ex->getMessage(), PHP_EOL; ++} ++try { ++ $archive->buildFromIterator(new RecursiveDirectoryIterator(__DIR__ . '/bug81211/foo', FilesystemIterator::SKIP_DOTS), __DIR__ . '/bug81211/foo'); ++} catch (UnexpectedValueException $ex) { ++ echo $ex->getMessage(), PHP_EOL; ++} ++?> ++--CLEAN-- ++<?php ++@unlink(__DIR__ . '/bug81211/archive.tar'); ++@unlink(__DIR__ . '/bug81211/foo/symlink'); ++@unlink(__DIR__ . '/bug81211/foobar/file'); ++@rmdir(__DIR__ . '/bug81211/foo'); ++@rmdir(__DIR__ . '/bug81211/foobar'); ++@rmdir(__DIR__ . '/bug81211'); ++?> ++--EXPECTF-- ++Iterator RecursiveIteratorIterator returned a path "%s%ebug81211\foobar\file" that is not in the base directory "%s%ebug81211\foo" ++Iterator RecursiveDirectoryIterator returned a path "%s%ebug81211\foobar\file" that is not in the base directory "%s%ebug81211\foo" +diff --git a/ext/standard/tests/file/windows_links/common.inc b/ext/standard/tests/file/windows_links/common.inc +index 2d4b47cd51..936a1e31e8 100644 +--- a/ext/standard/tests/file/windows_links/common.inc ++++ b/ext/standard/tests/file/windows_links/common.inc +@@ -20,4 +20,11 @@ function get_mountvol() { + return "$sysroot\\System32\\mountvol.exe"; + } + +-?> ++function skipIfSeCreateSymbolicLinkPrivilegeIsDisabled(string $filename) { ++ $ln = "$filename.lnk"; ++ $ret = exec("mklink $ln " . __FILE__ .' 2>&1', $out); ++ @unlink($ln); ++ if (strpos($ret, 'privilege') !== false) { ++ die('skip SeCreateSymbolicLinkPrivilege not enabled'); ++ } ++} +-- +2.31.1 + +From 3d93ceed09ce3e575676d349863c23f4d878cb0f Mon Sep 17 00:00:00 2001 +From: Stanislav Malyshev <stas@php.net> +Date: Mon, 23 Aug 2021 23:43:32 -0700 +Subject: [PATCH 2/3] Fix test + +(cherry picked from commit b815645aac76b494dc119fa6b88de32fa9bcccf1) +--- + ext/phar/tests/bug81211.phpt | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/ext/phar/tests/bug81211.phpt b/ext/phar/tests/bug81211.phpt +index 43d82143f2..96b1401b40 100644 +--- a/ext/phar/tests/bug81211.phpt ++++ b/ext/phar/tests/bug81211.phpt +@@ -41,5 +41,5 @@ try { + @rmdir(__DIR__ . '/bug81211'); + ?> + --EXPECTF-- +-Iterator RecursiveIteratorIterator returned a path "%s%ebug81211\foobar\file" that is not in the base directory "%s%ebug81211\foo" +-Iterator RecursiveDirectoryIterator returned a path "%s%ebug81211\foobar\file" that is not in the base directory "%s%ebug81211\foo" ++Iterator RecursiveIteratorIterator returned a path "%s%ebug81211%efoobar%efile" that is not in the base directory "%s%ebug81211%efoo" ++Iterator RecursiveDirectoryIterator returned a path "%s%ebug81211%efoobar%efile" that is not in the base directory "%s%ebug81211%efoo" +-- +2.31.1 + +From 3acef06f1251525fe938606a00677c36ad18ff4f Mon Sep 17 00:00:00 2001 +From: Remi Collet <remi@remirepo.net> +Date: Wed, 25 Aug 2021 15:23:50 +0200 +Subject: [PATCH 3/3] NEWS + +(cherry picked from commit 5539cefcda6aca7af220e7be7760a682abb88200) +--- + NEWS | 5 +++++ + 1 file changed, 5 insertions(+) + +diff --git a/NEWS b/NEWS +index 861dbd7dd5..8167eb8552 100644 +--- a/NEWS ++++ b/NEWS +@@ -1,6 +1,11 @@ + PHP NEWS + ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| + ++Backported from 7.3.30 ++ ++- Phar: ++ . Fixed bug #81211: Symlinks are followed when creating PHAR archive (cmb) ++ + Backported from 7.3.29 + + - Core: +-- +2.31.1 + diff --git a/php-bug81719.patch b/php-bug81719.patch new file mode 100644 index 0000000..40ba272 --- /dev/null +++ b/php-bug81719.patch @@ -0,0 +1,66 @@ +From 1f8f48703c7800b0e90344ccd73e74a1727f8a72 Mon Sep 17 00:00:00 2001 +From: Stanislav Malyshev <smalyshev@gmail.com> +Date: Mon, 6 Jun 2022 00:56:51 -0600 +Subject: [PATCH 2/3] Fix bug #81719: mysqlnd/pdo password buffer overflow + +(cherry picked from commit 58006537fc5f133ae8549efe5118cde418b3ace9) +(cherry picked from commit 9433de72e291db518357fe55531cc15432d43ec4) +(cherry picked from commit 1560224d3a26574f0195af3853e4d7e050b0b06f) +(cherry picked from commit 5e1d9182748c5330c4bf2154da858206e76914b6) +--- + ext/mysqlnd/mysqlnd_wireprotocol.c | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/ext/mysqlnd/mysqlnd_wireprotocol.c b/ext/mysqlnd/mysqlnd_wireprotocol.c +index 6113543e2b..fa8c6bff46 100644 +--- a/ext/mysqlnd/mysqlnd_wireprotocol.c ++++ b/ext/mysqlnd/mysqlnd_wireprotocol.c +@@ -798,7 +798,8 @@ static size_t + php_mysqlnd_change_auth_response_write(void * _packet, MYSQLND_CONN_DATA * conn) + { + MYSQLND_PACKET_CHANGE_AUTH_RESPONSE *packet= (MYSQLND_PACKET_CHANGE_AUTH_RESPONSE *) _packet; +- zend_uchar * buffer = conn->net->cmd_buffer.length >= packet->auth_data_len? conn->net->cmd_buffer.buffer : mnd_emalloc(packet->auth_data_len); ++ size_t total_packet_size = packet->auth_data_len + MYSQLND_HEADER_SIZE; ++ zend_uchar * buffer = conn->net->cmd_buffer.length >= total_packet_size? conn->net->cmd_buffer.buffer : mnd_emalloc(total_packet_size); + zend_uchar *p = buffer + MYSQLND_HEADER_SIZE; /* start after the header */ + + DBG_ENTER("php_mysqlnd_change_auth_response_write"); +-- +2.35.3 + +From b243ab09f95d2737b99ce87d485c052734c2f3f5 Mon Sep 17 00:00:00 2001 +From: Remi Collet <remi@remirepo.net> +Date: Tue, 7 Jun 2022 09:57:15 +0200 +Subject: [PATCH 3/3] NEWS + +(cherry picked from commit f451082baf14ee9ea86cdd19870e906adb368f02) +(cherry picked from commit 87247fb08e905e629836350ac4e639edd1b40ed8) +(cherry picked from commit 151499ec0f70bf4f1bd65aebf037bd6273f0ef34) +--- + NEWS | 10 ++++++++++ + 1 file changed, 10 insertions(+) + +diff --git a/NEWS b/NEWS +index 0207f4caed..8d609a489c 100644 +--- a/NEWS ++++ b/NEWS +@@ -1,6 +1,16 @@ + PHP NEWS + ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| + ++Backported from 7.4.30 ++ ++- mysqlnd: ++ . Fixed bug #81719: mysqlnd/pdo password buffer overflow. ++ (CVE-2022-31626) (c dot fol at ambionics dot io) ++ ++- pgsql ++ . Fixed bug #81720: Uninitialized array in pg_query_params(). ++ (CVE-2022-31625) (cmb) ++ + Backported from 7.3.33 + + - XML: +-- +2.35.3 + diff --git a/php-bug81720.patch b/php-bug81720.patch new file mode 100644 index 0000000..7e947c3 --- /dev/null +++ b/php-bug81720.patch @@ -0,0 +1,79 @@ +From f8b8096675156bb0560256af790532e2a810311e Mon Sep 17 00:00:00 2001 +From: "Christoph M. Becker" <cmbecker69@gmx.de> +Date: Tue, 17 May 2022 12:59:23 +0200 +Subject: [PATCH 1/3] Fix #81720: Uninitialized array in pg_query_params() + leading to RCE + +We must not free parameters which we haven't initialized yet. + +We also fix the not directly related issue, that we checked for the +wrong value being `NULL`, potentially causing a segfault. + +(cherry picked from commit 55f6895f4b4c677272fd4ee1113acdbd99c4b5ab) +(cherry picked from commit 6f979c832c861fb32e2dbad5e0cc29edcee7c500) +(cherry picked from commit 310b17f5c8938389b1dbd7d8ff5a8144bfb9a351) +(cherry picked from commit 9e7d6a2a1e8f43bdb86a0b6c1199f938f6ba78f5) +--- + ext/pgsql/pgsql.c | 4 ++-- + ext/pgsql/tests/bug81720.phpt | 27 +++++++++++++++++++++++++++ + 2 files changed, 29 insertions(+), 2 deletions(-) + create mode 100644 ext/pgsql/tests/bug81720.phpt + +diff --git a/ext/pgsql/pgsql.c b/ext/pgsql/pgsql.c +index b215697ca9..b9b7776f4d 100644 +--- a/ext/pgsql/pgsql.c ++++ b/ext/pgsql/pgsql.c +@@ -1981,7 +1981,7 @@ PHP_FUNCTION(pg_query_params) + if (Z_TYPE(tmp_val) != IS_STRING) { + php_error_docref(NULL, E_WARNING,"Error converting parameter"); + zval_ptr_dtor(&tmp_val); +- _php_pgsql_free_params(params, num_params); ++ _php_pgsql_free_params(params, i); + RETURN_FALSE; + } + params[i] = estrndup(Z_STRVAL(tmp_val), Z_STRLEN(tmp_val)); +@@ -5146,7 +5146,7 @@ PHP_FUNCTION(pg_send_execute) + if (Z_TYPE(tmp_val) != IS_STRING) { + php_error_docref(NULL, E_WARNING,"Error converting parameter"); + zval_ptr_dtor(&tmp_val); +- _php_pgsql_free_params(params, num_params); ++ _php_pgsql_free_params(params, i); + RETURN_FALSE; + } + params[i] = estrndup(Z_STRVAL(tmp_val), Z_STRLEN(tmp_val)); +diff --git a/ext/pgsql/tests/bug81720.phpt b/ext/pgsql/tests/bug81720.phpt +new file mode 100644 +index 0000000000..d79f1fcdd6 +--- /dev/null ++++ b/ext/pgsql/tests/bug81720.phpt +@@ -0,0 +1,27 @@ ++--TEST-- ++Bug #81720 (Uninitialized array in pg_query_params() leading to RCE) ++--SKIPIF-- ++<?php include("skipif.inc"); ?> ++--FILE-- ++<?php ++include('config.inc'); ++ ++$conn = pg_connect($conn_str); ++ ++try { ++ pg_query_params($conn, 'SELECT $1, $2', [1, new stdClass()]); ++} catch (Throwable $ex) { ++ echo $ex->getMessage(), PHP_EOL; ++} ++ ++try { ++ pg_send_prepare($conn, "my_query", 'SELECT $1, $2'); ++ pg_get_result($conn); ++ pg_send_execute($conn, "my_query", [1, new stdClass()]); ++} catch (Throwable $ex) { ++ echo $ex->getMessage(), PHP_EOL; ++} ++?> ++--EXPECT-- ++Object of class stdClass could not be converted to string ++Object of class stdClass could not be converted to string +-- +2.35.3 + diff --git a/php-bug81726.patch b/php-bug81726.patch new file mode 100644 index 0000000..ebeb4a2 --- /dev/null +++ b/php-bug81726.patch @@ -0,0 +1,180 @@ +From ce0c274748a7b2fa3f0faf7a671b93e0da8faf07 Mon Sep 17 00:00:00 2001 +From: "Christoph M. Becker" <cmbecker69@gmx.de> +Date: Mon, 25 Jul 2022 15:58:59 +0200 +Subject: [PATCH 2/2] Fix #81726: phar wrapper: DOS when using quine gzip file + +The phar wrapper needs to uncompress the file; the uncompressed file +might be compressed, so the wrapper implementation loops. This raises +potential DOS issues regarding too deep or even infinite recursion (the +latter are called compressed file quines[1]). We avoid that by +introducing a recursion limit; we choose the somewhat arbitrary limit +`3`. + +This issue has been reported by real_as3617 and gPayl0ad. + +[1] <https://honno.dev/gzip-quine/> + +(cherry picked from commit 404e8bdb68350931176a5bdc86fc417b34fb583d) +(cherry picked from commit 96fda78bcddd1d793cf2d0ee463dbb49621b577f) +--- + NEWS | 2 ++ + ext/phar/phar.c | 16 +++++++++++----- + ext/phar/tests/bug81726.gz | Bin 0 -> 204 bytes + ext/phar/tests/bug81726.phpt | 14 ++++++++++++++ + 4 files changed, 27 insertions(+), 5 deletions(-) + create mode 100644 ext/phar/tests/bug81726.gz + create mode 100644 ext/phar/tests/bug81726.phpt + +diff --git a/NEWS b/NEWS +index e82f34bbd5..cb09d494b8 100644 +--- a/NEWS ++++ b/NEWS +@@ -4,6 +4,8 @@ PHP NEWS + Backported from 7.4.31 + + - Core: ++ . Fixed bug #81726: phar wrapper: DOS when using quine gzip file. ++ (CVE-2022-31628). (cmb) + . Fixed bug #81727: Don't mangle HTTP variable names that clash with ones + that have a specific semantic meaning. (CVE-2022-31629). (Derick) + +diff --git a/ext/phar/phar.c b/ext/phar/phar.c +index 583ed453e6..c9928ecdcd 100644 +--- a/ext/phar/phar.c ++++ b/ext/phar/phar.c +@@ -1584,7 +1584,8 @@ static int phar_open_from_fp(php_stream* fp, char *fname, int fname_len, char *a + const char zip_magic[] = "PK\x03\x04"; + const char gz_magic[] = "\x1f\x8b\x08"; + const char bz_magic[] = "BZh"; +- char *pos, test = '\0'; ++ char *pos; ++ int recursion_count = 3; // arbitrary limit to avoid too deep or even infinite recursion + const int window_size = 1024; + char buffer[1024 + sizeof(token)]; /* a 1024 byte window + the size of the halt_compiler token (moving window) */ + const zend_long readsize = sizeof(buffer) - sizeof(token); +@@ -1612,8 +1613,7 @@ static int phar_open_from_fp(php_stream* fp, char *fname, int fname_len, char *a + MAPPHAR_ALLOC_FAIL("internal corruption of phar \"%s\" (truncated entry)") + } + +- if (!test) { +- test = '\1'; ++ if (recursion_count) { + pos = buffer+tokenlen; + if (!memcmp(pos, gz_magic, 3)) { + char err = 0; +@@ -1673,7 +1673,10 @@ static int phar_open_from_fp(php_stream* fp, char *fname, int fname_len, char *a + compression = PHAR_FILE_COMPRESSED_GZ; + + /* now, start over */ +- test = '\0'; ++ if (!--recursion_count) { ++ MAPPHAR_ALLOC_FAIL("unable to decompress gzipped phar archive \"%s\""); ++ break; ++ } + continue; + } else if (!memcmp(pos, bz_magic, 3)) { + php_stream_filter *filter; +@@ -1711,7 +1714,10 @@ static int phar_open_from_fp(php_stream* fp, char *fname, int fname_len, char *a + compression = PHAR_FILE_COMPRESSED_BZ2; + + /* now, start over */ +- test = '\0'; ++ if (!--recursion_count) { ++ MAPPHAR_ALLOC_FAIL("unable to decompress bzipped phar archive \"%s\""); ++ break; ++ } + continue; + } + +From f80e21336cc4dc37b6dc8808fec05a584c39d403 Mon Sep 17 00:00:00 2001 +From: "Christoph M. Becker" <cmbecker69@gmx.de> +Date: Tue, 27 Sep 2022 17:43:40 +0200 +Subject: [PATCH] Fix regression introduced by fixing bug 81726 + +When a tar phar is created, `phar_open_from_fp()` is also called, but +since the file has just been created, none of the format checks can +succeed, so we continue to loop, but must not check again for the +format. Therefore, we bring back the old `test` variable. + +Closes GH-9620. + +(cherry picked from commit 432bf196d59bcb661fcf9cb7029cea9b43f490af) +--- + ext/phar/phar.c | 7 +++++-- + 1 file changed, 5 insertions(+), 2 deletions(-) + +diff --git a/ext/phar/phar.c b/ext/phar/phar.c +index c9928ecdcd..f55e5fd4d8 100644 +--- a/ext/phar/phar.c ++++ b/ext/phar/phar.c +@@ -1584,7 +1584,7 @@ static int phar_open_from_fp(php_stream* fp, char *fname, int fname_len, char *a + const char zip_magic[] = "PK\x03\x04"; + const char gz_magic[] = "\x1f\x8b\x08"; + const char bz_magic[] = "BZh"; +- char *pos; ++ char *pos, test = '\0'; + int recursion_count = 3; // arbitrary limit to avoid too deep or even infinite recursion + const int window_size = 1024; + char buffer[1024 + sizeof(token)]; /* a 1024 byte window + the size of the halt_compiler token (moving window) */ +@@ -1613,7 +1613,8 @@ static int phar_open_from_fp(php_stream* fp, char *fname, int fname_len, char *a + MAPPHAR_ALLOC_FAIL("internal corruption of phar \"%s\" (truncated entry)") + } + +- if (recursion_count) { ++ if (!test && recursion_count) { ++ test = '\1'; + pos = buffer+tokenlen; + if (!memcmp(pos, gz_magic, 3)) { + char err = 0; +@@ -1673,6 +1674,7 @@ static int phar_open_from_fp(php_stream* fp, char *fname, int fname_len, char *a + compression = PHAR_FILE_COMPRESSED_GZ; + + /* now, start over */ ++ test = '\0'; + if (!--recursion_count) { + MAPPHAR_ALLOC_FAIL("unable to decompress gzipped phar archive \"%s\""); + break; +@@ -1714,6 +1716,7 @@ static int phar_open_from_fp(php_stream* fp, char *fname, int fname_len, char *a + compression = PHAR_FILE_COMPRESSED_BZ2; + + /* now, start over */ ++ test = '\0'; + if (!--recursion_count) { + MAPPHAR_ALLOC_FAIL("unable to decompress bzipped phar archive \"%s\""); + break; +-- +2.37.3 + +From 9d32d284b25f5df75780911a47b3c23cbaac1761 Mon Sep 17 00:00:00 2001 +From: Remi Collet <remi@remirepo.net> +Date: Fri, 30 Sep 2022 09:22:14 +0200 +Subject: [PATCH] fix NEWS + +--- + NEWS | 8 +++++--- + 1 file changed, 5 insertions(+), 3 deletions(-) + +diff --git a/NEWS b/NEWS +index fe4cb9c484..b7a19aea19 100644 +--- a/NEWS ++++ b/NEWS +@@ -1,14 +1,16 @@ + PHP NEWS + ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| + +-Backported from 7.4.31 ++Backported from 7.4.32 + + - Core: +- . Fixed bug #81726: phar wrapper: DOS when using quine gzip file. +- (CVE-2022-31628). (cmb) + . Fixed bug #81727: Don't mangle HTTP variable names that clash with ones + that have a specific semantic meaning. (CVE-2022-31629). (Derick) + ++- Phar: ++ . Fixed bug #81726: phar wrapper: DOS when using quine gzip file. ++ (CVE-2022-31628). (cmb) ++ + Backported from 7.4.30 + + - mysqlnd: diff --git a/php-bug81727.patch b/php-bug81727.patch new file mode 100644 index 0000000..c80c3c9 --- /dev/null +++ b/php-bug81727.patch @@ -0,0 +1,81 @@ +From bab5b52a66d7af02b4940e31a500c1fc4fd5e894 Mon Sep 17 00:00:00 2001 +From: Derick Rethans <github@derickrethans.nl> +Date: Fri, 9 Sep 2022 16:54:03 +0100 +Subject: [PATCH 1/2] Fix #81727: Don't mangle HTTP variable names that clash + with ones that have a specific semantic meaning. + +(cherry picked from commit 0611be4e82887cee0de6c4cbae320d34eec946ca) +(cherry picked from commit 8b300e157e92b0e945ad813d608f076b5323d721) +--- + NEWS | 6 ++++++ + ext/standard/tests/bug81727.phpt | 15 +++++++++++++++ + main/php_variables.c | 14 ++++++++++++++ + 3 files changed, 35 insertions(+) + create mode 100644 ext/standard/tests/bug81727.phpt + +diff --git a/NEWS b/NEWS +index 8d609a489c..e82f34bbd5 100644 +--- a/NEWS ++++ b/NEWS +@@ -1,6 +1,12 @@ + PHP NEWS + ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| + ++Backported from 7.4.31 ++ ++- Core: ++ . Fixed bug #81727: Don't mangle HTTP variable names that clash with ones ++ that have a specific semantic meaning. (CVE-2022-31629). (Derick) ++ + Backported from 7.4.30 + + - mysqlnd: +diff --git a/ext/standard/tests/bug81727.phpt b/ext/standard/tests/bug81727.phpt +new file mode 100644 +index 0000000000..71a9cb46c8 +--- /dev/null ++++ b/ext/standard/tests/bug81727.phpt +@@ -0,0 +1,15 @@ ++--TEST-- ++Bug #81727: $_COOKIE name starting with ..Host/..Secure should be discarded ++--COOKIE-- ++..Host-test=ignore; __Host-test=correct; . Secure-test=ignore; . Elephpant=Awesome; ++--FILE-- ++<?php ++var_dump($_COOKIE); ++?> ++--EXPECT-- ++array(2) { ++ ["__Host-test"]=> ++ string(7) "correct" ++ ["__Elephpant"]=> ++ string(7) "Awesome" ++} +diff --git a/main/php_variables.c b/main/php_variables.c +index 50ecc663bd..e5d3ffcf52 100644 +--- a/main/php_variables.c ++++ b/main/php_variables.c +@@ -103,6 +103,20 @@ PHPAPI void php_register_variable_ex(char *var_name, zval *val, zval *track_vars + } + var_len = p - var; + ++ /* Discard variable if mangling made it start with __Host-, where pre-mangling it did not start with __Host- */ ++ if (strncmp(var, "__Host-", sizeof("__Host-")-1) == 0 && strncmp(var_name, "__Host-", sizeof("__Host-")-1) != 0) { ++ zval_ptr_dtor_nogc(val); ++ free_alloca(var_orig, use_heap); ++ return; ++ } ++ ++ /* Discard variable if mangling made it start with __Secure-, where pre-mangling it did not start with __Secure- */ ++ if (strncmp(var, "__Secure-", sizeof("__Secure-")-1) == 0 && strncmp(var_name, "__Secure-", sizeof("__Secure-")-1) != 0) { ++ zval_ptr_dtor_nogc(val); ++ free_alloca(var_orig, use_heap); ++ return; ++ } ++ + if (var_len==0) { /* empty variable name, or variable name with a space in it */ + zval_dtor(val); + free_alloca(var_orig, use_heap); +-- +2.37.3 + diff --git a/php-bug81740.patch b/php-bug81740.patch new file mode 100644 index 0000000..5962dd9 --- /dev/null +++ b/php-bug81740.patch @@ -0,0 +1,87 @@ +From dad5b771fba02359facd7be0cd2fd0896cdc91ae Mon Sep 17 00:00:00 2001 +From: "Christoph M. Becker" <cmbecker69@gmx.de> +Date: Mon, 31 Oct 2022 17:20:23 +0100 +Subject: [PATCH 1/2] Fix #81740: PDO::quote() may return unquoted string + +`sqlite3_snprintf()` expects its first parameter to be `int`; we need +to avoid overflow. + +(cherry picked from commit 921b6813da3237a83e908998483f46ae3d8bacba) +(cherry picked from commit 7cb160efe19d3dfb8b92629805733ea186b55050) +--- + ext/pdo_sqlite/sqlite_driver.c | 3 +++ + ext/pdo_sqlite/tests/bug81740.phpt | 17 +++++++++++++++++ + 2 files changed, 20 insertions(+) + create mode 100644 ext/pdo_sqlite/tests/bug81740.phpt + +diff --git a/ext/pdo_sqlite/sqlite_driver.c b/ext/pdo_sqlite/sqlite_driver.c +index 481b62de97..56d15ae2c0 100644 +--- a/ext/pdo_sqlite/sqlite_driver.c ++++ b/ext/pdo_sqlite/sqlite_driver.c +@@ -232,6 +232,9 @@ static char *pdo_sqlite_last_insert_id(pdo_dbh_t *dbh, const char *name, size_t + /* NB: doesn't handle binary strings... use prepared stmts for that */ + static int sqlite_handle_quoter(pdo_dbh_t *dbh, const char *unquoted, size_t unquotedlen, char **quoted, size_t *quotedlen, enum pdo_param_type paramtype ) + { ++ if (unquotedlen > (INT_MAX - 3) / 2) { ++ return 0; ++ } + *quoted = safe_emalloc(2, unquotedlen, 3); + sqlite3_snprintf(2*unquotedlen + 3, *quoted, "'%q'", unquoted); + *quotedlen = strlen(*quoted); +diff --git a/ext/pdo_sqlite/tests/bug81740.phpt b/ext/pdo_sqlite/tests/bug81740.phpt +new file mode 100644 +index 0000000000..99fb07c304 +--- /dev/null ++++ b/ext/pdo_sqlite/tests/bug81740.phpt +@@ -0,0 +1,17 @@ ++--TEST-- ++Bug #81740 (PDO::quote() may return unquoted string) ++--SKIPIF-- ++<?php ++if (!extension_loaded('pdo_sqlite')) print 'skip not loaded'; ++if (getenv("SKIP_SLOW_TESTS")) die("skip slow test"); ++?> ++--INI-- ++memory_limit=-1 ++--FILE-- ++<?php ++$pdo = new PDO("sqlite::memory:"); ++$string = str_repeat("a", 0x80000000); ++var_dump($pdo->quote($string)); ++?> ++--EXPECT-- ++bool(false) +-- +2.38.1 + +From 3a800d9c39caca74158a0aa7a2e268cb5ebac701 Mon Sep 17 00:00:00 2001 +From: Remi Collet <remi@remirepo.net> +Date: Mon, 19 Dec 2022 09:24:02 +0100 +Subject: [PATCH 2/2] NEWS + +(cherry picked from commit 7328f3a0344806b846bd05657bdce96e47810bf0) +(cherry picked from commit dbfbd99e91701c0a5613133c06305fd70545e9ad) +--- + NEWS | 6 ++++++ + 1 file changed, 6 insertions(+) + +diff --git a/NEWS b/NEWS +index eded1b91d6..b7d4bf3082 100644 +--- a/NEWS ++++ b/NEWS +@@ -1,6 +1,12 @@ + PHP NEWS + ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| + ++Backported from 8.0.27 ++ ++- PDO/SQLite: ++ . Fixed bug #81740 (PDO::quote() may return unquoted string). ++ (CVE-2022-31631) (cmb) ++ + Backported from 7.4.32 + + - Core: +-- +2.38.1 + diff --git a/php-bug81744.patch b/php-bug81744.patch new file mode 100644 index 0000000..b4090f4 --- /dev/null +++ b/php-bug81744.patch @@ -0,0 +1,190 @@ +From 07efe5ef4a45f7b0fd99751892fafdc6604173cb Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= <tim@bastelstu.be> +Date: Mon, 23 Jan 2023 21:15:24 +0100 +Subject: [PATCH 1/8] crypt: Fix validation of malformed BCrypt hashes +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +PHP’s implementation of crypt_blowfish differs from the upstream Openwall +version by adding a “PHP Hackâ€, which allows one to cut short the BCrypt salt +by including a `$` character within the characters that represent the salt. + +Hashes that are affected by the “PHP Hack†may erroneously validate any +password as valid when used with `password_verify` and when comparing the +return value of `crypt()` against the input. + +The PHP Hack exists since the first version of PHP’s own crypt_blowfish +implementation that was added in 1e820eca02dcf322b41fd2fe4ed2a6b8309f8ab5. + +No clear reason is given for the PHP Hack’s existence. This commit removes it, +because BCrypt hashes containing a `$` character in their salt are not valid +BCrypt hashes. + +(cherry picked from commit c840f71524067aa474c00c3eacfb83bd860bfc8a) +(cherry picked from commit 7437aaae38cf4b3357e7580f9e22fd4a403b6c23) +--- + ext/standard/crypt_blowfish.c | 8 -- + .../tests/crypt/bcrypt_salt_dollar.phpt | 82 +++++++++++++++++++ + 2 files changed, 82 insertions(+), 8 deletions(-) + create mode 100644 ext/standard/tests/crypt/bcrypt_salt_dollar.phpt + +diff --git a/ext/standard/crypt_blowfish.c b/ext/standard/crypt_blowfish.c +index 5cf306715f..e923b55ed0 100644 +--- a/ext/standard/crypt_blowfish.c ++++ b/ext/standard/crypt_blowfish.c +@@ -377,7 +377,6 @@ static unsigned char BF_atoi64[0x60] = { + #define BF_safe_atoi64(dst, src) \ + { \ + tmp = (unsigned char)(src); \ +- if (tmp == '$') break; /* PHP hack */ \ + if ((unsigned int)(tmp -= 0x20) >= 0x60) return -1; \ + tmp = BF_atoi64[tmp]; \ + if (tmp > 63) return -1; \ +@@ -405,13 +404,6 @@ static int BF_decode(BF_word *dst, const char *src, int size) + *dptr++ = ((c3 & 0x03) << 6) | c4; + } while (dptr < end); + +- if (end - dptr == size) { +- return -1; +- } +- +- while (dptr < end) /* PHP hack */ +- *dptr++ = 0; +- + return 0; + } + +diff --git a/ext/standard/tests/crypt/bcrypt_salt_dollar.phpt b/ext/standard/tests/crypt/bcrypt_salt_dollar.phpt +new file mode 100644 +index 0000000000..32e335f4b0 +--- /dev/null ++++ b/ext/standard/tests/crypt/bcrypt_salt_dollar.phpt +@@ -0,0 +1,82 @@ ++--TEST-- ++bcrypt correctly rejects salts containing $ ++--FILE-- ++<?php ++for ($i = 0; $i < 23; $i++) { ++ $salt = '$2y$04$' . str_repeat('0', $i) . '$'; ++ $result = crypt("foo", $salt); ++ var_dump($salt); ++ var_dump($result); ++ var_dump($result === $salt); ++} ++?> ++--EXPECT-- ++string(8) "$2y$04$$" ++string(2) "*0" ++bool(false) ++string(9) "$2y$04$0$" ++string(2) "*0" ++bool(false) ++string(10) "$2y$04$00$" ++string(2) "*0" ++bool(false) ++string(11) "$2y$04$000$" ++string(2) "*0" ++bool(false) ++string(12) "$2y$04$0000$" ++string(2) "*0" ++bool(false) ++string(13) "$2y$04$00000$" ++string(2) "*0" ++bool(false) ++string(14) "$2y$04$000000$" ++string(2) "*0" ++bool(false) ++string(15) "$2y$04$0000000$" ++string(2) "*0" ++bool(false) ++string(16) "$2y$04$00000000$" ++string(2) "*0" ++bool(false) ++string(17) "$2y$04$000000000$" ++string(2) "*0" ++bool(false) ++string(18) "$2y$04$0000000000$" ++string(2) "*0" ++bool(false) ++string(19) "$2y$04$00000000000$" ++string(2) "*0" ++bool(false) ++string(20) "$2y$04$000000000000$" ++string(2) "*0" ++bool(false) ++string(21) "$2y$04$0000000000000$" ++string(2) "*0" ++bool(false) ++string(22) "$2y$04$00000000000000$" ++string(2) "*0" ++bool(false) ++string(23) "$2y$04$000000000000000$" ++string(2) "*0" ++bool(false) ++string(24) "$2y$04$0000000000000000$" ++string(2) "*0" ++bool(false) ++string(25) "$2y$04$00000000000000000$" ++string(2) "*0" ++bool(false) ++string(26) "$2y$04$000000000000000000$" ++string(2) "*0" ++bool(false) ++string(27) "$2y$04$0000000000000000000$" ++string(2) "*0" ++bool(false) ++string(28) "$2y$04$00000000000000000000$" ++string(2) "*0" ++bool(false) ++string(29) "$2y$04$000000000000000000000$" ++string(2) "*0" ++bool(false) ++string(30) "$2y$04$0000000000000000000000$" ++string(60) "$2y$04$000000000000000000000u2a2UpVexIt9k3FMJeAVr3c04F5tcI8K" ++bool(false) +-- +2.39.1 + +From 31b760acb171475a8960f2eb5cff26eeb7b55728 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= <tim@bastelstu.be> +Date: Mon, 23 Jan 2023 22:13:57 +0100 +Subject: [PATCH 2/8] crypt: Fix possible buffer overread in php_crypt() + +(cherry picked from commit a92acbad873a05470af1a47cb785a18eadd827b5) +(cherry picked from commit ed0281b588a6840cb95f3134a4e68847a3be5bb7) +--- + ext/standard/crypt.c | 1 + + ext/standard/tests/password/password_bcrypt_short.phpt | 8 ++++++++ + 2 files changed, 9 insertions(+) + create mode 100644 ext/standard/tests/password/password_bcrypt_short.phpt + +diff --git a/ext/standard/crypt.c b/ext/standard/crypt.c +index 13a7cec5b9..9913635fba 100644 +--- a/ext/standard/crypt.c ++++ b/ext/standard/crypt.c +@@ -202,6 +202,7 @@ PHPAPI zend_string *php_crypt(const char *password, const int pass_len, const ch + } else if ( + salt[0] == '$' && + salt[1] == '2' && ++ salt[2] != 0 && + salt[3] == '$') { + char output[PHP_MAX_SALT_LEN + 1]; + +diff --git a/ext/standard/tests/password/password_bcrypt_short.phpt b/ext/standard/tests/password/password_bcrypt_short.phpt +new file mode 100644 +index 0000000000..085bc8a239 +--- /dev/null ++++ b/ext/standard/tests/password/password_bcrypt_short.phpt +@@ -0,0 +1,8 @@ ++--TEST-- ++Test that password_hash() does not overread buffers when a short hash is passed ++--FILE-- ++<?php ++var_dump(password_verify("foo", '$2')); ++?> ++--EXPECT-- ++bool(false) +-- +2.39.1 + diff --git a/php-bug81746.patch b/php-bug81746.patch new file mode 100644 index 0000000..9872b1d --- /dev/null +++ b/php-bug81746.patch @@ -0,0 +1,100 @@ +From fdbd028d614508c83dfd5c8be83edd6839297abf Mon Sep 17 00:00:00 2001 +From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> +Date: Fri, 27 Jan 2023 19:28:27 +0100 +Subject: [PATCH 3/8] Fix array overrun when appending slash to paths + +Fix it by extending the array sizes by one character. As the input is +limited to the maximum path length, there will always be place to append +the slash. As the php_check_specific_open_basedir() simply uses the +strings to compare against each other, no new failures related to too +long paths are introduced. +We'll let the DOM and XML case handle a potentially too long path in the +library code. + +(cherry picked from commit ec10b28d64decbc54aa1e585dce580f0bd7a5953) +(cherry picked from commit 887cd0710ad856a0d22c329b6ea6c71ebd8621ae) +--- + ext/dom/document.c | 2 +- + ext/xmlreader/php_xmlreader.c | 2 +- + main/fopen_wrappers.c | 6 +++--- + 3 files changed, 5 insertions(+), 5 deletions(-) + +diff --git a/ext/dom/document.c b/ext/dom/document.c +index 29ef0a8cab..f13e1c8e76 100644 +--- a/ext/dom/document.c ++++ b/ext/dom/document.c +@@ -1359,7 +1359,7 @@ static xmlDocPtr dom_document_parser(zval *id, int mode, char *source, size_t so + int validate, recover, resolve_externals, keep_blanks, substitute_ent; + int resolved_path_len; + int old_error_reporting = 0; +- char *directory=NULL, resolved_path[MAXPATHLEN]; ++ char *directory=NULL, resolved_path[MAXPATHLEN + 1]; + + if (id != NULL) { + intern = Z_DOMOBJ_P(id); +diff --git a/ext/xmlreader/php_xmlreader.c b/ext/xmlreader/php_xmlreader.c +index 40b7d462cd..87a03f9bf7 100644 +--- a/ext/xmlreader/php_xmlreader.c ++++ b/ext/xmlreader/php_xmlreader.c +@@ -1040,7 +1040,7 @@ PHP_METHOD(xmlreader, XML) + xmlreader_object *intern = NULL; + char *source, *uri = NULL, *encoding = NULL; + int resolved_path_len, ret = 0; +- char *directory=NULL, resolved_path[MAXPATHLEN]; ++ char *directory=NULL, resolved_path[MAXPATHLEN + 1]; + xmlParserInputBufferPtr inputbfr; + xmlTextReaderPtr reader; + +diff --git a/main/fopen_wrappers.c b/main/fopen_wrappers.c +index e1f89ce5cc..1eb0f109f2 100644 +--- a/main/fopen_wrappers.c ++++ b/main/fopen_wrappers.c +@@ -137,10 +137,10 @@ PHPAPI ZEND_INI_MH(OnUpdateBaseDir) + */ + PHPAPI int php_check_specific_open_basedir(const char *basedir, const char *path) + { +- char resolved_name[MAXPATHLEN]; +- char resolved_basedir[MAXPATHLEN]; ++ char resolved_name[MAXPATHLEN + 1]; ++ char resolved_basedir[MAXPATHLEN + 1]; + char local_open_basedir[MAXPATHLEN]; +- char path_tmp[MAXPATHLEN]; ++ char path_tmp[MAXPATHLEN + 1]; + char *path_file; + int resolved_basedir_len; + int resolved_name_len; +-- +2.39.1 + +From 562442bebf64686f2df2b93dfab88fb17f9b247a Mon Sep 17 00:00:00 2001 +From: Remi Collet <remi@remirepo.net> +Date: Mon, 13 Feb 2023 11:46:47 +0100 +Subject: [PATCH 4/8] NEWS + +(cherry picked from commit 614468ce4056c0ef93aae09532dcffdf65b594b5) +--- + NEWS | 8 ++++++++ + 1 file changed, 8 insertions(+) + +diff --git a/NEWS b/NEWS +index b7d4bf3082..1fcd07f38b 100644 +--- a/NEWS ++++ b/NEWS +@@ -1,6 +1,14 @@ + PHP NEWS + ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| + ++Backported from 8.0.28 ++ ++- Core: ++ . Fixed bug #81744 (Password_verify() always return true with some hash). ++ (CVE-2023-0567). (Tim Düsterhus) ++ . Fixed bug #81746 (1-byte array overrun in common path resolve code). ++ (CVE-2023-0568). (Niels Dossche) ++ + Backported from 8.0.27 + + - PDO/SQLite: +-- +2.39.1 + diff --git a/php-cve-2023-0662.patch b/php-cve-2023-0662.patch new file mode 100644 index 0000000..7996367 --- /dev/null +++ b/php-cve-2023-0662.patch @@ -0,0 +1,148 @@ +From bbac8f5d5f4c1a42b01de76fbde0d951947f39f6 Mon Sep 17 00:00:00 2001 +From: Jakub Zelenka <bukka@php.net> +Date: Thu, 19 Jan 2023 14:11:18 +0000 +Subject: [PATCH 5/8] Fix repeated warning for file uploads limit exceeding + +(cherry picked from commit 3a2fdef1ae38881110006616ee1f0534b082ca45) +--- + main/rfc1867.c | 5 ++++- + 1 file changed, 4 insertions(+), 1 deletion(-) + +diff --git a/main/rfc1867.c b/main/rfc1867.c +index 6159284311..1c2373c84a 100644 +--- a/main/rfc1867.c ++++ b/main/rfc1867.c +@@ -930,7 +930,10 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */ + skip_upload = 1; + } else if (upload_cnt <= 0) { + skip_upload = 1; +- sapi_module.sapi_error(E_WARNING, "Maximum number of allowable file uploads has been exceeded"); ++ if (upload_cnt == 0) { ++ --upload_cnt; ++ sapi_module.sapi_error(E_WARNING, "Maximum number of allowable file uploads has been exceeded"); ++ } + } + + /* Return with an error if the posted data is garbled */ +-- +2.39.1 + +From a249335e6c4d76e58884c5aba23953e09fddc089 Mon Sep 17 00:00:00 2001 +From: Jakub Zelenka <bukka@php.net> +Date: Thu, 19 Jan 2023 14:31:25 +0000 +Subject: [PATCH 6/8] Introduce max_multipart_body_parts INI + +This fixes GHSA-54hq-v5wp-fqgv DOS vulnerabality by limitting number of +parsed multipart body parts as currently all parts were always parsed. + +(cherry picked from commit 8ec78d28d20c82c75c4747f44c52601cfdb22516) +--- + main/main.c | 1 + + main/rfc1867.c | 11 +++++++++++ + 2 files changed, 12 insertions(+) + +diff --git a/main/main.c b/main/main.c +index 93ac6cabdd..0d61542229 100644 +--- a/main/main.c ++++ b/main/main.c +@@ -587,6 +587,7 @@ PHP_INI_BEGIN() + PHP_INI_ENTRY("disable_functions", "", PHP_INI_SYSTEM, NULL) + PHP_INI_ENTRY("disable_classes", "", PHP_INI_SYSTEM, NULL) + PHP_INI_ENTRY("max_file_uploads", "20", PHP_INI_SYSTEM|PHP_INI_PERDIR, NULL) ++ PHP_INI_ENTRY("max_multipart_body_parts", "-1", PHP_INI_SYSTEM|PHP_INI_PERDIR, NULL) + + STD_PHP_INI_BOOLEAN("allow_url_fopen", "1", PHP_INI_SYSTEM, OnUpdateBool, allow_url_fopen, php_core_globals, core_globals) + STD_PHP_INI_BOOLEAN("allow_url_include", "0", PHP_INI_SYSTEM, OnUpdateBool, allow_url_include, php_core_globals, core_globals) +diff --git a/main/rfc1867.c b/main/rfc1867.c +index 1c2373c84a..022bb94028 100644 +--- a/main/rfc1867.c ++++ b/main/rfc1867.c +@@ -702,6 +702,7 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */ + void *event_extra_data = NULL; + unsigned int llen = 0; + int upload_cnt = INI_INT("max_file_uploads"); ++ int body_parts_cnt = INI_INT("max_multipart_body_parts"); + const zend_encoding *internal_encoding = zend_multibyte_get_internal_encoding(); + php_rfc1867_getword_t getword; + php_rfc1867_getword_conf_t getword_conf; +@@ -723,6 +724,11 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */ + return; + } + ++ if (body_parts_cnt < 0) { ++ body_parts_cnt = PG(max_input_vars) + upload_cnt; ++ } ++ int body_parts_limit = body_parts_cnt; ++ + /* Get the boundary */ + boundary = strstr(content_type_dup, "boundary"); + if (!boundary) { +@@ -807,6 +813,11 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */ + char *pair = NULL; + int end = 0; + ++ if (--body_parts_cnt < 0) { ++ php_error_docref(NULL, E_WARNING, "Multipart body parts limit exceeded %d. To increase the limit change max_multipart_body_parts in php.ini.", body_parts_limit); ++ goto fileupload_done; ++ } ++ + while (isspace(*cd)) { + ++cd; + } +-- +2.39.1 + +From 0f375f617e475209d0ffaeb5c710f916098e4ebd Mon Sep 17 00:00:00 2001 +From: Remi Collet <remi@remirepo.net> +Date: Tue, 14 Feb 2023 09:14:47 +0100 +Subject: [PATCH 7/8] NEWS + +(cherry picked from commit 472db3ee3a00ac00d36019eee0b3b7362334481c) +--- + NEWS | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/NEWS b/NEWS +index 1fcd07f38b..5f3b8c743f 100644 +--- a/NEWS ++++ b/NEWS +@@ -9,6 +9,10 @@ Backported from 8.0.28 + . Fixed bug #81746 (1-byte array overrun in common path resolve code). + (CVE-2023-0568). (Niels Dossche) + ++- FPM: ++ . Fixed bug GHSA-54hq-v5wp-fqgv (DOS vulnerability when parsing multipart ++ request body). (CVE-2023-0662) (Jakub Zelenka) ++ + Backported from 8.0.27 + + - PDO/SQLite: +-- +2.39.1 + +From 5bfe7527d4ad3fc662d04d5ba6b645202105b82f Mon Sep 17 00:00:00 2001 +From: Remi Collet <remi@remirepo.net> +Date: Tue, 14 Feb 2023 11:47:22 +0100 +Subject: [PATCH 8/8] fix NEWS, not FPM specific + +(cherry picked from commit c04f310440a906fc4ca885f4ecf6e3e4cd36edc7) +--- + NEWS | 2 -- + 1 file changed, 2 deletions(-) + +diff --git a/NEWS b/NEWS +index 5f3b8c743f..11f6e7ad5a 100644 +--- a/NEWS ++++ b/NEWS +@@ -8,8 +8,6 @@ Backported from 8.0.28 + (CVE-2023-0567). (Tim Düsterhus) + . Fixed bug #81746 (1-byte array overrun in common path resolve code). + (CVE-2023-0568). (Niels Dossche) +- +-- FPM: + . Fixed bug GHSA-54hq-v5wp-fqgv (DOS vulnerability when parsing multipart + request body). (CVE-2023-0662) (Jakub Zelenka) + +-- +2.39.1 + diff --git a/php-cve-2023-3247.patch b/php-cve-2023-3247.patch new file mode 100644 index 0000000..d16530a --- /dev/null +++ b/php-cve-2023-3247.patch @@ -0,0 +1,151 @@ +From b9e09489f8160eb5e38a11e84ef6c8b74c2ec828 Mon Sep 17 00:00:00 2001 +From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> +Date: Sun, 16 Apr 2023 15:05:03 +0200 +Subject: [PATCH] Fix missing randomness check and insufficient random bytes + for SOAP HTTP Digest +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +If php_random_bytes_throw fails, the nonce will be uninitialized, but +still sent to the server. The client nonce is intended to protect +against a malicious server. See section 5.10 and 5.12 of RFC 7616 [1], +and bullet point 2 below. + +Tim pointed out that even though it's the MD5 of the nonce that gets sent, +enumerating 31 bits is trivial. So we have still a stack information leak +of 31 bits. + +Furthermore, Tim found the following issues: +* The small size of cnonce might cause the server to erroneously reject + a request due to a repeated (cnonce, nc) pair. As per the birthday + problem 31 bits of randomness will return a duplication with 50% + chance after less than 55000 requests and nc always starts counting at 1. +* The cnonce is intended to protect the client and password against a + malicious server that returns a constant server nonce where the server + precomputed a rainbow table between passwords and correct client response. + As storage is fairly cheap, a server could precompute the client responses + for (a subset of) client nonces and still have a chance of reversing the + client response with the same probability as the cnonce duplication. + + Precomputing the rainbow table for all 2^31 cnonces increases the rainbow + table size by factor 2 billion, which is infeasible. But precomputing it + for 2^14 cnonces only increases the table size by factor 16k and the server + would still have a 10% chance of successfully reversing a password with a + single client request. + +This patch fixes the issues by increasing the nonce size, and checking +the return value of php_random_bytes_throw(). In the process we also get +rid of the MD5 hashing of the nonce. + +[1] RFC 7616: https://www.rfc-editor.org/rfc/rfc7616 + +Co-authored-by: Tim Düsterhus <timwolla@php.net> +(cherry picked from commit 126d517ce240e9f638d9a5eaa509eaca49ef562a) +(cherry picked from commit 0cfca9aa1395271833848daec0bace51d965531d) +--- + NEWS | 6 ++++++ + ext/soap/php_http.c | 19 ++++++++++++++----- + 2 files changed, 20 insertions(+), 5 deletions(-) + +diff --git a/NEWS b/NEWS +index 11f6e7ad5a8..0770d913467 100644 +--- a/NEWS ++++ b/NEWS +@@ -1,6 +1,12 @@ + PHP NEWS + ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| + ++Backported from 8.0.29 ++ ++- Soap: ++ . Fixed bug GHSA-76gg-c692-v2mw (Missing error check and insufficient random ++ bytes in HTTP Digest authentication for SOAP). (nielsdos, timwolla) ++ + Backported from 8.0.28 + + - Core: +diff --git a/ext/soap/php_http.c b/ext/soap/php_http.c +index 3a890d7c36f..3bfa4f6f54c 100644 +--- a/ext/soap/php_http.c ++++ b/ext/soap/php_http.c +@@ -646,14 +646,23 @@ int make_http_soap_request(zval *this_ptr, + if ((digest = zend_hash_str_find(Z_OBJPROP_P(this_ptr), "_digest", sizeof("_digest")-1)) != NULL) { + if (Z_TYPE_P(digest) == IS_ARRAY) { + char HA1[33], HA2[33], response[33], cnonce[33], nc[9]; ++ unsigned char nonce[16]; + PHP_MD5_CTX md5ctx; + unsigned char hash[16]; + +- PHP_MD5Init(&md5ctx); +- snprintf(cnonce, sizeof(cnonce), ZEND_LONG_FMT, php_rand()); +- PHP_MD5Update(&md5ctx, (unsigned char*)cnonce, strlen(cnonce)); +- PHP_MD5Final(hash, &md5ctx); +- make_digest(cnonce, hash); ++ if (UNEXPECTED(php_random_bytes_throw(&nonce, sizeof(nonce)) != SUCCESS)) { ++ ZEND_ASSERT(EG(exception)); ++ php_stream_close(stream); ++ zend_hash_str_del(Z_OBJPROP_P(this_ptr), "httpurl", sizeof("httpurl")-1); ++ zend_hash_str_del(Z_OBJPROP_P(this_ptr), "httpsocket", sizeof("httpsocket")-1); ++ zend_hash_str_del(Z_OBJPROP_P(this_ptr), "_use_proxy", sizeof("_use_proxy")-1); ++ smart_str_free(&soap_headers_z); ++ smart_str_free(&soap_headers); ++ return FALSE; ++ } ++ ++ php_hash_bin2hex(cnonce, nonce, sizeof(nonce)); ++ cnonce[32] = 0; + + if ((tmp = zend_hash_str_find(Z_ARRVAL_P(digest), "nc", sizeof("nc")-1)) != NULL && + Z_TYPE_P(tmp) == IS_LONG) { +From e4dd20803ac5579deec54dc6b699d359890f96f0 Mon Sep 17 00:00:00 2001 +From: Remi Collet <remi@remirepo.net> +Date: Tue, 6 Jun 2023 18:05:22 +0200 +Subject: [PATCH] Fix GH-11382 add missing hash header for bin2hex + +(cherry picked from commit 40439039c224bb8cdebd1b7b3d03b8cc11e7cce7) +--- + ext/soap/php_http.c | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/ext/soap/php_http.c b/ext/soap/php_http.c +index 3bfa4f6f54..72b5bdec2b 100644 +--- a/ext/soap/php_http.c ++++ b/ext/soap/php_http.c +@@ -22,7 +22,8 @@ + #include "php_soap.h" + #include "ext/standard/base64.h" + #include "ext/standard/md5.h" +-#include "ext/standard/php_rand.h" ++#include "ext/standard/php_random.h" ++#include "ext/hash/php_hash.h" + + static char *get_http_header_value(char *headers, char *type); + static zend_string *get_http_body(php_stream *socketd, int close, char *headers); +From 4e2dda0221e03b9b9dfc767b26dae656bc7a2407 Mon Sep 17 00:00:00 2001 +From: Remi Collet <remi@remirepo.net> +Date: Thu, 15 Jun 2023 08:47:55 +0200 +Subject: [PATCH] add cve + +(cherry picked from commit f3021d66d7bb42d2578530cc94f9bde47e58eb10) +--- + NEWS | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/NEWS b/NEWS +index 0770d91346..835c538663 100644 +--- a/NEWS ++++ b/NEWS +@@ -5,7 +5,8 @@ Backported from 8.0.29 + + - Soap: + . Fixed bug GHSA-76gg-c692-v2mw (Missing error check and insufficient random +- bytes in HTTP Digest authentication for SOAP). (nielsdos, timwolla) ++ bytes in HTTP Digest authentication for SOAP). ++ (CVE-2023-3247) (nielsdos, timwolla) + + Backported from 8.0.28 + +-- +2.40.1 + diff --git a/php-cve-2023-3823.patch b/php-cve-2023-3823.patch new file mode 100644 index 0000000..b1397b5 --- /dev/null +++ b/php-cve-2023-3823.patch @@ -0,0 +1,93 @@ +From 47388f7e4e1369feeffdb6976b469e7dfa72d9cb Mon Sep 17 00:00:00 2001 +From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> +Date: Mon, 10 Jul 2023 13:25:34 +0200 +Subject: [PATCH 1/3] Fix buffer mismanagement in phar_dir_read() + +Fixes GHSA-jqcx-ccgc-xwhv. + +(cherry picked from commit 80316123f3e9dcce8ac419bd9dd43546e2ccb5ef) +(cherry picked from commit c398fe98c044c8e7c23135acdc38d4ef7bedc983) +(cherry picked from commit 3f14261065e4c0552afa9cb16411475050a41c2c) +(cherry picked from commit f8f433d0d8eaac21af4f4532496d33f9c2b381d6) +(cherry picked from commit f41261182dad0f831d8727967c127da1f08c8ce5) +--- + ext/phar/dirstream.c | 15 ++++++++------ + ext/phar/tests/GHSA-jqcx-ccgc-xwhv.phpt | 27 +++++++++++++++++++++++++ + 2 files changed, 36 insertions(+), 6 deletions(-) + create mode 100644 ext/phar/tests/GHSA-jqcx-ccgc-xwhv.phpt + +diff --git a/ext/phar/dirstream.c b/ext/phar/dirstream.c +index d81f6939bc..aca53ba79a 100644 +--- a/ext/phar/dirstream.c ++++ b/ext/phar/dirstream.c +@@ -92,25 +92,28 @@ static int phar_dir_seek(php_stream *stream, zend_off_t offset, int whence, zend + */ + static size_t phar_dir_read(php_stream *stream, char *buf, size_t count) /* {{{ */ + { +- size_t to_read; + HashTable *data = (HashTable *)stream->abstract; + zend_string *str_key; + zend_ulong unused; + ++ if (count != sizeof(php_stream_dirent)) { ++ return -1; ++ } ++ + if (HASH_KEY_NON_EXISTENT == zend_hash_get_current_key(data, &str_key, &unused)) { + return 0; + } + + zend_hash_move_forward(data); +- to_read = MIN(ZSTR_LEN(str_key), count); + +- if (to_read == 0 || count < ZSTR_LEN(str_key)) { ++ php_stream_dirent *dirent = (php_stream_dirent *) buf; ++ ++ if (sizeof(dirent->d_name) <= ZSTR_LEN(str_key)) { + return 0; + } + +- memset(buf, 0, sizeof(php_stream_dirent)); +- memcpy(((php_stream_dirent *) buf)->d_name, ZSTR_VAL(str_key), to_read); +- ((php_stream_dirent *) buf)->d_name[to_read + 1] = '\0'; ++ memset(dirent, 0, sizeof(php_stream_dirent)); ++ PHP_STRLCPY(dirent->d_name, ZSTR_VAL(str_key), sizeof(dirent->d_name), ZSTR_LEN(str_key)); + + return sizeof(php_stream_dirent); + } +diff --git a/ext/phar/tests/GHSA-jqcx-ccgc-xwhv.phpt b/ext/phar/tests/GHSA-jqcx-ccgc-xwhv.phpt +new file mode 100644 +index 0000000000..4e12f05fb6 +--- /dev/null ++++ b/ext/phar/tests/GHSA-jqcx-ccgc-xwhv.phpt +@@ -0,0 +1,27 @@ ++--TEST-- ++GHSA-jqcx-ccgc-xwhv (Buffer overflow and overread in phar_dir_read()) ++--SKIPIF-- ++<?php if (!extension_loaded("phar")) die("skip"); ?> ++--INI-- ++phar.readonly=0 ++--FILE-- ++<?php ++$phar = new Phar(__DIR__. '/GHSA-jqcx-ccgc-xwhv.phar'); ++$phar->startBuffering(); ++$phar->addFromString(str_repeat('A', PHP_MAXPATHLEN - 1), 'This is the content of file 1.'); ++$phar->addFromString(str_repeat('B', PHP_MAXPATHLEN - 1).'C', 'This is the content of file 2.'); ++$phar->stopBuffering(); ++ ++$handle = opendir('phar://' . __DIR__ . '/GHSA-jqcx-ccgc-xwhv.phar'); ++var_dump(strlen(readdir($handle))); ++// Must not be a string of length PHP_MAXPATHLEN+1 ++var_dump(readdir($handle)); ++closedir($handle); ++?> ++--CLEAN-- ++<?php ++unlink(__DIR__. '/GHSA-jqcx-ccgc-xwhv.phar'); ++?> ++--EXPECTF-- ++int(%d) ++bool(false) +-- +2.41.0 + diff --git a/php-cve-2023-3824.patch b/php-cve-2023-3824.patch new file mode 100644 index 0000000..e071bb2 --- /dev/null +++ b/php-cve-2023-3824.patch @@ -0,0 +1,408 @@ +From 3824593952c6ce4c37cd43137b0877202f5c304e Mon Sep 17 00:00:00 2001 +From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> +Date: Sat, 15 Jul 2023 17:33:52 +0200 +Subject: [PATCH 2/3] Sanitize libxml2 globals before parsing + +Fixes GHSA-3qrf-m4j2-pcrr. + +To parse a document with libxml2, you first need to create a parsing context. +The parsing context contains parsing options (e.g. XML_NOENT to substitute +entities) that the application (in this case PHP) can set. +Unfortunately, libxml2 also supports providing default set options. +For example, if you call xmlSubstituteEntitiesDefault(1) then the XML_NOENT +option will be added to the parsing options every time you create a parsing +context **even if the application never requested XML_NOENT**. + +Third party extensions can override these globals, in particular the +substitute entity global. This causes entity substitution to be +unexpectedly active. + +Fix it by setting the parsing options to a sane known value. +For API calls that depend on global state we introduce +PHP_LIBXML_SANITIZE_GLOBALS() and PHP_LIBXML_RESTORE_GLOBALS(). +For other APIs that work directly with a context we introduce +php_libxml_sanitize_parse_ctxt_options(). + +(cherry picked from commit c283c3ab0ba45d21b2b8745c1f9c7cbfe771c975) +(cherry picked from commit b3758bd21223b97c042cae7bd26a66cde081ea98) +(cherry picked from commit 4fb61f06b1aff89a4d7e548c37ffa5bf573270c3) +(cherry picked from commit d7de6908dfc8774e86a54100ad4e2ee810426001) +(cherry picked from commit 66a1fcc69765bb704146fe7d084848302dd3c89e) +--- + ext/dom/document.c | 15 +++++++++++++++ + ext/dom/documentfragment.c | 2 ++ + ext/libxml/php_libxml.h | 36 +++++++++++++++++++++++++++++++++++ + ext/simplexml/simplexml.c | 6 ++++++ + ext/soap/php_xml.c | 2 ++ + ext/xml/compat.c | 2 ++ + ext/xmlreader/php_xmlreader.c | 9 +++++++++ + ext/xsl/xsltprocessor.c | 9 ++++----- + 8 files changed, 76 insertions(+), 5 deletions(-) + +diff --git a/ext/dom/document.c b/ext/dom/document.c +index f13e1c8e76..1d9c7023b5 100644 +--- a/ext/dom/document.c ++++ b/ext/dom/document.c +@@ -1438,6 +1438,7 @@ static xmlDocPtr dom_document_parser(zval *id, int mode, char *source, size_t so + options |= XML_PARSE_NOBLANKS; + } + ++ php_libxml_sanitize_parse_ctxt_options(ctxt); + xmlCtxtUseOptions(ctxt, options); + + ctxt->recovery = recover; +@@ -1735,7 +1736,9 @@ PHP_FUNCTION(dom_document_xinclude) + + DOM_GET_OBJ(docp, id, xmlDocPtr, intern); + ++ PHP_LIBXML_SANITIZE_GLOBALS(xinclude); + err = xmlXIncludeProcessFlags(docp, (int)flags); ++ PHP_LIBXML_RESTORE_GLOBALS(xinclude); + + /* XML_XINCLUDE_START and XML_XINCLUDE_END nodes need to be removed as these + are added via xmlXIncludeProcess to mark beginning and ending of xincluded document +@@ -1774,6 +1777,7 @@ PHP_FUNCTION(dom_document_validate) + + DOM_GET_OBJ(docp, id, xmlDocPtr, intern); + ++ PHP_LIBXML_SANITIZE_GLOBALS(validate); + cvp = xmlNewValidCtxt(); + + cvp->userData = NULL; +@@ -1785,6 +1789,7 @@ PHP_FUNCTION(dom_document_validate) + } else { + RETVAL_FALSE; + } ++ PHP_LIBXML_RESTORE_GLOBALS(validate); + + xmlFreeValidCtxt(cvp); + +@@ -1818,14 +1823,18 @@ static void _dom_document_schema_validate(INTERNAL_FUNCTION_PARAMETERS, int type + + DOM_GET_OBJ(docp, id, xmlDocPtr, intern); + ++ PHP_LIBXML_SANITIZE_GLOBALS(new_parser_ctxt); ++ + switch (type) { + case DOM_LOAD_FILE: + if (CHECK_NULL_PATH(source, source_len)) { ++ PHP_LIBXML_RESTORE_GLOBALS(new_parser_ctxt); + php_error_docref(NULL, E_WARNING, "Invalid Schema file source"); + RETURN_FALSE; + } + valid_file = _dom_get_valid_file_path(source, resolved_path, MAXPATHLEN); + if (!valid_file) { ++ PHP_LIBXML_RESTORE_GLOBALS(new_parser_ctxt); + php_error_docref(NULL, E_WARNING, "Invalid Schema file source"); + RETURN_FALSE; + } +@@ -1846,6 +1855,7 @@ static void _dom_document_schema_validate(INTERNAL_FUNCTION_PARAMETERS, int type + parser); + sptr = xmlSchemaParse(parser); + xmlSchemaFreeParserCtxt(parser); ++ PHP_LIBXML_RESTORE_GLOBALS(new_parser_ctxt); + if (!sptr) { + php_error_docref(NULL, E_WARNING, "Invalid Schema"); + RETURN_FALSE; +@@ -1866,11 +1876,13 @@ static void _dom_document_schema_validate(INTERNAL_FUNCTION_PARAMETERS, int type + } + #endif + ++ PHP_LIBXML_SANITIZE_GLOBALS(validate); + xmlSchemaSetValidOptions(vptr, valid_opts); + xmlSchemaSetValidErrors(vptr, php_libxml_error_handler, php_libxml_error_handler, vptr); + is_valid = xmlSchemaValidateDoc(vptr, docp); + xmlSchemaFree(sptr); + xmlSchemaFreeValidCtxt(vptr); ++ PHP_LIBXML_RESTORE_GLOBALS(validate); + + if (is_valid == 0) { + RETURN_TRUE; +@@ -1940,12 +1952,14 @@ static void _dom_document_relaxNG_validate(INTERNAL_FUNCTION_PARAMETERS, int typ + return; + } + ++ PHP_LIBXML_SANITIZE_GLOBALS(parse); + xmlRelaxNGSetParserErrors(parser, + (xmlRelaxNGValidityErrorFunc) php_libxml_error_handler, + (xmlRelaxNGValidityWarningFunc) php_libxml_error_handler, + parser); + sptr = xmlRelaxNGParse(parser); + xmlRelaxNGFreeParserCtxt(parser); ++ PHP_LIBXML_RESTORE_GLOBALS(parse); + if (!sptr) { + php_error_docref(NULL, E_WARNING, "Invalid RelaxNG"); + RETURN_FALSE; +@@ -2045,6 +2059,7 @@ static void dom_load_html(INTERNAL_FUNCTION_PARAMETERS, int mode) /* {{{ */ + ctxt->sax->error = php_libxml_ctx_error; + ctxt->sax->warning = php_libxml_ctx_warning; + } ++ php_libxml_sanitize_parse_ctxt_options(ctxt); + if (options) { + htmlCtxtUseOptions(ctxt, (int)options); + } +diff --git a/ext/dom/documentfragment.c b/ext/dom/documentfragment.c +index 0b08202726..84e03aab32 100644 +--- a/ext/dom/documentfragment.c ++++ b/ext/dom/documentfragment.c +@@ -134,7 +134,9 @@ PHP_METHOD(domdocumentfragment, appendXML) { + } + + if (data) { ++ PHP_LIBXML_SANITIZE_GLOBALS(parse); + err = xmlParseBalancedChunkMemory(nodep->doc, NULL, NULL, 0, (xmlChar *) data, &lst); ++ PHP_LIBXML_RESTORE_GLOBALS(parse); + if (err != 0) { + RETURN_FALSE; + } +diff --git a/ext/libxml/php_libxml.h b/ext/libxml/php_libxml.h +index 5021a3d43f..db5dfad66d 100644 +--- a/ext/libxml/php_libxml.h ++++ b/ext/libxml/php_libxml.h +@@ -122,6 +122,42 @@ PHP_LIBXML_API void php_libxml_shutdown(void); + ZEND_TSRMLS_CACHE_EXTERN() + #endif + ++/* Other extension may override the global state options, these global options ++ * are copied initially to ctxt->options. Set the options to a known good value. ++ * See libxml2 globals.c and parserInternals.c. ++ * The unique_name argument allows multiple sanitizes and restores within the ++ * same function, even nested is necessary. */ ++#define PHP_LIBXML_SANITIZE_GLOBALS(unique_name) \ ++ int xml_old_loadsubset_##unique_name = xmlLoadExtDtdDefaultValue; \ ++ xmlLoadExtDtdDefaultValue = 0; \ ++ int xml_old_validate_##unique_name = xmlDoValidityCheckingDefaultValue; \ ++ xmlDoValidityCheckingDefaultValue = 0; \ ++ int xml_old_pedantic_##unique_name = xmlPedanticParserDefault(0); \ ++ int xml_old_substitute_##unique_name = xmlSubstituteEntitiesDefault(0); \ ++ int xml_old_linenrs_##unique_name = xmlLineNumbersDefault(0); \ ++ int xml_old_blanks_##unique_name = xmlKeepBlanksDefault(1); ++ ++#define PHP_LIBXML_RESTORE_GLOBALS(unique_name) \ ++ xmlLoadExtDtdDefaultValue = xml_old_loadsubset_##unique_name; \ ++ xmlDoValidityCheckingDefaultValue = xml_old_validate_##unique_name; \ ++ (void) xmlPedanticParserDefault(xml_old_pedantic_##unique_name); \ ++ (void) xmlSubstituteEntitiesDefault(xml_old_substitute_##unique_name); \ ++ (void) xmlLineNumbersDefault(xml_old_linenrs_##unique_name); \ ++ (void) xmlKeepBlanksDefault(xml_old_blanks_##unique_name); ++ ++/* Alternative for above, working directly on the context and not setting globals. ++ * Generally faster because no locking is involved, and this has the advantage that it sets the options to a known good value. */ ++static zend_always_inline void php_libxml_sanitize_parse_ctxt_options(xmlParserCtxtPtr ctxt) ++{ ++ ctxt->loadsubset = 0; ++ ctxt->validate = 0; ++ ctxt->pedantic = 0; ++ ctxt->replaceEntities = 0; ++ ctxt->linenumbers = 0; ++ ctxt->keepBlanks = 1; ++ ctxt->options = 0; ++} ++ + #else /* HAVE_LIBXML */ + #define libxml_module_ptr NULL + #endif +diff --git a/ext/simplexml/simplexml.c b/ext/simplexml/simplexml.c +index 0637e06af8..435096e9f5 100644 +--- a/ext/simplexml/simplexml.c ++++ b/ext/simplexml/simplexml.c +@@ -2229,7 +2229,9 @@ PHP_FUNCTION(simplexml_load_file) + RETURN_FALSE; + } + ++ PHP_LIBXML_SANITIZE_GLOBALS(read_file); + docp = xmlReadFile(filename, NULL, (int)options); ++ PHP_LIBXML_RESTORE_GLOBALS(read_file); + + if (!docp) { + RETURN_FALSE; +@@ -2283,7 +2285,9 @@ PHP_FUNCTION(simplexml_load_string) + RETURN_FALSE; + } + ++ PHP_LIBXML_SANITIZE_GLOBALS(read_memory); + docp = xmlReadMemory(data, (int)data_len, NULL, NULL, (int)options); ++ PHP_LIBXML_RESTORE_GLOBALS(read_memory); + + if (!docp) { + RETURN_FALSE; +@@ -2333,7 +2337,9 @@ SXE_METHOD(__construct) + return; + } + ++ PHP_LIBXML_SANITIZE_GLOBALS(read_file_or_memory); + docp = is_url ? xmlReadFile(data, NULL, (int)options) : xmlReadMemory(data, (int)data_len, NULL, NULL, (int)options); ++ PHP_LIBXML_RESTORE_GLOBALS(read_file_or_memory); + + if (!docp) { + ((php_libxml_node_object *)sxe)->document = NULL; +diff --git a/ext/soap/php_xml.c b/ext/soap/php_xml.c +index a9c6a56858..84366e96c9 100644 +--- a/ext/soap/php_xml.c ++++ b/ext/soap/php_xml.c +@@ -94,6 +94,7 @@ xmlDocPtr soap_xmlParseFile(const char *filename) + if (ctxt) { + zend_bool old; + ++ php_libxml_sanitize_parse_ctxt_options(ctxt); + ctxt->keepBlanks = 0; + ctxt->sax->ignorableWhitespace = soap_ignorableWhitespace; + ctxt->sax->comment = soap_Comment; +@@ -144,6 +145,7 @@ xmlDocPtr soap_xmlParseMemory(const void *buf, size_t buf_size) + if (ctxt) { + zend_bool old; + ++ php_libxml_sanitize_parse_ctxt_options(ctxt); + ctxt->sax->ignorableWhitespace = soap_ignorableWhitespace; + ctxt->sax->comment = soap_Comment; + ctxt->sax->warning = NULL; +diff --git a/ext/xml/compat.c b/ext/xml/compat.c +index 694fde95c3..02d1093b9f 100644 +--- a/ext/xml/compat.c ++++ b/ext/xml/compat.c +@@ -19,6 +19,7 @@ + #include "php.h" + #if defined(HAVE_LIBXML) && (defined(HAVE_XML) || defined(HAVE_XMLRPC)) && !defined(HAVE_LIBEXPAT) + #include "expat_compat.h" ++#include "ext/libxml/php_libxml.h" + + typedef struct _php_xml_ns { + xmlNsPtr nsptr; +@@ -473,6 +474,7 @@ XML_ParserCreate_MM(const XML_Char *encoding, const XML_Memory_Handling_Suite *m + parser->parser->charset = XML_CHAR_ENCODING_NONE; + #endif + ++ php_libxml_sanitize_parse_ctxt_options(parser->parser); + #if LIBXML_VERSION >= 20703 + xmlCtxtUseOptions(parser->parser, XML_PARSE_OLDSAX); + #endif +diff --git a/ext/xmlreader/php_xmlreader.c b/ext/xmlreader/php_xmlreader.c +index 87a03f9bf7..8f03fa57ed 100644 +--- a/ext/xmlreader/php_xmlreader.c ++++ b/ext/xmlreader/php_xmlreader.c +@@ -301,6 +301,7 @@ static xmlRelaxNGPtr _xmlreader_get_relaxNG(char *source, size_t source_len, siz + return NULL; + } + ++ PHP_LIBXML_SANITIZE_GLOBALS(parse); + if (error_func || warn_func) { + xmlRelaxNGSetParserErrors(parser, + (xmlRelaxNGValidityErrorFunc) error_func, +@@ -309,6 +310,7 @@ static xmlRelaxNGPtr _xmlreader_get_relaxNG(char *source, size_t source_len, siz + } + sptr = xmlRelaxNGParse(parser); + xmlRelaxNGFreeParserCtxt(parser); ++ PHP_LIBXML_RESTORE_GLOBALS(parse); + + return sptr; + } +@@ -881,7 +883,9 @@ PHP_METHOD(xmlreader, open) + valid_file = _xmlreader_get_valid_file_path(source, resolved_path, MAXPATHLEN ); + + if (valid_file) { ++ PHP_LIBXML_SANITIZE_GLOBALS(reader_for_file); + reader = xmlReaderForFile(valid_file, encoding, options); ++ PHP_LIBXML_RESTORE_GLOBALS(reader_for_file); + } + + if (reader == NULL) { +@@ -959,7 +963,9 @@ PHP_METHOD(xmlreader, setSchema) + + intern = Z_XMLREADER_P(id); + if (intern && intern->ptr) { ++ PHP_LIBXML_SANITIZE_GLOBALS(schema); + retval = xmlTextReaderSchemaValidate(intern->ptr, source); ++ PHP_LIBXML_RESTORE_GLOBALS(schema); + + if (retval == 0) { + RETURN_TRUE; +@@ -1079,6 +1085,7 @@ PHP_METHOD(xmlreader, XML) + } + uri = (char *) xmlCanonicPath((const xmlChar *) resolved_path); + } ++ PHP_LIBXML_SANITIZE_GLOBALS(text_reader); + reader = xmlNewTextReader(inputbfr, uri); + + if (reader != NULL) { +@@ -1099,9 +1106,11 @@ PHP_METHOD(xmlreader, XML) + xmlFree(uri); + } + ++ PHP_LIBXML_RESTORE_GLOBALS(text_reader); + return; + } + } ++ PHP_LIBXML_RESTORE_GLOBALS(text_reader); + } + + if (uri) { +diff --git a/ext/xsl/xsltprocessor.c b/ext/xsl/xsltprocessor.c +index d12da0e655..3e9031531b 100644 +--- a/ext/xsl/xsltprocessor.c ++++ b/ext/xsl/xsltprocessor.c +@@ -399,7 +399,7 @@ PHP_FUNCTION(xsl_xsltprocessor_import_stylesheet) + xmlDoc *doc = NULL, *newdoc = NULL; + xsltStylesheetPtr sheetp, oldsheetp; + xsl_object *intern; +- int prevSubstValue, prevExtDtdValue, clone_docu = 0; ++ int clone_docu = 0; + xmlNode *nodep = NULL; + zend_object_handlers *std_hnd; + zval *cloneDocu, member, rv; +@@ -422,13 +422,12 @@ PHP_FUNCTION(xsl_xsltprocessor_import_stylesheet) + stylesheet document otherwise the node proxies will be a mess */ + newdoc = xmlCopyDoc(doc, 1); + xmlNodeSetBase((xmlNodePtr) newdoc, (xmlChar *)doc->URL); +- prevSubstValue = xmlSubstituteEntitiesDefault(1); +- prevExtDtdValue = xmlLoadExtDtdDefaultValue; ++ PHP_LIBXML_SANITIZE_GLOBALS(parse); ++ xmlSubstituteEntitiesDefault(1); + xmlLoadExtDtdDefaultValue = XML_DETECT_IDS | XML_COMPLETE_ATTRS; + + sheetp = xsltParseStylesheetDoc(newdoc); +- xmlSubstituteEntitiesDefault(prevSubstValue); +- xmlLoadExtDtdDefaultValue = prevExtDtdValue; ++ PHP_LIBXML_RESTORE_GLOBALS(parse); + + if (!sheetp) { + xmlFreeDoc(newdoc); +-- +2.41.0 + +From 59ec21e12409bd87106f3437dbcc680608eb85a8 Mon Sep 17 00:00:00 2001 +From: Remi Collet <remi@remirepo.net> +Date: Tue, 1 Aug 2023 07:22:33 +0200 +Subject: [PATCH 3/3] NEWS + +(cherry picked from commit ef1d507acf7be23d7624dc3c891683b2218feb51) +(cherry picked from commit 3cf7c2b10e577136b267f2d90bfdff6743271c5c) +(cherry picked from commit 79c0bf87711036b83f8ee1723c034ccc839d847b) +(cherry picked from commit 3ac0ce8a462cb31815330d1410e1a8a615c395eb) +--- + NEWS | 10 ++++++++++ + 1 file changed, 10 insertions(+) + +diff --git a/NEWS b/NEWS +index 835c538663..53ca87c8e6 100644 +--- a/NEWS ++++ b/NEWS +@@ -1,6 +1,16 @@ + PHP NEWS + ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| + ++Backported from 8.0.30 ++ ++- Libxml: ++ . Fixed bug GHSA-3qrf-m4j2-pcrr (Security issue with external entity loading ++ in XML without enabling it). (CVE-2023-3823) (nielsdos, ilutov) ++ ++- Phar: ++ . Fixed bug GHSA-jqcx-ccgc-xwhv (Buffer mismanagement in phar_dir_read()). ++ (CVE-2023-3824) (nielsdos) ++ + Backported from 8.0.29 + + - Soap: +-- +2.41.0 + diff --git a/php-cve-2024-11233.patch b/php-cve-2024-11233.patch new file mode 100644 index 0000000..7d98ce5 --- /dev/null +++ b/php-cve-2024-11233.patch @@ -0,0 +1,162 @@ +From 9f08040f58aab60a13cbc06013cf684a9537342a 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 6/9] 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) +(cherry picked from commit 44a5975f83a02eb8169d12af912e6222b28216d0) +(cherry picked from commit 7065fa31a468139f07b40f7036ce4761037dafd2) +(cherry picked from commit 0a651c02701268532a9754542f629af85e28ae02) +(cherry picked from commit 0abb863a23d132c4c4d0dd996f526da087dc1c05) +--- + 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 3dd66e8ae6..21499484bf 100644 +--- a/ext/standard/filters.c ++++ b/ext/standard/filters.c +@@ -1124,6 +1124,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; +@@ -1137,15 +1140,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 0000000000..8fdcce8ff2 +--- /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 + +From 0535796c578ac643141db13d95ef4cd82b5dcef9 Mon Sep 17 00:00:00 2001 +From: "Christoph M. Becker" <cmbecker69@gmx.de> +Date: Mon, 8 Jun 2020 23:19:43 +0200 +Subject: [PATCH 7/9] Fix #74267: segfault with streams and invalid data + +If the current character is a line break character, it cannot be a tab +or space character, so we would always fail with an invalid sequence +error. Obviously, these `scan_stat == 4` conditions are meant to be +exclusive. + +Furthermore, if `in_pp == NULL || in_left_p == NULL` is true, we hit a +segfault if we are not returning right away. Obviously, the additional +constraints don't make sense, so we remove them. + +(cherry picked from commit 12c59f6660706321f9d42c55421ff6864439c8b7) +(cherry picked from commit d149a44b005e0d24208b0bd0f7179dfbbfffefb1) +(cherry picked from commit c173da70f301433a5d49df1d53a7930f92974820) +--- + ext/standard/filters.c | 7 +++---- + ext/standard/tests/filters/bug74267.phpt | 26 ++++++++++++++++++++++++ + 2 files changed, 29 insertions(+), 4 deletions(-) + create mode 100644 ext/standard/tests/filters/bug74267.phpt + +diff --git a/ext/standard/filters.c b/ext/standard/filters.c +index 21499484bf..c17cf1e033 100644 +--- a/ext/standard/filters.c ++++ b/ext/standard/filters.c +@@ -790,7 +790,7 @@ static php_conv_err_t php_conv_qprint_encode_convert(php_conv_qprint_encode *ins + lb_ptr = inst->lb_ptr; + lb_cnt = inst->lb_cnt; + +- if ((in_pp == NULL || in_left_p == NULL) && (lb_ptr >=lb_cnt)) { ++ if (in_pp == NULL || in_left_p == NULL) { + return PHP_CONV_ERR_SUCCESS; + } + +@@ -1018,7 +1018,7 @@ static php_conv_err_t php_conv_qprint_decode_convert(php_conv_qprint_decode *ins + lb_ptr = inst->lb_ptr; + lb_cnt = inst->lb_cnt; + +- if ((in_pp == NULL || in_left_p == NULL) && lb_cnt == lb_ptr) { ++ if (in_pp == NULL || in_left_p == NULL) { + if (inst->scan_stat != 0) { + return PHP_CONV_ERR_UNEXPECTED_EOS; + } +@@ -1115,8 +1115,7 @@ static php_conv_err_t php_conv_qprint_decode_convert(php_conv_qprint_decode *ins + *ps == (unsigned char)inst->lbchars[lb_cnt]) { + lb_cnt++; + scan_stat = 5; +- } +- if (*ps != '\t' && *ps != ' ') { ++ } else if (*ps != '\t' && *ps != ' ') { + err = PHP_CONV_ERR_INVALID_SEQ; + goto out; + } +diff --git a/ext/standard/tests/filters/bug74267.phpt b/ext/standard/tests/filters/bug74267.phpt +new file mode 100644 +index 0000000000..17d7996b7f +--- /dev/null ++++ b/ext/standard/tests/filters/bug74267.phpt +@@ -0,0 +1,26 @@ ++--TEST-- ++Bug #74267 (segfault with streams and invalid data) ++--FILE-- ++<?php ++$stream = fopen('php://memory', 'w'); ++stream_filter_append($stream, 'convert.quoted-printable-decode', STREAM_FILTER_WRITE, ['line-break-chars' => "\r\n"]); ++ ++$lines = [ ++ "\r\n", ++ " -=()\r\n", ++ " -=\r\n", ++ "\r\n" ++ ]; ++ ++foreach ($lines as $line) { ++ fwrite($stream, $line); ++} ++ ++fclose($stream); ++echo "done\n"; ++?> ++--EXPECTF-- ++Warning: fwrite(): stream filter (convert.quoted-printable-decode): invalid byte sequence in %s on line %d ++ ++Warning: fwrite(): stream filter (convert.quoted-printable-decode): invalid byte sequence in %s on line %d ++done +-- +2.47.0 + diff --git a/php-cve-2024-11234.patch b/php-cve-2024-11234.patch new file mode 100644 index 0000000..a4de455 --- /dev/null +++ b/php-cve-2024-11234.patch @@ -0,0 +1,99 @@ +From 8dab7d0bb9c4133a082c70403af0c6a4c1b0025b Mon Sep 17 00:00:00 2001 +From: Jakub Zelenka <bukka@php.net> +Date: Fri, 8 Nov 2024 23:43:47 +0100 +Subject: [PATCH 4/9] 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) +(cherry picked from commit 494de65139592da0e5e5b6fdf198c2f9c762f4d6) +(cherry picked from commit dcb89ed9d0217510f3906ce0c517f704e6bd80dc) +(cherry picked from commit 11787051a17d2fcea427cd66c3fcc5e99ab94a03) +(cherry picked from commit 59bfc165234a2bb79916c340cd98d011deedc995) +--- + 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 ab10ed11b6..b1a13ee1cf 100644 +--- a/ext/standard/http_fopen_wrapper.c ++++ b/ext/standard/http_fopen_wrapper.c +@@ -182,6 +182,11 @@ 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 && (strlen(resource->scheme) > 4) && resource->scheme[4] == 's'; + /* choose default ports */ + if (use_ssl && resource->port == 0) +@@ -201,6 +206,13 @@ 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 +@@ -387,12 +399,6 @@ finish: + strncpy(scratch, "GET ", scratch_len); + } + +- /* 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 */ + strcat(scratch, 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 0000000000..6e68f67654 +--- /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..ad9cd5c --- /dev/null +++ b/php-cve-2024-11236.patch @@ -0,0 +1,150 @@ +From d55e216044e111faa5fc60503bb977d5792d2068 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/9] Fix GHSA-5hqh-c84r-qjcv: Integer overflow in the dblib + quoter causing OOB writes + +(cherry picked from commit d9baa9fed8c3ba692a36b388c0c7762e5102e2e0) +(cherry picked from commit 5d9e54065ed18c51e4f25d8900635f90810c7394) +(cherry picked from commit 97546df8d6900b115536c17af9213f1da837b82e) +(cherry picked from commit 5e7cd3e7ed7c894550ca35514708ffe1874a31ad) +(cherry picked from commit c6ee9a7d0385e4cd6cf9dcd0104dd6714e2a968d) +(cherry picked from commit cee60784e8c25da9a5be7c6012ffef483d124d33) +--- + 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 c53ac89d26..9a539aec3f 100644 +--- a/ext/pdo_dblib/dblib_driver.c ++++ b/ext/pdo_dblib/dblib_driver.c +@@ -154,15 +154,21 @@ 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; + + /* 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; + } + + *quotedlen += 2; /* +2 for opening, closing quotes */ ++ if (UNEXPECTED(*quotedlen > ZSTR_MAX_LEN - extralen)) { ++ return 0; ++ } ++ ++ *quotedlen += extralen; + q = *quoted = emalloc(*quotedlen+1); /* Add byte for terminal null */ + *q++ = '\''; + +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 0000000000..431c61951e +--- /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 1746cbb4e93490be51bbd0f0146a7b01cdecb135 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/9] Fix GHSA-5hqh-c84r-qjcv: Integer overflow in the firebird + quoter causing OOB writes + +(cherry picked from commit 69c5f68fdc3deed9ebce2cc44b4bf5e0c47cd28f) +(cherry picked from commit b4f73be75dbdde970a18cc7a636898b10400fb3f) +(cherry picked from commit 0530cbfe5c3044537de52d8382eba5d69dbac726) +(cherry picked from commit 72d4c4e435544c2d87d634188d480099345b601b) +(cherry picked from commit 8a4f389396493a43f9de9ba48920b6a82b6d1370) +(cherry picked from commit fe1067ac5b79aaa3b691e6ef7fb4d36dcf9a6fff) +--- + 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 48808d6f3d..de9d1971bd 100644 +--- a/ext/pdo_firebird/firebird_driver.c ++++ b/ext/pdo_firebird/firebird_driver.c +@@ -290,7 +290,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; + +@@ -305,6 +305,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 + +From 46d6c59da7c630627b7b2c65109a1f309c525ae4 Mon Sep 17 00:00:00 2001 +From: Remi Collet <remi@remirepo.net> +Date: Fri, 22 Nov 2024 15:24:16 +0100 +Subject: [PATCH 3/9] backport ZSTR_MAX_LEN + +(cherry picked from commit 37056ad634d9c44bac0d6c8e730eafaec1344840) +(cherry picked from commit ff868946218d6d1661a0c35757e2058cb3ed23ec) +(cherry picked from commit 351dee5281697c14712e261f560d671e1661e51a) +--- + Zend/zend_string.h | 3 +++ + 1 file changed, 3 insertions(+) + +diff --git a/Zend/zend_string.h b/Zend/zend_string.h +index 113e5cacba..e3286e17f7 100644 +--- a/Zend/zend_string.h ++++ b/Zend/zend_string.h +@@ -61,6 +61,9 @@ END_EXTERN_C() + + #define _ZSTR_STRUCT_SIZE(len) (_ZSTR_HEADER_SIZE + len + 1) + ++#define ZSTR_MAX_OVERHEAD (ZEND_MM_ALIGNED_SIZE(_ZSTR_HEADER_SIZE + 1)) ++#define ZSTR_MAX_LEN (SIZE_MAX - ZSTR_MAX_OVERHEAD) ++ + #define ZSTR_ALLOCA_ALLOC(str, _len, use_heap) do { \ + (str) = (zend_string *)do_alloca(ZEND_MM_ALIGNED_SIZE_EX(_ZSTR_STRUCT_SIZE(_len), 8), (use_heap)); \ + GC_REFCOUNT(str) = 1; \ +-- +2.47.0 + diff --git a/php-cve-2024-2756.patch b/php-cve-2024-2756.patch new file mode 100644 index 0000000..7a9e942 --- /dev/null +++ b/php-cve-2024-2756.patch @@ -0,0 +1,201 @@ +From ec9b61593fa2b9400d4519b9969645c1266a381d Mon Sep 17 00:00:00 2001 +From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> +Date: Sun, 17 Mar 2024 21:04:47 +0100 +Subject: [PATCH 1/4] Fix GHSA-wpj3-hf5j-x4v4: __Host-/__Secure- cookie bypass + due to partial CVE-2022-31629 fix + +The check happened too early as later code paths may perform more +mangling rules. Move the check downwards right before adding the actual +variable. + +(cherry picked from commit 093c08af25fb323efa0c8e6154aa9fdeae3d3b53) +(cherry picked from commit 2e07a3acd7a6b53c55325b94bed97748d7697b53) +(cherry picked from commit a6c1c62a25ac23b08a86af11d68f0e2eaafc102b) +(cherry picked from commit 46b570a1e4aeb4a414898fcc09503ac388d16256) +(cherry picked from commit c213de619a532d35e8f7abe4a245433dbf21c960) +(cherry picked from commit a1b0060906bc4eedaf5bb3577a0d6d4b0e6b9dfd) +--- + ext/standard/tests/ghsa-wpj3-hf5j-x4v4.phpt | 63 +++++++++++++++++++++ + main/php_variables.c | 41 +++++++++----- + 2 files changed, 90 insertions(+), 14 deletions(-) + create mode 100644 ext/standard/tests/ghsa-wpj3-hf5j-x4v4.phpt + +diff --git a/ext/standard/tests/ghsa-wpj3-hf5j-x4v4.phpt b/ext/standard/tests/ghsa-wpj3-hf5j-x4v4.phpt +new file mode 100644 +index 0000000000..77fcb68089 +--- /dev/null ++++ b/ext/standard/tests/ghsa-wpj3-hf5j-x4v4.phpt +@@ -0,0 +1,63 @@ ++--TEST-- ++ghsa-wpj3-hf5j-x4v4 (__Host-/__Secure- cookie bypass due to partial CVE-2022-31629 fix) ++--COOKIE-- ++..Host-test=ignore_1; ++._Host-test=ignore_2; ++.[Host-test=ignore_3; ++_.Host-test=ignore_4; ++__Host-test=ignore_5; ++_[Host-test=ignore_6; ++[.Host-test=ignore_7; ++[_Host-test=ignore_8; ++[[Host-test=ignore_9; ++..Host-test[]=ignore_10; ++._Host-test[]=ignore_11; ++.[Host-test[]=ignore_12; ++_.Host-test[]=ignore_13; ++__Host-test[]=legitimate_14; ++_[Host-test[]=legitimate_15; ++[.Host-test[]=ignore_16; ++[_Host-test[]=ignore_17; ++[[Host-test[]=ignore_18; ++..Secure-test=ignore_1; ++._Secure-test=ignore_2; ++.[Secure-test=ignore_3; ++_.Secure-test=ignore_4; ++__Secure-test=ignore_5; ++_[Secure-test=ignore_6; ++[.Secure-test=ignore_7; ++[_Secure-test=ignore_8; ++[[Secure-test=ignore_9; ++..Secure-test[]=ignore_10; ++._Secure-test[]=ignore_11; ++.[Secure-test[]=ignore_12; ++_.Secure-test[]=ignore_13; ++__Secure-test[]=legitimate_14; ++_[Secure-test[]=legitimate_15; ++[.Secure-test[]=ignore_16; ++[_Secure-test[]=ignore_17; ++[[Secure-test[]=ignore_18; ++--FILE-- ++<?php ++var_dump($_COOKIE); ++?> ++--EXPECT-- ++array(3) { ++ ["__Host-test"]=> ++ array(1) { ++ [0]=> ++ string(13) "legitimate_14" ++ } ++ ["_"]=> ++ array(2) { ++ ["Host-test["]=> ++ string(13) "legitimate_15" ++ ["Secure-test["]=> ++ string(13) "legitimate_15" ++ } ++ ["__Secure-test"]=> ++ array(1) { ++ [0]=> ++ string(13) "legitimate_14" ++ } ++} +diff --git a/main/php_variables.c b/main/php_variables.c +index e5d3ffcf52..46de3477ca 100644 +--- a/main/php_variables.c ++++ b/main/php_variables.c +@@ -53,6 +53,21 @@ PHPAPI void php_register_variable_safe(char *var, char *strval, size_t str_len, + php_register_variable_ex(var, &new_entry, track_vars_array); + } + ++/* Discard variable if mangling made it start with __Host-, where pre-mangling it did not start with __Host- ++ * Discard variable if mangling made it start with __Secure-, where pre-mangling it did not start with __Secure- */ ++static zend_bool php_is_forbidden_variable_name(const char *mangled_name, size_t mangled_name_len, const char *pre_mangled_name) ++{ ++ if (mangled_name_len >= sizeof("__Host-")-1 && strncmp(mangled_name, "__Host-", sizeof("__Host-")-1) == 0 && strncmp(pre_mangled_name, "__Host-", sizeof("__Host-")-1) != 0) { ++ return 1; ++ } ++ ++ if (mangled_name_len >= sizeof("__Secure-")-1 && strncmp(mangled_name, "__Secure-", sizeof("__Secure-")-1) == 0 && strncmp(pre_mangled_name, "__Secure-", sizeof("__Secure-")-1) != 0) { ++ return 1; ++ } ++ ++ return 0; ++} ++ + PHPAPI void php_register_variable_ex(char *var_name, zval *val, zval *track_vars_array) + { + char *p = NULL; +@@ -103,20 +118,6 @@ PHPAPI void php_register_variable_ex(char *var_name, zval *val, zval *track_vars + } + var_len = p - var; + +- /* Discard variable if mangling made it start with __Host-, where pre-mangling it did not start with __Host- */ +- if (strncmp(var, "__Host-", sizeof("__Host-")-1) == 0 && strncmp(var_name, "__Host-", sizeof("__Host-")-1) != 0) { +- zval_ptr_dtor_nogc(val); +- free_alloca(var_orig, use_heap); +- return; +- } +- +- /* Discard variable if mangling made it start with __Secure-, where pre-mangling it did not start with __Secure- */ +- if (strncmp(var, "__Secure-", sizeof("__Secure-")-1) == 0 && strncmp(var_name, "__Secure-", sizeof("__Secure-")-1) != 0) { +- zval_ptr_dtor_nogc(val); +- free_alloca(var_orig, use_heap); +- return; +- } +- + if (var_len==0) { /* empty variable name, or variable name with a space in it */ + zval_dtor(val); + free_alloca(var_orig, use_heap); +@@ -194,6 +195,12 @@ PHPAPI void php_register_variable_ex(char *var_name, zval *val, zval *track_vars + return; + } + } else { ++ if (php_is_forbidden_variable_name(index, index_len, var_name)) { ++ zval_ptr_dtor_nogc(val); ++ free_alloca(var_orig, use_heap); ++ return; ++ } ++ + gpc_element_p = zend_symtable_str_find(symtable1, index, index_len); + if (!gpc_element_p) { + zval tmp; +@@ -230,6 +237,12 @@ plain_var: + zval_ptr_dtor(&gpc_element); + } + } else { ++ if (php_is_forbidden_variable_name(index, index_len, var_name)) { ++ zval_ptr_dtor_nogc(val); ++ free_alloca(var_orig, use_heap); ++ return; ++ } ++ + /* + * According to rfc2965, more specific paths are listed above the less specific ones. + * If we encounter a duplicate cookie name, we should skip it, since it is not possible +-- +2.44.0 + +From d8e42d4a8471e19710dbb60018ed956eed34af90 Mon Sep 17 00:00:00 2001 +From: Remi Collet <remi@remirepo.net> +Date: Wed, 10 Apr 2024 08:59:32 +0200 +Subject: [PATCH 2/4] NEWS + +(cherry picked from commit 366cc249b7d54707572beb7096e8f6c65ee79719) +(cherry picked from commit dcdd49ef3bfbd8ccc778850d6a0f9b98adf625d4) +(cherry picked from commit 8642473b624f809b768180b104c013f74e3a99a0) +(cherry picked from commit ee591001f7a3db7405b4fa027659768c2355df6d) +(cherry picked from commit 035bc48bafe5d567f4ab8de6d1752a724e361690) +--- + NEWS | 6 ++++++ + 1 file changed, 6 insertions(+) + +diff --git a/NEWS b/NEWS +index 53ca87c8e6..fae611c48c 100644 +--- a/NEWS ++++ b/NEWS +@@ -1,6 +1,12 @@ + PHP NEWS + ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| + ++Backported from 8.1.28 ++ ++- Standard: ++ . Fixed bug GHSA-wpj3-hf5j-x4v4 (__Host-/__Secure- cookie bypass due to ++ partial CVE-2022-31629 fix). (CVE-2024-2756) (nielsdos) ++ + Backported from 8.0.30 + + - Libxml: +-- +2.44.0 + diff --git a/php-cve-2024-3096.patch b/php-cve-2024-3096.patch new file mode 100644 index 0000000..7ba38c1 --- /dev/null +++ b/php-cve-2024-3096.patch @@ -0,0 +1,88 @@ +From 459b4ac6a8d9bec32110b68ac194d71ec2b72182 Mon Sep 17 00:00:00 2001 +From: Jakub Zelenka <bukka@php.net> +Date: Fri, 29 Mar 2024 15:27:59 +0000 +Subject: [PATCH 3/4] Fix bug GHSA-q6x7-frmf-grcw: password_verify can + erroneously return true + +Disallow null character in bcrypt password + +(cherry picked from commit 0ba5229a3f7572846e91c8f5382e87785f543826) +(cherry picked from commit 81794c73068d9a44bf109bbcc9793e7b56a1c051) +(cherry picked from commit 4a7ceb9d6427f8d368f1a8739267b1f8310ec201) +(cherry picked from commit 747100905eceffb1f67096b437001e42900eb6bb) +(cherry picked from commit d22d9ebb29dce86edd622205dd1196a2796c08c7) +(cherry picked from commit cd9a376c28c6f4ce83aab53ec069234fe1d2a819) +--- + ext/standard/password.c | 5 +++++ + ext/standard/tests/password/password_bcrypt_errors.phpt | 4 ++++ + 2 files changed, 9 insertions(+) + +diff --git a/ext/standard/password.c b/ext/standard/password.c +index 33c6e5c718..e2466d1179 100644 +--- a/ext/standard/password.c ++++ b/ext/standard/password.c +@@ -283,6 +283,11 @@ PHP_FUNCTION(password_hash) + cost = zval_get_long(option_buffer); + } + ++ if (memchr(password, '\0', password_len)) { ++ php_error_docref(NULL, E_WARNING, "Bcrypt password must not contain null character"); ++ RETURN_NULL(); ++ } ++ + if (cost < 4 || cost > 31) { + php_error_docref(NULL, E_WARNING, "Invalid bcrypt cost parameter specified: " ZEND_LONG_FMT, cost); + RETURN_NULL(); +diff --git a/ext/standard/tests/password/password_bcrypt_errors.phpt b/ext/standard/tests/password/password_bcrypt_errors.phpt +index e8f7600b63..f95b72670a 100644 +--- a/ext/standard/tests/password/password_bcrypt_errors.phpt ++++ b/ext/standard/tests/password/password_bcrypt_errors.phpt +@@ -16,6 +16,8 @@ var_dump(password_hash("foo", PASSWORD_BCRYPT, array("salt" => 123))); + + var_dump(password_hash("foo", PASSWORD_BCRYPT, array("cost" => "foo"))); + ++var_dump(password_hash("null\0password", PASSWORD_BCRYPT)); ++ + ?> + --EXPECTF-- + Warning: password_hash(): Invalid bcrypt cost parameter specified: 3 in %s on line %d +@@ -42,4 +44,6 @@ NULL + Warning: password_hash(): Invalid bcrypt cost parameter specified: 0 in %s on line %d + NULL + ++Warning: password_hash(): Bcrypt password must not contain null character in %s on line %d ++NULL + +-- +2.44.0 + +From d339e614f1e4cbf1aeb5fbee76bb0583885aeb30 Mon Sep 17 00:00:00 2001 +From: Remi Collet <remi@remirepo.net> +Date: Wed, 10 Apr 2024 09:01:09 +0200 +Subject: [PATCH 4/4] NEWS + +(cherry picked from commit 24f77904ee2259d722559f129f96a1f145a2367b) +(cherry picked from commit 027bdbc636632be49ecfad8d4191509faacb34ac) +(cherry picked from commit fbeed182bb0b0c4c453e064198b5cc3814a10de0) +(cherry picked from commit be830600a8e4c33a25e965d0782903e885e91c6d) +(cherry picked from commit 9ec5a1ed8bed7ca5a14e991ff3e767dbfa773dcd) +--- + NEWS | 2 ++ + 1 file changed, 2 insertions(+) + +diff --git a/NEWS b/NEWS +index fae611c48c..fed03499f6 100644 +--- a/NEWS ++++ b/NEWS +@@ -6,6 +6,8 @@ Backported from 8.1.28 + - Standard: + . Fixed bug GHSA-wpj3-hf5j-x4v4 (__Host-/__Secure- cookie bypass due to + partial CVE-2022-31629 fix). (CVE-2024-2756) (nielsdos) ++ . Fixed bug GHSA-h746-cjrr-wfmr (password_verify can erroneously return true, ++ opening ATO risk). (CVE-2024-3096) (Jakub Zelenka) + + Backported from 8.0.30 + +-- +2.44.0 + diff --git a/php-cve-2024-5458.patch b/php-cve-2024-5458.patch new file mode 100644 index 0000000..88c4db2 --- /dev/null +++ b/php-cve-2024-5458.patch @@ -0,0 +1,188 @@ +From f232d87846394315071ae115fcb8f1c4d1771eb3 Mon Sep 17 00:00:00 2001 +From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> +Date: Wed, 22 May 2024 22:25:02 +0200 +Subject: [PATCH 1/2] Fix GHSA-w8qr-v226-r27w + +We should not early-out with success status if we found an ipv6 +hostname, we should keep checking the rest of the conditions. +Because integrating the if-check of the ipv6 hostname in the +"Validate domain" if-check made the code hard to read, I extracted the +condition out to a separate function. This also required to make +a few pointers const in order to have some clean code. + +(cherry picked from commit 4066610b47e22c24cbee91be434a94357056a479) +(cherry picked from commit 08be64e40197fc12dca5f802d16748d9c3cb4cb4) +(cherry picked from commit 76362f9526afbd5565003d981f9507aaf62337f2) +(cherry picked from commit 87a7b8dab75e221a1fcd74cf34dc650f7253c12c) +(cherry picked from commit b919ad0323dbc3e1e1ac0f6ba8ec2ad380579918) +--- + ext/filter/logical_filters.c | 35 ++++++++++--------- + ext/filter/tests/ghsa-w8qr-v226-r27w.phpt | 41 +++++++++++++++++++++++ + 2 files changed, 61 insertions(+), 15 deletions(-) + create mode 100644 ext/filter/tests/ghsa-w8qr-v226-r27w.phpt + +diff --git a/ext/filter/logical_filters.c b/ext/filter/logical_filters.c +index 7a37f80e46..76a753f60c 100644 +--- a/ext/filter/logical_filters.c ++++ b/ext/filter/logical_filters.c +@@ -81,7 +81,7 @@ + #define FORMAT_IPV4 4 + #define FORMAT_IPV6 6 + +-static int _php_filter_validate_ipv6(char *str, size_t str_len); ++static int _php_filter_validate_ipv6(const char *str, size_t str_len); + + static int php_filter_parse_int(const char *str, size_t str_len, zend_long *ret) { /* {{{ */ + zend_long ctx_value; +@@ -532,6 +532,14 @@ static int is_userinfo_valid(char *str) + return 1; + } + ++static zend_bool php_filter_is_valid_ipv6_hostname(const char *s, size_t l) ++{ ++ const char *e = s + l; ++ const char *t = e - 1; ++ ++ return *s == '[' && *t == ']' && _php_filter_validate_ipv6(s + 1, l - 2); ++} ++ + void php_filter_validate_url(PHP_INPUT_FILTER_PARAM_DECL) /* {{{ */ + { + php_url *url; +@@ -551,7 +559,7 @@ void php_filter_validate_url(PHP_INPUT_FILTER_PARAM_DECL) /* {{{ */ + } + + if (url->scheme != NULL && (!strcasecmp(url->scheme, "http") || !strcasecmp(url->scheme, "https"))) { +- char *e, *s, *t; ++ const char *s; + size_t l; + + if (url->host == NULL) { +@@ -560,17 +568,14 @@ void php_filter_validate_url(PHP_INPUT_FILTER_PARAM_DECL) /* {{{ */ + + s = url->host; + l = strlen(s); +- e = url->host + l; +- t = e - 1; +- +- /* An IPv6 enclosed by square brackets is a valid hostname */ +- if (*s == '[' && *t == ']' && _php_filter_validate_ipv6((s + 1), l - 2)) { +- php_url_free(url); +- return; +- } + +- // Validate domain +- if (!_php_filter_validate_domain(url->host, l, FILTER_FLAG_HOSTNAME)) { ++ if ( ++ /* An IPv6 enclosed by square brackets is a valid hostname.*/ ++ !php_filter_is_valid_ipv6_hostname(s, l) && ++ /* Validate domain. ++ * This includes a loose check for an IPv4 address. */ ++ !_php_filter_validate_domain(url->host, l, FILTER_FLAG_HOSTNAME) ++ ) { + php_url_free(url); + RETURN_VALIDATION_FAILED + } +@@ -691,15 +696,15 @@ static int _php_filter_validate_ipv4(char *str, size_t str_len, int *ip) /* {{{ + } + /* }}} */ + +-static int _php_filter_validate_ipv6(char *str, size_t str_len) /* {{{ */ ++static int _php_filter_validate_ipv6(const char *str, size_t str_len) /* {{{ */ + { + int compressed = 0; + int blocks = 0; + int n; + char *ipv4; +- char *end; ++ const char *end; + int ip4elm[4]; +- char *s = str; ++ const char *s = str; + + if (!memchr(str, ':', str_len)) { + return 0; +diff --git a/ext/filter/tests/ghsa-w8qr-v226-r27w.phpt b/ext/filter/tests/ghsa-w8qr-v226-r27w.phpt +new file mode 100644 +index 0000000000..0092408ee5 +--- /dev/null ++++ b/ext/filter/tests/ghsa-w8qr-v226-r27w.phpt +@@ -0,0 +1,41 @@ ++--TEST-- ++GHSA-w8qr-v226-r27w ++--EXTENSIONS-- ++filter ++--FILE-- ++<?php ++ ++function test(string $input) { ++ var_dump(filter_var($input, FILTER_VALIDATE_URL)); ++} ++ ++echo "--- These ones should fail ---\n"; ++test("http://t[est@127.0.0.1"); ++test("http://t[est@[::1]"); ++test("http://t[est@[::1"); ++test("http://t[est@::1]"); ++test("http://php.net\\@aliyun.com/aaa.do"); ++test("http://test[@2001:db8:3333:4444:5555:6666:1.2.3.4]"); ++test("http://te[st@2001:db8:3333:4444:5555:6666:1.2.3.4]"); ++test("http://te[st@2001:db8:3333:4444:5555:6666:1.2.3.4"); ++ ++echo "--- These ones should work ---\n"; ++test("http://test@127.0.0.1"); ++test("http://test@[2001:db8:3333:4444:5555:6666:1.2.3.4]"); ++test("http://test@[::1]"); ++ ++?> ++--EXPECT-- ++--- These ones should fail --- ++bool(false) ++bool(false) ++bool(false) ++bool(false) ++bool(false) ++bool(false) ++bool(false) ++bool(false) ++--- These ones should work --- ++string(21) "http://test@127.0.0.1" ++string(50) "http://test@[2001:db8:3333:4444:5555:6666:1.2.3.4]" ++string(17) "http://test@[::1]" +-- +2.45.1 + +From 573b921a612068f66f6540b69b0a9bc9f372ecf1 Mon Sep 17 00:00:00 2001 +From: Remi Collet <remi@remirepo.net> +Date: Tue, 4 Jun 2024 16:48:08 +0200 +Subject: [PATCH 2/2] NEWS + +(cherry picked from commit a1ff81b786bd519597e770795be114f5171f0648) +(cherry picked from commit ec1d5e6468479e64acc7fb8cb955f053b64ea9a0) +(cherry picked from commit cfe1b1acead13b6af163f3ce947d3a1dbded82a0) +(cherry picked from commit 6b6444a1b72d6249cfa592f20395efe67ca55f73) +(cherry picked from commit 8d4db37794eff4761a336d7cee53d47f0eb0d313) +--- + NEWS | 6 ++++++ + 1 file changed, 6 insertions(+) + +diff --git a/NEWS b/NEWS +index fed03499f6..1f6f1376b4 100644 +--- a/NEWS ++++ b/NEWS +@@ -1,6 +1,12 @@ + PHP NEWS + ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| + ++Backported from 8.1.29 ++ ++- Filter: ++ . Fixed bug GHSA-w8qr-v226-r27w (Filter bypass in filter_var FILTER_VALIDATE_URL). ++ (CVE-2024-5458) (nielsdos) ++ + Backported from 8.1.28 + + - Standard: +-- +2.45.1 + diff --git a/php-cve-2024-8925.patch b/php-cve-2024-8925.patch new file mode 100644 index 0000000..1ef014e --- /dev/null +++ b/php-cve-2024-8925.patch @@ -0,0 +1,242 @@ +From cc487bc778a079891c67e927edf0fc1a46f0e5e3 Mon Sep 17 00:00:00 2001 +From: Arnaud Le Blanc <arnaud.lb@gmail.com> +Date: Mon, 9 Sep 2024 15:22:07 +0200 +Subject: [PATCH 3/8] Fix GHSA-9pqp-7h25-4f32 + +multipart/form-data boundaries larger than the read buffer result in erroneous +parsing, which violates data integrity. + +Limit boundary size, as allowed by RFC 1521: + + Encapsulation boundaries [...] must be no longer than 70 characters, not + counting the two leading hyphens. + +We correctly parse payloads with boundaries of length up to +FILLUNIT-strlen("\r\n--") bytes, so allow this for BC. + +(cherry picked from commit 19b49258d0c5a61398d395d8afde1123e8d161e0) +(cherry picked from commit 2b0daf421c162376892832588eccdfa9a286ed09) +(cherry picked from commit a24ac172f52e75101913f3946cfa5515f723c99f) +(cherry picked from commit 08f0adf0700f8bbaa4fd75b7a694bbd9ae45300d) +(cherry picked from commit 5731a40507feea683591addf3599d210cd7a1fd9) +(cherry picked from commit c9e67e9debe6ed0b313ebc6769a3ca0e417cd781) +--- + main/rfc1867.c | 7 ++ + tests/basic/GHSA-9pqp-7h25-4f32.inc | 3 + + tests/basic/GHSA-9pqp-7h25-4f32.phpt | 100 +++++++++++++++++++++++++++ + 3 files changed, 110 insertions(+) + create mode 100644 tests/basic/GHSA-9pqp-7h25-4f32.inc + create mode 100644 tests/basic/GHSA-9pqp-7h25-4f32.phpt + +diff --git a/main/rfc1867.c b/main/rfc1867.c +index 022bb94028..813c2566de 100644 +--- a/main/rfc1867.c ++++ b/main/rfc1867.c +@@ -767,6 +767,13 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */ + boundary_len = boundary_end-boundary; + } + ++ /* Boundaries larger than FILLUNIT-strlen("\r\n--") characters lead to ++ * erroneous parsing */ ++ if (boundary_len > FILLUNIT-strlen("\r\n--")) { ++ sapi_module.sapi_error(E_WARNING, "Boundary too large in multipart/form-data POST data"); ++ return; ++ } ++ + /* Initialize the buffer */ + if (!(mbuff = multipart_buffer_new(boundary, boundary_len))) { + sapi_module.sapi_error(E_WARNING, "Unable to initialize the input buffer"); +diff --git a/tests/basic/GHSA-9pqp-7h25-4f32.inc b/tests/basic/GHSA-9pqp-7h25-4f32.inc +new file mode 100644 +index 0000000000..adf72a361a +--- /dev/null ++++ b/tests/basic/GHSA-9pqp-7h25-4f32.inc +@@ -0,0 +1,3 @@ ++<?php ++print "Hello world\n"; ++var_dump($_POST); +diff --git a/tests/basic/GHSA-9pqp-7h25-4f32.phpt b/tests/basic/GHSA-9pqp-7h25-4f32.phpt +new file mode 100644 +index 0000000000..af81916370 +--- /dev/null ++++ b/tests/basic/GHSA-9pqp-7h25-4f32.phpt +@@ -0,0 +1,100 @@ ++--TEST-- ++GHSA-9pqp-7h25-4f32 ++--SKIPIF-- ++<?php ++if (!getenv('TEST_PHP_CGI_EXECUTABLE')) { ++ die("skip php-cgi not available"); ++} ++?> ++--FILE-- ++<?php ++ ++const FILLUNIT = 5 * 1024; ++ ++function test($boundaryLen) { ++ printf("Boundary len: %d\n", $boundaryLen); ++ ++ $cmd = [ ++ getenv('TEST_PHP_CGI_EXECUTABLE'), ++ '-C', ++ '-n', ++ __DIR__ . '/GHSA-9pqp-7h25-4f32.inc', ++ ]; ++ ++ $boundary = str_repeat('A', $boundaryLen); ++ $body = "" ++ . "--$boundary\r\n" ++ . "Content-Disposition: form-data; name=\"koko\"\r\n" ++ . "\r\n" ++ . "BBB\r\n--" . substr($boundary, 0, -1) . "CCC\r\n" ++ . "--$boundary--\r\n" ++ ; ++ ++ $env = array_merge($_ENV, [ ++ 'REDIRECT_STATUS' => '1', ++ 'CONTENT_TYPE' => "multipart/form-data; boundary=$boundary", ++ 'CONTENT_LENGTH' => strlen($body), ++ 'REQUEST_METHOD' => 'POST', ++ 'SCRIPT_FILENAME' => __DIR__ . '/GHSA-9pqp-7h25-4f32.inc', ++ ]); ++ ++ $spec = [ ++ 0 => ['pipe', 'r'], ++ 1 => STDOUT, ++ 2 => STDOUT, ++ ]; ++ ++ $pipes = []; ++ ++ print "Starting...\n"; ++ ++ $handle = proc_open($cmd, $spec, $pipes, getcwd(), $env); ++ ++ fwrite($pipes[0], $body); ++ ++ $status = proc_close($handle); ++ ++ print "\n"; ++} ++ ++for ($offset = -1; $offset <= 1; $offset++) { ++ test(FILLUNIT - strlen("\r\n--") + $offset); ++} ++ ++?> ++--EXPECTF-- ++Boundary len: 5115 ++Starting... ++X-Powered-By: %s ++Content-type: text/html; charset=UTF-8 ++ ++Hello world ++array(1) { ++ ["koko"]=> ++ string(5124) "BBB ++--AAA%sCCC" ++} ++ ++Boundary len: 5116 ++Starting... ++X-Powered-By: %s ++Content-type: text/html; charset=UTF-8 ++ ++Hello world ++array(1) { ++ ["koko"]=> ++ string(5125) "BBB ++--AAA%sCCC" ++} ++ ++Boundary len: 5117 ++Starting... ++X-Powered-By: %s ++Content-type: text/html; charset=UTF-8 ++ ++<br /> ++<b>Warning</b>: Boundary too large in multipart/form-data POST data in <b>Unknown</b> on line <b>0</b><br /> ++Hello world ++array(0) { ++} ++ +-- +2.46.1 + +From d5931f3c995e5d1d92289f0acbbaef1678d911a6 Mon Sep 17 00:00:00 2001 +From: Jakub Zelenka <bukka@php.net> +Date: Mon, 23 Sep 2024 18:54:31 +0100 +Subject: [PATCH 6/8] Skip GHSA-9pqp-7h25-4f32 test on Windows + +(cherry picked from commit c70e25630832fa10d421328eed2b8e1a36af7a64) +(cherry picked from commit c75683864f6e4188439e8ca2adbb05824918be12) +(cherry picked from commit 2fd1b83817d20523e72bef3ad524cd5797f51acf) +(cherry picked from commit 79eace3a64544088738d2fd341407cc32fe3ecaf) +(cherry picked from commit 0c9258e4914695ca21b3d0cd3b1746bfc926f02e) +(cherry picked from commit 2d5ff57eb7a36f9f0655c7073c4c702a903d9005) +--- + tests/basic/GHSA-9pqp-7h25-4f32.phpt | 3 +++ + 1 file changed, 3 insertions(+) + +diff --git a/tests/basic/GHSA-9pqp-7h25-4f32.phpt b/tests/basic/GHSA-9pqp-7h25-4f32.phpt +index af81916370..29bcb6557d 100644 +--- a/tests/basic/GHSA-9pqp-7h25-4f32.phpt ++++ b/tests/basic/GHSA-9pqp-7h25-4f32.phpt +@@ -5,6 +5,9 @@ GHSA-9pqp-7h25-4f32 + if (!getenv('TEST_PHP_CGI_EXECUTABLE')) { + die("skip php-cgi not available"); + } ++if (substr(PHP_OS, 0, 3) == 'WIN') { ++ die("skip not for Windows in CI - probably resource issue"); ++} + ?> + --FILE-- + <?php +-- +2.46.1 + +From fb9688a470070f0ab656e2f94efbee2988a30eaf Mon Sep 17 00:00:00 2001 +From: Remi Collet <remi@remirepo.net> +Date: Thu, 26 Sep 2024 15:49:03 +0200 +Subject: [PATCH 8/8] adapt GHSA-9pqp-7h25-4f32 test for 7.x + +(cherry picked from commit 29065f33f37f99ba33254cb23c941647bcd7372c) +(cherry picked from commit 87ed9429a17e38daec4dcfd7a3c3db194197ccb3) +(cherry picked from commit d97de82afe8696b6d76cc11bc7b6d6c2652d06d9) +(cherry picked from commit 64a9dfdec2cb530428c9cbe90f98f346c5d23797) +--- + tests/basic/GHSA-9pqp-7h25-4f32.phpt | 5 +++-- + 1 file changed, 3 insertions(+), 2 deletions(-) + +diff --git a/tests/basic/GHSA-9pqp-7h25-4f32.phpt b/tests/basic/GHSA-9pqp-7h25-4f32.phpt +index 29bcb6557d..b913edc1c4 100644 +--- a/tests/basic/GHSA-9pqp-7h25-4f32.phpt ++++ b/tests/basic/GHSA-9pqp-7h25-4f32.phpt +@@ -21,8 +21,10 @@ function test($boundaryLen) { + getenv('TEST_PHP_CGI_EXECUTABLE'), + '-C', + '-n', ++ '-dlog_errors=1', + __DIR__ . '/GHSA-9pqp-7h25-4f32.inc', + ]; ++ $cmd = implode(' ', $cmd); + + $boundary = str_repeat('A', $boundaryLen); + $body = "" +@@ -92,11 +94,10 @@ array(1) { + + Boundary len: 5117 + Starting... ++PHP Warning: Boundary too large in multipart/form-data POST data in Unknown on line 0 + X-Powered-By: %s + Content-type: text/html; charset=UTF-8 + +-<br /> +-<b>Warning</b>: Boundary too large in multipart/form-data POST data in <b>Unknown</b> on line <b>0</b><br /> + Hello world + array(0) { + } +-- +2.46.1 + diff --git a/php-cve-2024-8926.patch b/php-cve-2024-8926.patch new file mode 100644 index 0000000..54f833e --- /dev/null +++ b/php-cve-2024-8926.patch @@ -0,0 +1,206 @@ +From 42c04a1cbf382227da86037a7c6d49524f19f6ba Mon Sep 17 00:00:00 2001 +From: Jan Ehrhardt <github@ehrhardt.nl> +Date: Wed, 5 Jun 2024 20:31:25 +0200 +Subject: [PATCH 1/8] Fix GHSA-3qgc-jrrr-25jv + +--- + sapi/cgi/cgi_main.c | 23 ++++++++++++++- + sapi/cgi/tests/ghsa-3qgc-jrrr-25jv.phpt | 38 +++++++++++++++++++++++++ + 2 files changed, 60 insertions(+), 1 deletion(-) + create mode 100644 sapi/cgi/tests/ghsa-3qgc-jrrr-25jv.phpt + +diff --git a/sapi/cgi/cgi_main.c b/sapi/cgi/cgi_main.c +index 896635711b..1d7288a35d 100644 +--- a/sapi/cgi/cgi_main.c ++++ b/sapi/cgi/cgi_main.c +@@ -1805,8 +1805,13 @@ int main(int argc, char *argv[]) + } + } + ++ /* Apache CGI will pass the query string to the command line if it doesn't contain a '='. ++ * This can create an issue where a malicious request can pass command line arguments to ++ * the executable. Ideally we skip argument parsing when we're in cgi or fastcgi mode, ++ * but that breaks PHP scripts on Linux with a hashbang: `#!/php-cgi -d option=value`. ++ * Therefore, this code only prevents passing arguments if the query string starts with a '-'. ++ * Similarly, scripts spawned in subprocesses on Windows may have the same issue. */ + if((query_string = getenv("QUERY_STRING")) != NULL && strchr(query_string, '=') == NULL) { +- /* we've got query string that has no = - apache CGI will pass it to command line */ + unsigned char *p; + decoded_query_string = strdup(query_string); + php_url_decode(decoded_query_string, strlen(decoded_query_string)); +@@ -1816,6 +1821,22 @@ int main(int argc, char *argv[]) + if(*p == '-') { + skip_getopt = 1; + } ++ ++ /* On Windows we have to take into account the "best fit" mapping behaviour. */ ++#ifdef PHP_WIN32 ++ if (*p >= 0x80) { ++ wchar_t wide_buf[1]; ++ wide_buf[0] = *p; ++ char char_buf[4]; ++ size_t wide_buf_len = sizeof(wide_buf) / sizeof(wide_buf[0]); ++ size_t char_buf_len = sizeof(char_buf) / sizeof(char_buf[0]); ++ if (WideCharToMultiByte(CP_ACP, 0, wide_buf, wide_buf_len, char_buf, char_buf_len, NULL, NULL) == 0 ++ || char_buf[0] == '-') { ++ skip_getopt = 1; ++ } ++ } ++#endif ++ + free(decoded_query_string); + } + +diff --git a/sapi/cgi/tests/ghsa-3qgc-jrrr-25jv.phpt b/sapi/cgi/tests/ghsa-3qgc-jrrr-25jv.phpt +new file mode 100644 +index 0000000000..fd2fcdfbf8 +--- /dev/null ++++ b/sapi/cgi/tests/ghsa-3qgc-jrrr-25jv.phpt +@@ -0,0 +1,38 @@ ++--TEST-- ++GHSA-3qgc-jrrr-25jv ++--SKIPIF-- ++<?php ++include 'skipif.inc'; ++if (PHP_OS_FAMILY !== "Windows") die("skip Only for Windows"); ++ ++$codepage = trim(shell_exec("powershell Get-ItemPropertyValue HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Nls\\CodePage ACP")); ++if ($codepage !== '932' && $codepage !== '936' && $codepage !== '950') die("skip Wrong codepage"); ++?> ++--FILE-- ++<?php ++include 'include.inc'; ++ ++$filename = __DIR__."/GHSA-3qgc-jrrr-25jv_tmp.php"; ++$script = '<?php echo "hello "; echo "world"; ?>'; ++file_put_contents($filename, $script); ++ ++$php = get_cgi_path(); ++reset_env_vars(); ++ ++putenv("SERVER_NAME=Test"); ++putenv("SCRIPT_FILENAME=$filename"); ++putenv("QUERY_STRING=%ads"); ++putenv("REDIRECT_STATUS=1"); ++ ++passthru("$php -s"); ++ ++?> ++--CLEAN-- ++<?php ++@unlink(__DIR__."/GHSA-3qgc-jrrr-25jv_tmp.php"); ++?> ++--EXPECTF-- ++X-Powered-By: PHP/%s ++Content-type: %s ++ ++hello world +-- +2.46.1 + +From e925cc566a427efbc472e1c3783d885ba4730271 Mon Sep 17 00:00:00 2001 +From: Jan Ehrhardt <github@ehrhardt.nl> +Date: Sun, 9 Jun 2024 20:14:22 +0200 +Subject: [PATCH 2/8] NEWS: Add backport from 8.1.29 + +--- + NEWS | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/NEWS b/NEWS +index 1f6f1376b4..6f26c17ead 100644 +--- a/NEWS ++++ b/NEWS +@@ -3,6 +3,10 @@ PHP NEWS + + Backported from 8.1.29 + ++- CGI: ++ . Fixed bug GHSA-3qgc-jrrr-25jv (Bypass of CVE-2012-1823, Argument Injection ++ in PHP-CGI). (CVE-2024-4577) (nielsdos) ++ + - Filter: + . Fixed bug GHSA-w8qr-v226-r27w (Filter bypass in filter_var FILTER_VALIDATE_URL). + (CVE-2024-5458) (nielsdos) +-- +2.46.1 + +From 493b4986d3cb8bfaccbab82628a4b91044670572 Mon Sep 17 00:00:00 2001 +From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> +Date: Fri, 14 Jun 2024 19:49:22 +0200 +Subject: [PATCH 4/8] Fix GHSA-p99j-rfp4-xqvq + +It's no use trying to work around whatever the operating system and Apache +do because we'll be fighting that until eternity. +Change the skip_getopt condition such that when we're running in +CGI or FastCGI mode we always skip the argument parsing. +This is a BC break, but this seems to be the only way to get rid of this +class of issues. + +(cherry picked from commit abcfd980bfa03298792fd3aba051c78d52f10642) +(cherry picked from commit 2d2552e092b6ff32cd823692d512f126ee629842) +(cherry picked from commit 1158d06f0b20532ab7309cb20f0be843f9662e3c) +(cherry picked from commit 89c66773413267949de995671bfb4bd03c34fbf9) +(cherry picked from commit 53a0269aa1d952eec1c65e0e0d3e9800e0427ded) +(cherry picked from commit 56f24340b2cd718d54fca9bc95cbf1f34b50b71f) +--- + sapi/cgi/cgi_main.c | 26 ++++++++------------------ + 1 file changed, 8 insertions(+), 18 deletions(-) + +diff --git a/sapi/cgi/cgi_main.c b/sapi/cgi/cgi_main.c +index 1d7288a35d..f3b75bf7e7 100644 +--- a/sapi/cgi/cgi_main.c ++++ b/sapi/cgi/cgi_main.c +@@ -1742,7 +1742,6 @@ int main(int argc, char *argv[]) + int status = 0; + #endif + char *query_string; +- char *decoded_query_string; + int skip_getopt = 0; + + #if 0 && defined(PHP_DEBUG) +@@ -1810,10 +1809,15 @@ int main(int argc, char *argv[]) + * the executable. Ideally we skip argument parsing when we're in cgi or fastcgi mode, + * but that breaks PHP scripts on Linux with a hashbang: `#!/php-cgi -d option=value`. + * Therefore, this code only prevents passing arguments if the query string starts with a '-'. +- * Similarly, scripts spawned in subprocesses on Windows may have the same issue. */ ++ * Similarly, scripts spawned in subprocesses on Windows may have the same issue. ++ * However, Windows has lots of conversion rules and command line parsing rules that ++ * are too difficult and dangerous to reliably emulate. */ + if((query_string = getenv("QUERY_STRING")) != NULL && strchr(query_string, '=') == NULL) { ++#ifdef PHP_WIN32 ++ skip_getopt = cgi || fastcgi; ++#else + unsigned char *p; +- decoded_query_string = strdup(query_string); ++ char *decoded_query_string = strdup(query_string); + php_url_decode(decoded_query_string, strlen(decoded_query_string)); + for (p = (unsigned char *)decoded_query_string; *p && *p <= ' '; p++) { + /* skip all leading spaces */ +@@ -1822,22 +1826,8 @@ int main(int argc, char *argv[]) + skip_getopt = 1; + } + +- /* On Windows we have to take into account the "best fit" mapping behaviour. */ +-#ifdef PHP_WIN32 +- if (*p >= 0x80) { +- wchar_t wide_buf[1]; +- wide_buf[0] = *p; +- char char_buf[4]; +- size_t wide_buf_len = sizeof(wide_buf) / sizeof(wide_buf[0]); +- size_t char_buf_len = sizeof(char_buf) / sizeof(char_buf[0]); +- if (WideCharToMultiByte(CP_ACP, 0, wide_buf, wide_buf_len, char_buf, char_buf_len, NULL, NULL) == 0 +- || char_buf[0] == '-') { +- skip_getopt = 1; +- } +- } +-#endif +- + free(decoded_query_string); ++#endif + } + + while (!skip_getopt && (c = php_getopt(argc, argv, OPTIONS, &php_optarg, &php_optind, 0, 2)) != -1) { +-- +2.46.1 + diff --git a/php-cve-2024-8927.patch b/php-cve-2024-8927.patch new file mode 100644 index 0000000..1442d74 --- /dev/null +++ b/php-cve-2024-8927.patch @@ -0,0 +1,102 @@ +From 234a673bb5bee58ce752d6fefa4cba99435ae21c Mon Sep 17 00:00:00 2001 +From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> +Date: Tue, 18 Jun 2024 21:28:26 +0200 +Subject: [PATCH 5/8] Fix GHSA-94p6-54jq-9mwp + +Apache only generates REDIRECT_STATUS, so explicitly check for that +if the server name is Apache, don't allow other variable names. +Furthermore, redirect.so and Netscape no longer exist, so +remove those entries as we can't check their server name anymore. + +We now also check for the configuration override *first* such that it +always take precedence. This would allow for a mitigation path if +something like this happens in the future. + +(cherry picked from commit 48808d98f4fc2a05193cdcc1aedd6c66816450f1) +(cherry picked from commit 8aa748ee0657cdee8d883ba50d04b68bc450f686) +(cherry picked from commit c7308ba7cd0533501b40eba255602bb5e085550f) +(cherry picked from commit 21e2b0ab382a898f627c97d39f5e5afc2431afe7) +(cherry picked from commit 74f1553070cb6237e25945407be7f75a43736113) +(cherry picked from commit 1e522a66b2b5376545c3e3dfc743e4e6614aade9) +--- + sapi/cgi/cgi_main.c | 23 +++++++++++------------ + 1 file changed, 11 insertions(+), 12 deletions(-) + +diff --git a/sapi/cgi/cgi_main.c b/sapi/cgi/cgi_main.c +index f3b75bf7e7..7b3ff2e17c 100644 +--- a/sapi/cgi/cgi_main.c ++++ b/sapi/cgi/cgi_main.c +@@ -1916,18 +1916,17 @@ int main(int argc, char *argv[]) + + /* check force_cgi after startup, so we have proper output */ + if (cgi && CGIG(force_redirect)) { +- /* Apache will generate REDIRECT_STATUS, +- * Netscape and redirect.so will generate HTTP_REDIRECT_STATUS. +- * redirect.so and installation instructions available from +- * http://www.koehntopp.de/php. +- * -- kk@netuse.de +- */ +- if (!getenv("REDIRECT_STATUS") && +- !getenv ("HTTP_REDIRECT_STATUS") && +- /* this is to allow a different env var to be configured +- * in case some server does something different than above */ +- (!CGIG(redirect_status_env) || !getenv(CGIG(redirect_status_env))) +- ) { ++ /* This is to allow a different environment variable to be configured ++ * in case the we cannot auto-detect which environment variable to use. ++ * Checking this first to allow user overrides in case the environment ++ * variable can be set by an untrusted party. */ ++ const char *redirect_status_env = CGIG(redirect_status_env); ++ if (!redirect_status_env) { ++ /* Apache will generate REDIRECT_STATUS. */ ++ redirect_status_env = "REDIRECT_STATUS"; ++ } ++ ++ if (!getenv(redirect_status_env)) { + zend_try { + SG(sapi_headers).http_response_code = 400; + PUTS("<b>Security Alert!</b> The PHP CGI cannot be accessed directly.\n\n\ +-- +2.46.1 + +From 9a3477d3c48272520840f9e20a7135e929e68c0e Mon Sep 17 00:00:00 2001 +From: Remi Collet <remi@remirepo.net> +Date: Thu, 26 Sep 2024 11:50:54 +0200 +Subject: [PATCH 7/8] NEWS for 8.1.30 backports + +(cherry picked from commit af3fb385e7b328ab89db26ec712d89c7096f0743) +(cherry picked from commit 1154fbd3ddfa418bf2492c5366adaefb47c47737) +(cherry picked from commit b4667e4ebe241d95775962b1e8b24788e7945de2) +(cherry picked from commit e80cb90b00aa403a5aa995f612ecb358323e9572) +(cherry picked from commit fbd3eff22ba8becf30263ddf6ab92a9c2ca93181) +--- + NEWS | 13 +++++++++++++ + 1 file changed, 13 insertions(+) + +diff --git a/NEWS b/NEWS +index 6f26c17ead..cf90002253 100644 +--- a/NEWS ++++ b/NEWS +@@ -1,6 +1,19 @@ + PHP NEWS + ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| + ++Backported from 8.1.30 ++ ++- CGI: ++ . Fixed bug GHSA-p99j-rfp4-xqvq (Bypass of CVE-2024-4577, Parameter Injection ++ Vulnerability). (CVE-2024-8926) (nielsdos) ++ . Fixed bug GHSA-94p6-54jq-9mwp (cgi.force_redirect configuration is ++ bypassable due to the environment variable collision). (CVE-2024-8927) ++ (nielsdos) ++ ++- SAPI: ++ . Fixed bug GHSA-9pqp-7h25-4f32 (Erroneous parsing of multipart form data). ++ (CVE-2024-8925) (Arnaud) ++ + Backported from 8.1.29 + + - CGI: +-- +2.46.1 + diff --git a/php-cve-2024-8932.patch b/php-cve-2024-8932.patch new file mode 100644 index 0000000..97b3cd3 --- /dev/null +++ b/php-cve-2024-8932.patch @@ -0,0 +1,131 @@ +From 0ad928e34b6462c83c53cb1d98271db9f2633410 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 5/9] Fix GHSA-g665-fm4p-vhff: OOB access in ldap_escape + +(cherry picked from commit f9ecf90070a11dad09ca7671a712f81cc2a7d52f) +(cherry picked from commit 9f367d847989b339c33369737daf573e30bab5f1) +(cherry picked from commit 50e9e72530a4805980384b8ea6672877af816145) +(cherry picked from commit 9822bfae85607dffc13848d40a2340daf090f39b) +(cherry picked from commit f8756a7a1d185727a5bfd212b1442a6d153a9471) +(cherry picked from commit c8a7aed24cd977a578fd7f1ae60cfdf0032cce26) +--- + ext/ldap/ldap.c | 15 ++++++++++-- + ext/ldap/tests/GHSA-g665-fm4p-vhff-1.phpt | 28 ++++++++++++++++++++++ + ext/ldap/tests/GHSA-g665-fm4p-vhff-2.phpt | 29 +++++++++++++++++++++++ + 3 files changed, 70 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 1c9340c777..0eaa290260 100644 +--- a/ext/ldap/ldap.c ++++ b/ext/ldap/ldap.c +@@ -60,6 +60,7 @@ + + #include "ext/standard/php_string.h" + #include "ext/standard/info.h" ++#include "Zend/zend_exceptions.h" + + #ifdef HAVE_LDAP_SASL_H + #include <sasl.h> +@@ -2728,7 +2729,11 @@ 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; + } + + ret = zend_string_alloc(len, 0); +@@ -2794,7 +2799,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)); ++ zend_string *result = php_ldap_do_escape(map, value, valuelen); ++ 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 0000000000..734bbe91d4 +--- /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 0000000000..5c1b0fb661 +--- /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-2026-6722.patch b/php-cve-2026-6722.patch new file mode 100644 index 0000000..735ee1d --- /dev/null +++ b/php-cve-2026-6722.patch @@ -0,0 +1,113 @@ +From 237b2b14cad370a25ddec00be93a9710003b5048 Mon Sep 17 00:00:00 2001 +From: Ilija Tovilo <ilija.tovilo@me.com> +Date: Sun, 3 May 2026 19:56:53 +0200 +Subject: [PATCH 1/6] GHSA-85c2-q967-79q5: [soap] Fix stale + SOAP_GLOBAL(ref_map) pointer with Apache Map + +Fixes GHSA-85c2-q967-79q5 +Fixes CVE-2026-6722 + +(cherry picked from commit aee3b3ac9b816b0def1c462695b483b49a83148e) +(cherry picked from commit 15064460d6682766f91c1a841d27cdfbc38907e8) +(cherry picked from commit bbc1be3fc763b81707ccaa91a4cd1d439b753b12) +(cherry picked from commit 6c4b67ca091afea4f436202d7f9db38a129106dc) +(cherry picked from commit 017843d76d595ae97cb97eba4affd69501244571) +(cherry picked from commit 8fc3ed35cf67234da5201f64051e2ffa96d70f86) +(cherry picked from commit 7151aacadf978a14d06e09dd5899e8727f232056) +--- + ext/soap/php_encoding.c | 3 +- + ext/soap/tests/GHSA-85c2-q967-79q5.phpt | 61 +++++++++++++++++++++++++ + 2 files changed, 63 insertions(+), 1 deletion(-) + create mode 100644 ext/soap/tests/GHSA-85c2-q967-79q5.phpt + +diff --git a/ext/soap/php_encoding.c b/ext/soap/php_encoding.c +index 47afe2703c7..40fba95980a 100644 +--- a/ext/soap/php_encoding.c ++++ b/ext/soap/php_encoding.c +@@ -381,6 +381,7 @@ static zend_bool soap_check_xml_ref(zval *data, xmlNodePtr node) + static void soap_add_xml_ref(zval *data, xmlNodePtr node) + { + if (SOAP_GLOBAL(ref_map)) { ++ Z_TRY_ADDREF_P(data); + zend_hash_index_update(SOAP_GLOBAL(ref_map), (zend_ulong)node, data); + } + } +@@ -3472,7 +3473,7 @@ void encode_reset_ns() + } else { + SOAP_GLOBAL(ref_map) = emalloc(sizeof(HashTable)); + } +- zend_hash_init(SOAP_GLOBAL(ref_map), 0, NULL, NULL, 0); ++ zend_hash_init(SOAP_GLOBAL(ref_map), 0, NULL, ZVAL_PTR_DTOR, 0); + } + + void encode_finish() +diff --git a/ext/soap/tests/GHSA-85c2-q967-79q5.phpt b/ext/soap/tests/GHSA-85c2-q967-79q5.phpt +new file mode 100644 +index 00000000000..8bcac26ad18 +--- /dev/null ++++ b/ext/soap/tests/GHSA-85c2-q967-79q5.phpt +@@ -0,0 +1,61 @@ ++--TEST-- ++GHSA-85c2-q967-79q5: Stale SOAP_GLOBAL(ref_map) pointer with Apache Map ++--CREDITS-- ++brettgervasoni ++--EXTENSIONS-- ++soap ++--FILE-- ++<?php ++ ++class Handler { ++ public function test(...$args) { ++ $GLOBALS['result'] = $args; ++ } ++} ++ ++$envelope = <<<'XML' ++<?xml version="1.0" encoding="UTF-8"?> ++<soapenv:Envelope ++ xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" ++ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ++ xmlns:xsd="http://www.w3.org/2001/XMLSchema"> ++ ++ <soapenv:Body> ++ <test> ++ <map xsi:type="apache:Map" xmlns:apache="http://xml.apache.org/xml-soap"> ++ <item> ++ <key>foo</key> ++ <value id="stale"><object>bar</object></value> ++ </item> ++ <item> ++ <key>foo</key> ++ <value>baz</value> ++ </item> ++ </map> ++ <stale href="#stale"/> ++ </test> ++ </soapenv:Body> ++</soapenv:Envelope> ++XML; ++ ++$s = new SoapServer(null, ['uri' => 'urn:a']); ++$s->setClass(Handler::class); ++$s->handle($envelope); ++var_dump($result); ++ ++?> ++--EXPECTF-- ++<?xml version="1.0" encoding="UTF-8"?> ++<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="urn:a" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><ns1:testResponse><return xsi:nil="true"/></ns1:testResponse></SOAP-ENV:Body></SOAP-ENV:Envelope> ++array(2) { ++ [0]=> ++ array(1) { ++ ["foo"]=> ++ string(3) "baz" ++ } ++ [1]=> ++ object(stdClass)#%d (1) { ++ ["object"]=> ++ string(3) "bar" ++ } ++} +-- +2.54.0 + diff --git a/php-cve-2026-6735.patch b/php-cve-2026-6735.patch new file mode 100644 index 0000000..844bccb --- /dev/null +++ b/php-cve-2026-6735.patch @@ -0,0 +1,2697 @@ +From 382b1902c46ae1965b7973b801d007366a7851c2 Mon Sep 17 00:00:00 2001 +From: Jakub Zelenka <bukka@php.net> +Date: Sun, 3 May 2026 20:01:41 +0200 +Subject: [PATCH 5/6] GHSA-7qg2-v9fj-4mwv: [fpm] XSS within status endpoint + +Fixes GHSA-7qg2-v9fj-4mwv +Fixes CVE-2026-6735 + +(cherry picked from commit 99a5ad7441de9914246c7863adb6997396008b9d) +(cherry picked from commit cc2960e782eb5cc262d7bd572a7d18979a811954) +(cherry picked from commit 62daef7b73108ceda2545862cde0673f252ba2d2) +(cherry picked from commit aeaf48ca0bceba42b9595dff30d9e96029c54613) + +backport some new FPM tester features + +(cherry picked from commit 8b1746466f9fcf248f9879fabfa356106d365da0) +(cherry picked from commit 8e0efa0f20484c8bbfdb8671d61b232b70e2bd0a) +(cherry picked from commit 9be5be215954c010087134964d4fd3ab907cdbda) +--- + sapi/fpm/fpm/fpm_status.c | 31 +- + sapi/fpm/tests/fcgi.inc | 192 ++- + .../tests/ghsa-7qg2-v9fj-4mwv-status-xss.phpt | 48 + + sapi/fpm/tests/logtool.inc | 476 ++++++ + sapi/fpm/tests/response.inc | 281 ++++ + sapi/fpm/tests/tester.inc | 1279 +++++++++++++++++ + 6 files changed, 2244 insertions(+), 63 deletions(-) + create mode 100644 sapi/fpm/tests/ghsa-7qg2-v9fj-4mwv-status-xss.phpt + create mode 100644 sapi/fpm/tests/logtool.inc + create mode 100644 sapi/fpm/tests/response.inc + create mode 100644 sapi/fpm/tests/tester.inc + +diff --git a/sapi/fpm/fpm/fpm_status.c b/sapi/fpm/fpm/fpm_status.c +index 666a1d9aba5..9693f0342ae 100644 +--- a/sapi/fpm/fpm/fpm_status.c ++++ b/sapi/fpm/fpm/fpm_status.c +@@ -386,9 +386,10 @@ int fpm_status_handle_request(void) /* {{{ */ + + /* no need to test the var 'full' */ + if (full_syntax) { +- int i, first; +- zend_string *tmp_query_string; +- char *query_string; ++ unsigned int i; ++ int first; ++ zend_string *tmp_query_string, *tmp_request_uri_string; ++ char *query_string, *request_uri_string; + struct timeval duration, now; + #ifdef HAVE_FPM_LQ + float cpu; +@@ -415,13 +416,30 @@ int fpm_status_handle_request(void) /* {{{ */ + } + } + ++ request_uri_string = NULL; ++ tmp_request_uri_string = NULL; ++ if (proc.request_uri[0] != '\0') { ++ if (encode) { ++ tmp_request_uri_string = php_escape_html_entities_ex( ++ (unsigned char*)proc.request_uri, ++ strlen(proc.request_uri), 1, ENT_DISALLOWED | ENT_HTML_DOC_XML1 | ENT_COMPAT, ++ NULL, /* double_encode */ 1); ++ request_uri_string = ZSTR_VAL(tmp_request_uri_string); ++ } else { ++ request_uri_string = proc.request_uri; ++ } ++ } ++ + query_string = NULL; + tmp_query_string = NULL; + if (proc.query_string[0] != '\0') { + if (!encode) { + query_string = proc.query_string; + } else { +- tmp_query_string = php_escape_html_entities_ex((unsigned char *)proc.query_string, strlen(proc.query_string), 1, ENT_HTML_IGNORE_ERRORS & ENT_COMPAT, NULL, 1); ++ tmp_query_string = php_escape_html_entities_ex( ++ (unsigned char*)proc.query_string, ++ strlen(proc.query_string), 1, ENT_DISALLOWED | ENT_HTML_DOC_XML1 | ENT_COMPAT, ++ NULL, /* double_encode */ 1); + query_string = ZSTR_VAL(tmp_query_string); + } + } +@@ -449,7 +467,7 @@ int fpm_status_handle_request(void) /* {{{ */ + proc.requests, + duration.tv_sec * 1000000UL + duration.tv_usec, + proc.request_method[0] != '\0' ? proc.request_method : "-", +- proc.request_uri[0] != '\0' ? proc.request_uri : "-", ++ request_uri_string ? request_uri_string : "-", + query_string ? "?" : "", + query_string ? query_string : "", + proc.content_length, +@@ -462,6 +480,9 @@ int fpm_status_handle_request(void) /* {{{ */ + PUTS(buffer); + efree(buffer); + ++ if (tmp_request_uri_string) { ++ zend_string_free(tmp_request_uri_string); ++ } + if (tmp_query_string) { + zend_string_free(tmp_query_string); + } +diff --git a/sapi/fpm/tests/fcgi.inc b/sapi/fpm/tests/fcgi.inc +index b31676260da..07603a808e0 100644 +--- a/sapi/fpm/tests/fcgi.inc ++++ b/sapi/fpm/tests/fcgi.inc +@@ -72,25 +72,25 @@ class Client + + /** + * Socket +- * @var Resource ++ * @var resource + */ + private $_sock = null; + + /** + * Host +- * @var String ++ * @var string + */ + private $_host = null; + + /** + * Port +- * @var Integer ++ * @var int + */ + private $_port = null; + + /** + * Keep Alive +- * @var Boolean ++ * @var bool + */ + private $_keepAlive = false; + +@@ -110,27 +110,27 @@ class Client + + /** + * Use persistent sockets to connect to backend +- * @var Boolean ++ * @var bool + */ + private $_persistentSocket = false; + + /** + * Connect timeout in milliseconds +- * @var Integer ++ * @var int + */ + private $_connectTimeout = 5000; + + /** + * Read/Write timeout in milliseconds +- * @var Integer ++ * @var int + */ + private $_readWriteTimeout = 5000; + + /** + * Constructor + * +- * @param String $host Host of the FastCGI application +- * @param Integer $port Port of the FastCGI application ++ * @param string $host Host of the FastCGI application ++ * @param int $port Port of the FastCGI application + */ + public function __construct($host, $port) + { +@@ -138,15 +138,25 @@ class Client + $this->_port = $port; + } + ++ /** ++ * Get host. ++ * ++ * @return string ++ */ ++ public function getHost() ++ { ++ return $this->_host; ++ } ++ + /** + * Define whether or not the FastCGI application should keep the connection + * alive at the end of a request + * +- * @param Boolean $b true if the connection should stay alive, false otherwise ++ * @param bool $b true if the connection should stay alive, false otherwise + */ + public function setKeepAlive($b) + { +- $this->_keepAlive = (boolean)$b; ++ $this->_keepAlive = (bool)$b; + if (!$this->_keepAlive && $this->_sock) { + fclose($this->_sock); + } +@@ -155,7 +165,7 @@ class Client + /** + * Get the keep alive status + * +- * @return Boolean true if the connection should stay alive, false otherwise ++ * @return bool true if the connection should stay alive, false otherwise + */ + public function getKeepAlive() + { +@@ -166,12 +176,12 @@ class Client + * Define whether or not PHP should attempt to re-use sockets opened by previous + * request for efficiency + * +- * @param Boolean $b true if persistent socket should be used, false otherwise ++ * @param bool $b true if persistent socket should be used, false otherwise + */ + public function setPersistentSocket($b) + { + $was_persistent = ($this->_sock && $this->_persistentSocket); +- $this->_persistentSocket = (boolean)$b; ++ $this->_persistentSocket = (bool)$b; + if (!$this->_persistentSocket && $was_persistent) { + fclose($this->_sock); + } +@@ -180,7 +190,7 @@ class Client + /** + * Get the pesistent socket status + * +- * @return Boolean true if the socket should be persistent, false otherwise ++ * @return bool true if the socket should be persistent, false otherwise + */ + public function getPersistentSocket() + { +@@ -191,7 +201,7 @@ class Client + /** + * Set the connect timeout + * +- * @param Integer number of milliseconds before connect will timeout ++ * @param int number of milliseconds before connect will timeout + */ + public function setConnectTimeout($timeoutMs) + { +@@ -201,7 +211,7 @@ class Client + /** + * Get the connect timeout + * +- * @return Integer number of milliseconds before connect will timeout ++ * @return int number of milliseconds before connect will timeout + */ + public function getConnectTimeout() + { +@@ -211,7 +221,7 @@ class Client + /** + * Set the read/write timeout + * +- * @param Integer number of milliseconds before read or write call will timeout ++ * @param int number of milliseconds before read or write call will timeout + */ + public function setReadWriteTimeout($timeoutMs) + { +@@ -222,7 +232,7 @@ class Client + /** + * Get the read timeout + * +- * @return Integer number of milliseconds before read will timeout ++ * @return int number of milliseconds before read will timeout + */ + public function getReadWriteTimeout() + { +@@ -232,14 +242,18 @@ class Client + /** + * Helper to avoid duplicating milliseconds to secs/usecs in a few places + * +- * @param Integer millisecond timeout +- * @return Boolean ++ * @param int millisecond timeout ++ * @return bool + */ + private function set_ms_timeout($timeoutMs) { + if (!$this->_sock) { + return false; + } +- return stream_set_timeout($this->_sock, floor($timeoutMs / 1000), ($timeoutMs % 1000) * 1000); ++ return stream_set_timeout( ++ $this->_sock, ++ floor($timeoutMs / 1000), ++ ($timeoutMs % 1000) * 1000 ++ ); + } + + +@@ -250,9 +264,21 @@ class Client + { + if (!$this->_sock) { + if ($this->_persistentSocket) { +- $this->_sock = pfsockopen($this->_host, $this->_port, $errno, $errstr, $this->_connectTimeout/1000); ++ $this->_sock = pfsockopen( ++ $this->_host, ++ $this->_port, ++ $errno, ++ $errstr, ++ $this->_connectTimeout/1000 ++ ); + } else { +- $this->_sock = fsockopen($this->_host, $this->_port, $errno, $errstr, $this->_connectTimeout/1000); ++ $this->_sock = fsockopen( ++ $this->_host, ++ $this->_port, ++ $errno, ++ $errstr, ++ $this->_connectTimeout/1000 ++ ); + } + + if (!$this->_sock) { +@@ -268,9 +294,10 @@ class Client + /** + * Build a FastCGI packet + * +- * @param Integer $type Type of the packet +- * @param String $content Content of the packet +- * @param Integer $requestId RequestId ++ * @param int $type Type of the packet ++ * @param string $content Content of the packet ++ * @param int $requestId RequestId ++ * @return string + */ + private function buildPacket($type, $content, $requestId = 1) + { +@@ -289,9 +316,9 @@ class Client + /** + * Build an FastCGI Name value pair + * +- * @param String $name Name +- * @param String $value Value +- * @return String FastCGI Name value pair ++ * @param string $name Name ++ * @param string $value Value ++ * @return string FastCGI Name value pair + */ + private function buildNvpair($name, $value) + { +@@ -302,14 +329,16 @@ class Client + $nvpair = chr($nlen); + } else { + /* nameLengthB3 & nameLengthB2 & nameLengthB1 & nameLengthB0 */ +- $nvpair = chr(($nlen >> 24) | 0x80) . chr(($nlen >> 16) & 0xFF) . chr(($nlen >> 8) & 0xFF) . chr($nlen & 0xFF); ++ $nvpair = chr(($nlen >> 24) | 0x80) . chr(($nlen >> 16) & 0xFF) ++ . chr(($nlen >> 8) & 0xFF) . chr($nlen & 0xFF); + } + if ($vlen < 128) { + /* valueLengthB0 */ + $nvpair .= chr($vlen); + } else { + /* valueLengthB3 & valueLengthB2 & valueLengthB1 & valueLengthB0 */ +- $nvpair .= chr(($vlen >> 24) | 0x80) . chr(($vlen >> 16) & 0xFF) . chr(($vlen >> 8) & 0xFF) . chr($vlen & 0xFF); ++ $nvpair .= chr(($vlen >> 24) | 0x80) . chr(($vlen >> 16) & 0xFF) ++ . chr(($vlen >> 8) & 0xFF) . chr($vlen & 0xFF); + } + /* nameData & valueData */ + return $nvpair . $name . $value; +@@ -318,7 +347,7 @@ class Client + /** + * Read a set of FastCGI Name value pairs + * +- * @param String $data Data containing the set of FastCGI NVPair ++ * @param string $data Data containing the set of FastCGI NVPair + * @return array of NVPair + */ + private function readNvpair($data, $length = null) +@@ -357,7 +386,7 @@ class Client + /** + * Decode a FastCGI Packet + * +- * @param String $data String containing all the packet ++ * @param string $data string containing all the packet + * @return array + */ + private function decodePacketHeader($data) +@@ -403,6 +432,7 @@ class Client + * + * @param array $requestedInfo information to retrieve + * @return array ++ * @throws \Exception + */ + public function getValues(array $requestedInfo) + { +@@ -423,11 +453,14 @@ class Client + } + + /** +- * Execute a request to the FastCGI application ++ * Execute a request to the FastCGI application and return response body + * + * @param array $params Array of parameters +- * @param String $stdin Content +- * @return String ++ * @param string $stdin Content ++ * @return string ++ * @throws ForbiddenException ++ * @throws TimedOutException ++ * @throws \Exception + */ + public function request(array $params, $stdin) + { +@@ -435,20 +468,38 @@ class Client + return $this->wait_for_response($id); + } + ++ /** ++ * Execute a request to the FastCGI application and return request data ++ * ++ * @param array $params Array of parameters ++ * @param string $stdin Content ++ * @return array ++ * @throws ForbiddenException ++ * @throws TimedOutException ++ * @throws \Exception ++ */ ++ public function request_data(array $params, $stdin) ++ { ++ $id = $this->async_request($params, $stdin); ++ return $this->wait_for_response_data($id); ++ } ++ + /** + * Execute a request to the FastCGI application asyncronously +- * ++ * + * This sends request to application and returns the assigned ID for that request. + * + * You should keep this id for later use with wait_for_response(). Ids are chosen randomly +- * rather than seqentially to guard against false-positives when using persistent sockets. +- * In that case it is possible that a delayed response to a request made by a previous script +- * invocation comes back on this socket and is mistaken for response to request made with same ID +- * during this request. ++ * rather than sequentially to guard against false-positives when using persistent sockets. ++ * In that case it is possible that a delayed response to a request made by a previous script ++ * invocation comes back on this socket and is mistaken for response to request made with same ++ * ID during this request. + * + * @param array $params Array of parameters +- * @param String $stdin Content +- * @return Integer ++ * @param string $stdin Content ++ * @return int ++ * @throws TimedOutException ++ * @throws \Exception + */ + public function async_request(array $params, $stdin) + { +@@ -460,10 +511,12 @@ class Client + // Using persistent sockets implies you want them keept alive by server! + $keepAlive = intval($this->_keepAlive || $this->_persistentSocket); + +- $request = $this->buildPacket(self::BEGIN_REQUEST +- ,chr(0) . chr(self::RESPONDER) . chr($keepAlive) . str_repeat(chr(0), 5) +- ,$id +- ); ++ $request = $this->buildPacket( ++ self::BEGIN_REQUEST, ++ chr(0) . chr(self::RESPONDER) . chr($keepAlive) ++ . str_repeat(chr(0), 5), ++ $id ++ ); + + $paramsRequest = ''; + foreach ($params as $key => $value) { +@@ -494,21 +547,26 @@ class Client + + $this->_requests[$id] = array( + 'state' => self::REQ_STATE_WRITTEN, +- 'response' => null ++ 'response' => null, ++ 'err_response' => null, ++ 'out_response' => null, + ); + + return $id; + } + + /** +- * Blocking call that waits for response to specific request +- * +- * @param Integer $requestId +- * @param Integer $timeoutMs [optional] the number of milliseconds to wait. Defaults to the ReadWriteTimeout value set. +- * @return string response body ++ * Blocking call that waits for response data of the specific request ++ * ++ * @param int $requestId ++ * @param int $timeoutMs [optional] the number of milliseconds to wait. ++ * @return array response data ++ * @throws ForbiddenException ++ * @throws TimedOutException ++ * @throws \Exception + */ +- public function wait_for_response($requestId, $timeoutMs = 0) { +- ++ public function wait_for_response_data($requestId, $timeoutMs = 0) ++ { + if (!isset($this->_requests[$requestId])) { + throw new \Exception('Invalid request id given'); + } +@@ -537,12 +595,15 @@ class Client + if ($resp['type'] == self::STDOUT || $resp['type'] == self::STDERR) { + if ($resp['type'] == self::STDERR) { + $this->_requests[$resp['requestId']]['state'] = self::REQ_STATE_ERR; ++ $this->_requests[$resp['requestId']]['err_response'] .= $resp['content']; ++ } else { ++ $this->_requests[$resp['requestId']]['out_response'] .= $resp['content']; + } + $this->_requests[$resp['requestId']]['response'] .= $resp['content']; + } + if ($resp['type'] == self::END_REQUEST) { + $this->_requests[$resp['requestId']]['state'] = self::REQ_STATE_OK; +- if ($resp['requestId'] == $requestId) { ++ if ($resp['requestId'] == $requestId) { + break; + } + } +@@ -586,7 +647,22 @@ class Client + throw new \Exception('Role value not known [UNKNOWN_ROLE]'); + break; + case self::REQUEST_COMPLETE: +- return $this->_requests[$requestId]['response']; ++ return $this->_requests[$requestId]; + } + } ++ ++ /** ++ * Blocking call that waits for response to specific request ++ * ++ * @param int $requestId ++ * @param int $timeoutMs [optional] the number of milliseconds to wait. ++ * @return string The response content. ++ * @throws ForbiddenException ++ * @throws TimedOutException ++ * @throws \Exception ++ */ ++ public function wait_for_response($requestId, $timeoutMs = 0) ++ { ++ return $this->wait_for_response_data($requestId, $timeoutMs)['response']; ++ } + } +diff --git a/sapi/fpm/tests/ghsa-7qg2-v9fj-4mwv-status-xss.phpt b/sapi/fpm/tests/ghsa-7qg2-v9fj-4mwv-status-xss.phpt +new file mode 100644 +index 00000000000..475bc130a42 +--- /dev/null ++++ b/sapi/fpm/tests/ghsa-7qg2-v9fj-4mwv-status-xss.phpt +@@ -0,0 +1,48 @@ ++--TEST-- ++FPM: GHSA-7qg2-v9fj-4mwv - status xss ++--SKIPIF-- ++<?php include "skipif.inc"; ?> ++--FILE-- ++<?php ++ ++require_once "tester.inc"; ++ ++$cfg = <<<EOT ++[global] ++error_log = {{FILE:LOG}} ++[unconfined] ++listen = {{ADDR}} ++pm = static ++pm.max_children = 2 ++pm.status_path = /status ++catch_workers_output = yes ++EOT; ++ ++$code = <<<EOT ++<?php ++usleep(200000); ++EOT; ++ ++$tester = new FPM\Tester($cfg, $code); ++$tester->start(); ++$tester->expectLogStartNotices(); ++$responses = $tester ++ ->multiRequest([ ++ ['uri' => '/<script>alert(1)</script>', 'query' => '<script>alert(2)</script>'], ++ ['uri' => '/status', 'query' => 'full&html', 'delay' => 100000], ++ ]); ++var_dump(strpos($responses[1]->getBody(), '<script>')); ++$tester->terminate(); ++$tester->expectLogTerminatingNotices(); ++$tester->close(); ++ ++?> ++Done ++--EXPECT-- ++bool(false) ++Done ++--CLEAN-- ++<?php ++require_once "tester.inc"; ++FPM\Tester::clean(); ++?> +diff --git a/sapi/fpm/tests/logtool.inc b/sapi/fpm/tests/logtool.inc +new file mode 100644 +index 00000000000..219c6fedbb8 +--- /dev/null ++++ b/sapi/fpm/tests/logtool.inc +@@ -0,0 +1,476 @@ ++<?php ++ ++namespace FPM; ++ ++class LogTool ++{ ++ const P_TIME = '\[\d\d-\w\w\w-\d{4} \d\d:\d\d:\d\d\]'; ++ const P_PREFIX = '\[pool unconfined\] child \d+ said into stderr: '; ++ const FINAL_SUFFIX = ', pipe is closed'; ++ ++ /** ++ * @var string ++ */ ++ private $message; ++ ++ /** ++ * @var string ++ */ ++ private $level; ++ ++ /** ++ * @var int ++ */ ++ private $position; ++ ++ /** ++ * @var int ++ */ ++ private $suffixPosition; ++ ++ /** ++ * @var int ++ */ ++ private $limit; ++ ++ /** ++ * @var string ++ */ ++ private $pattern; ++ ++ /** ++ * @var string ++ */ ++ private $error; ++ ++ /** ++ * @param string $message ++ * @param int $limit ++ * @param int $repeat ++ */ ++ public function setExpectedMessage(string $message, int $limit, int $repeat = 0) ++ { ++ $this->message = ($repeat > 0) ? str_repeat($message, $repeat) : $message; ++ $this->limit = $limit; ++ $this->position = 0; ++ } ++ ++ /** ++ * @param string $level ++ * @return int ++ */ ++ public function setExpectedLevel(string $level) ++ { ++ return $this->level = $level; ++ } ++ ++ /** ++ * @return string ++ */ ++ public function getExpectedLevel(): string ++ { ++ return $this->level ?: 'WARNING'; ++ } ++ ++ /** ++ * @param string $line ++ * @return bool ++ */ ++ public function checkTruncatedMessage(string $line) ++ { ++ if ($this->message === null) { ++ throw new \LogicException('The message has not been set'); ++ } ++ $lineLen = strlen($line); ++ if (!$this->checkLineLength($line)) { ++ return false; ++ } ++ $this->pattern = '/^PHP message: (.*?)(\.\.\.)?$/'; ++ if (preg_match($this->pattern, $line, $matches) === 0) { ++ return $this->error("Unexpected truncated message: {$line}"); ++ } ++ ++ if ($lineLen === $this->limit) { ++ if (!isset($matches[2])) { ++ return $this->error("The truncated line is not ended with '...'"); ++ } ++ if (!$this->checkMessage($matches[1])) { ++ return false; ++ } ++ } else { ++ if (isset($matches[2])) { ++ // this is expecting that the expected message does not end with '...' ++ // which should not be an issue for the test purpose. ++ return $this->error("The line is complete and should not end with '...'"); ++ } ++ if (!$this->checkMessage($matches[1], -1)) { ++ return false; ++ } ++ } ++ ++ return true; ++ } ++ ++ /** ++ * @param array $lines ++ * @param bool $terminated ++ * @param bool $decorated ++ * @return bool ++ */ ++ public function checkWrappedMessage(array $lines, bool $terminated = true, bool $decorated = true) ++ { ++ if ($this->message === null) { ++ throw new \LogicException('The message has not been set'); ++ } ++ if ($decorated) { ++ $this->pattern = sprintf( ++ '/^(%s %s: %s)"([^"]*)"(.*)?$/', ++ self::P_TIME, ++ $this->getExpectedLevel(), ++ self::P_PREFIX ++ ); ++ } else { ++ $this->pattern = null; ++ } ++ ++ $idx = 0; ++ foreach ($lines as $idx => $line) { ++ if (!$this->checkLine($line)) { ++ break; ++ } ++ } ++ ++ if ($this->suffixPosition > 0) { ++ $suffixPattern = sprintf( ++ '/^%s %s: %s(.*)$/', ++ self::P_TIME, $this->getExpectedLevel(), ++ self::P_PREFIX ++ ); ++ $line = $lines[++$idx]; ++ if (preg_match($suffixPattern, $line, $matches) === 0) { ++ return $this->error("Unexpected line: $line"); ++ } ++ if ($matches[1] !== substr(self::FINAL_SUFFIX, $this->suffixPosition)) { ++ return $this->error( ++ "The suffix has not been finished from position $this->suffixPosition in line: $line" ++ ); ++ } ++ } ++ ++ if ($terminated) { ++ return $this->expectTerminatorLines($lines, $idx); ++ } ++ ++ return true; ++ } ++ ++ /** ++ * @param string $line ++ * @return bool ++ */ ++ private function checkLine(string $line) ++ { ++ if ($this->pattern === null) { ++ // plain (not decorated) output ++ $out = rtrim($line); ++ $finalSuffix = null; ++ } elseif (($res = preg_match($this->pattern, $line, $matches)) > 0) { ++ $out = $matches[2]; ++ $finalSuffix = $matches[3] ?? false; ++ } else { ++ return $this->error("Unexpected line: $line"); ++ } ++ ++ $rem = strlen($this->message) - $this->position; ++ $lineLen = strlen($line); ++ if (!$this->checkLineLength($line, $lineLen)) { ++ return false; ++ } ++ if (!$this->checkMessage($out, $this->position)) { ++ return false; ++ } ++ $outLen = strlen($out); ++ if ($rem > $outLen) { // continuous line ++ if ($lineLen !== $this->limit) { ++ if ($lineLen + ($rem - $outLen) < $this->limit) { ++ return $this->error("Printed less than the message len"); ++ } ++ return $this->error( ++ "The continuous line length is $lineLen but it should equal to limit $this->limit" ++ ); ++ } ++ $this->position += $outLen; ++ return true; ++ } ++ if ($rem !== $outLen) { ++ return $this->error("Printed more than the message len"); ++ } ++ if ($finalSuffix === null || $finalSuffix === "") { ++ return false; ++ } ++ if ($finalSuffix === false) { ++ return $this->error("No final suffix"); ++ } ++ if (strpos(self::FINAL_SUFFIX, $finalSuffix) === false) { ++ return $this->error("The final suffix has to be equal to ', pipe is closed'"); ++ } ++ if (self::FINAL_SUFFIX !== $finalSuffix) { ++ $this->suffixPosition = strlen($finalSuffix); ++ } ++ // complete final suffix printed ++ return false; ++ } ++ ++ /** ++ * @param string $line ++ * @param int $lineLen ++ * @return bool ++ */ ++ private function checkLineLength(string $line, $lineLen = null) { ++ $lineLen = $lineLen ?: strlen($line); ++ if ($lineLen > $this->limit) { ++ return $this->error( ++ "The line length is $lineLen which is higher than limit $this->limit" ++ ); ++ } ++ ++ return true; ++ } ++ ++ /** ++ * @param string $matchedMessage ++ * @param int $expectedMessageStart ++ * @return bool ++ */ ++ private function checkMessage(string $matchedMessage, int $expectedMessageStart = 0) ++ { ++ if ($expectedMessageStart < 0) { ++ $expectedMessage = $this->message; ++ } else { ++ $expectedMessage = substr($this->message, $expectedMessageStart, strlen($matchedMessage)); ++ } ++ if ($expectedMessage !== $matchedMessage) { ++ return $this->error( ++ sprintf( ++ "The actual string(%d) does not match expected string(%d):\n", ++ strlen($matchedMessage), ++ strlen($expectedMessage) ++ ) . ++ "- EXPECT: '$expectedMessage'\n" . ++ "- ACTUAL: '$matchedMessage'" ++ ); ++ } ++ ++ return true; ++ } ++ ++ /** ++ * @param array $lines ++ * @return bool ++ */ ++ public function expectStartingLines(array $lines) ++ { ++ if ($this->getError()) { ++ return false; ++ } ++ ++ if (count($lines) < 2) { ++ return $this->error("No starting lines"); ++ } ++ ++ return ( ++ $this->expectNotice($lines[0], 'fpm is running, pid \d+') && ++ $this->expectNotice($lines[1], 'ready to handle connections') ++ ); ++ } ++ ++ /** ++ * @param array $lines ++ * @param int $idx ++ * @return bool ++ */ ++ public function expectTerminatorLines(array $lines, int $idx = -1) ++ { ++ if ($this->getError()) { ++ return false; ++ } ++ ++ if (count($lines) - $idx < 3) { ++ return $this->error("No terminating lines"); ++ } ++ ++ return ( ++ $this->expectNotice($lines[++$idx], 'Terminating ...') && ++ $this->expectNotice($lines[++$idx], 'exiting, bye-bye!') ++ ); ++ } ++ ++ /** ++ * @param string $type ++ * @param string $line ++ * @param string $expectedMessage ++ * @param string|null $pool ++ * @return bool ++ */ ++ public function expectEntry(string $type, string $line, string $expectedMessage, $pool = null) ++ { ++ if ($this->getError()) { ++ return false; ++ } ++ if ($pool !== null) { ++ $expectedMessage = '\[pool ' . $pool . '\] ' . $expectedMessage; ++ } ++ ++ $line = rtrim($line); ++ $pattern = sprintf('/^%s %s: %s$/', self::P_TIME, $type, $expectedMessage); ++ ++ if (preg_match($pattern, $line, $matches) === 0) { ++ return $this->error( ++ "The $type does not match expected message:\n" . ++ "- PATTERN: $pattern\n" . ++ "- MESSAGE: $line\n" . ++ "- EXPECT: '$expectedMessage'\n" . ++ "- ACTUAL: '" . substr($line, strpos($line, $type) + strlen($type) + 2) . "'" ++ ); ++ } ++ ++ return true; ++ } ++ ++ /** ++ * @param string $line ++ * @param string $expectedMessage ++ * @param string|null $pool ++ * @return bool ++ */ ++ public function expectDebug(string $line, string $expectedMessage, $pool = null) ++ { ++ return $this->expectEntry('DEBUG', $line, $expectedMessage, $pool); ++ } ++ ++ /** ++ * @param string $line ++ * @param string $expectedMessage ++ * @param string|null $pool ++ * @return bool ++ */ ++ public function expectNotice(string $line, string $expectedMessage, $pool = null) ++ { ++ return $this->expectEntry('NOTICE', $line, $expectedMessage, $pool); ++ } ++ ++ /** ++ * @param string $line ++ * @param string $expectedMessage ++ * @param string|null $pool ++ * @return bool ++ */ ++ public function expectWarning(string $line, string $expectedMessage, $pool = null) ++ { ++ return $this->expectEntry('WARNING', $line, $expectedMessage, $pool); ++ } ++ ++ /** ++ * @param string $line ++ * @param string $expectedMessage ++ * @param string|null $pool ++ * @return bool ++ */ ++ public function expectError(string $line, string $expectedMessage, $pool = null) ++ { ++ return $this->expectEntry('ERROR', $line, $expectedMessage, $pool); ++ } ++ ++ /** ++ * @param string $line ++ * @param string $expectedMessage ++ * @param string|null $pool ++ * @return bool ++ */ ++ public function expectAlert(string $line, string $expectedMessage, $pool = null) ++ { ++ return $this->expectEntry('ALERT', $line, $expectedMessage, $pool); ++ } ++ ++ ++ /** ++ * @param string $msg ++ * @return bool ++ */ ++ private function error(string $msg) ++ { ++ $this->error = $msg; ++ echo "ERROR: $msg\n"; ++ return false; ++ } ++ ++ /** ++ * @return string ++ */ ++ public function getError() ++ { ++ return $this->error; ++ } ++} ++ ++if (isset($argv[1]) && $argv[1] === 'logtool-selftest') { ++ $cases = [ ++ [ ++ 'limit' => 1050, ++ 'lines' => [ ++ '[08-Oct-2017 19:53:50] WARNING: [pool unconfined] child 23183 said into stderr: "' . ++ str_repeat('a', 968) . '"', ++ '[08-Oct-2017 19:53:50] WARNING: [pool unconfined] child 23183 said into stderr: "' . ++ str_repeat('a', 968) . '"', ++ '[08-Oct-2017 19:53:50] WARNING: [pool unconfined] child 23183 said into stderr: "' . ++ str_repeat('a', 112) . '", pipe is closed', ++ '[08-Oct-2017 19:53:55] NOTICE: Terminating ...', ++ '[08-Oct-2017 19:53:55] NOTICE: exiting, bye-bye!', ++ ], ++ 'message' => str_repeat('a', 2048), ++ 'type' => 'stdio', ++ ], ++ [ ++ 'limit' => 1050, ++ 'lines' => [ ++ '[08-Oct-2017 19:53:50] WARNING: [pool unconfined] child 23183 said into stderr: "' . ++ str_repeat('a', 968) . '"', ++ '[08-Oct-2017 19:53:50] WARNING: [pool unconfined] child 23183 said into stderr: "' . ++ str_repeat('a', 968) . '"', ++ '[08-Oct-2017 19:53:50] WARNING: [pool unconfined] child 23183 said into stderr: "' . ++ str_repeat('a', 964) . '", pi', ++ '[08-Oct-2017 19:53:50] WARNING: [pool unconfined] child 23183 said into stderr: pe is closed', ++ '[08-Oct-2017 19:53:55] NOTICE: Terminating ...', ++ '[08-Oct-2017 19:53:55] NOTICE: exiting, bye-bye!', ++ ], ++ 'message' => str_repeat('a', 2900), ++ 'type' => 'stdio', ++ ], ++ [ ++ 'limit' => 1024, ++ 'line' => '[08-Oct-2017 19:53:50] WARNING: ' . str_repeat('a',989) . '...', ++ 'message' => str_repeat('a', 2900), ++ 'type' => 'message', ++ ], ++ [ ++ 'limit' => 1024, ++ 'line' => '[08-Oct-2017 19:53:50] WARNING: ' . str_repeat('a',20), ++ 'message' => str_repeat('a', 20), ++ 'type' => 'message', ++ ], ++ ]; ++ foreach ($cases as $case) { ++ printf("Test message with len %d and limit %d: ", strlen($case['message']), $case['limit']); ++ $logTool = new LogTool(); ++ $logTool->setExpectedMessage($case['message'], $case['limit']); ++ if ($case['type'] === 'stdio') { ++ $logTool->checkWrappedMessage($case['lines']); ++ } else { ++ $logTool->checkTruncatedMessage($case['line']); ++ } ++ if (!$logTool->getError()) { ++ echo "OK\n"; ++ } ++ } ++ echo "Done\n"; ++} +diff --git a/sapi/fpm/tests/response.inc b/sapi/fpm/tests/response.inc +new file mode 100644 +index 00000000000..24285bf560c +--- /dev/null ++++ b/sapi/fpm/tests/response.inc +@@ -0,0 +1,281 @@ ++<?php ++ ++namespace FPM; ++ ++class Response ++{ ++ const HEADER_SEPARATOR = "\r\n\r\n"; ++ ++ /** ++ * @var array ++ */ ++ private $data; ++ ++ /** ++ * @var string ++ */ ++ private $rawData; ++ ++ /** ++ * @var string ++ */ ++ private $rawHeaders; ++ ++ /** ++ * @var string ++ */ ++ private $rawBody; ++ ++ /** ++ * @var array ++ */ ++ private $headers; ++ ++ /** ++ * @var bool ++ */ ++ private $valid; ++ ++ /** ++ * @var bool ++ */ ++ private $expectInvalid; ++ ++ /** ++ * @param string|array|null $data ++ * @param bool $expectInvalid ++ */ ++ public function __construct($data = null, $expectInvalid = false) ++ { ++ if (!is_array($data)) { ++ $data = [ ++ 'response' => $data, ++ 'err_response' => null, ++ 'out_response' => $data, ++ ]; ++ } ++ ++ $this->data = $data; ++ $this->expectInvalid = $expectInvalid; ++ } ++ ++ /** ++ * @param mixed $body ++ * @param string $contentType ++ * @return Response ++ */ ++ public function expectBody($body, $contentType = 'text/html') ++ { ++ if ($multiLine = is_array($body)) { ++ $body = implode("\n", $body); ++ } ++ ++ if ( ++ $this->checkIfValid() && ++ $this->checkDefaultHeaders($contentType) && ++ $body !== $this->rawBody ++ ) { ++ if ($multiLine) { ++ $this->error( ++ "==> The expected body:\n$body\n" . ++ "==> does not match the actual body:\n$this->rawBody" ++ ); ++ } else { ++ $this->error( ++ "The expected body '$body' does not match actual body '$this->rawBody'" ++ ); ++ } ++ } ++ ++ return $this; ++ } ++ ++ /** ++ * @return Response ++ */ ++ public function expectEmptyBody() ++ { ++ return $this->expectBody(''); ++ } ++ ++ /** ++ * @param string $contentType ++ * @return string|null ++ */ ++ public function getBody($contentType = 'text/html') ++ { ++ if ($this->checkIfValid() && $this->checkDefaultHeaders($contentType)) { ++ return $this->rawBody; ++ } ++ ++ return null; ++ } ++ ++ /** ++ * Print raw body ++ */ ++ public function dumpBody() ++ { ++ var_dump($this->getBody()); ++ } ++ ++ /** ++ * Print raw body ++ */ ++ public function printBody() ++ { ++ echo $this->getBody(); ++ } ++ ++ /** ++ * Debug response output ++ */ ++ public function debugOutput() ++ { ++ echo "-------------- RESPONSE: --------------\n"; ++ echo "OUT:\n"; ++ echo $this->data['out_response']; ++ echo "ERR:\n"; ++ echo $this->data['err_response']; ++ echo "---------------------------------------\n\n"; ++ } ++ ++ /** ++ * @return string|null ++ */ ++ public function getErrorData() ++ { ++ return $this->data['err_response']; ++ } ++ ++ /** ++ * Check if the response is valid and if not emit error message ++ * ++ * @return bool ++ */ ++ private function checkIfValid() ++ { ++ if ($this->isValid()) { ++ return true; ++ } ++ ++ if (!$this->expectInvalid) { ++ $this->error("The response is invalid: $this->rawData"); ++ } ++ ++ return false; ++ } ++ ++ /** ++ * @param string $contentType ++ * @return bool ++ */ ++ private function checkDefaultHeaders($contentType) ++ { ++ // check default headers ++ return ( ++ $this->checkHeader('X-Powered-By', '|^PHP/7|', true) && ++ $this->checkHeader('Content-type', '|^' . $contentType . '(;\s?charset=\w+)?|', true) ++ ); ++ } ++ ++ /** ++ * @param string $name ++ * @param string $value ++ * @param bool $useRegex ++ * @return bool ++ */ ++ private function checkHeader(string $name, string $value, $useRegex = false) ++ { ++ $lcName = strtolower($name); ++ $headers = $this->getHeaders(); ++ if (!isset($headers[$lcName])) { ++ return $this->error("The header $name is not present"); ++ } ++ $header = $headers[$lcName]; ++ ++ if (!$useRegex) { ++ if ($header === $value) { ++ return true; ++ } ++ return $this->error("The header $name value '$header' is not the same as '$value'"); ++ } ++ ++ if (!preg_match($value, $header)) { ++ return $this->error("The header $name value '$header' does not match RegExp '$value'"); ++ } ++ ++ return true; ++ } ++ ++ /** ++ * @return array|null ++ */ ++ private function getHeaders() ++ { ++ if (!$this->isValid()) { ++ return null; ++ } ++ ++ if (is_array($this->headers)) { ++ return $this->headers; ++ } ++ ++ $headerRows = explode("\r\n", $this->rawHeaders); ++ $headers = []; ++ foreach ($headerRows as $headerRow) { ++ $colonPosition = strpos($headerRow, ':'); ++ if ($colonPosition === false) { ++ $this->error("Invalid header row (no colon): $headerRow"); ++ } ++ $headers[strtolower(substr($headerRow, 0, $colonPosition))] = trim( ++ substr($headerRow, $colonPosition + 1) ++ ); ++ } ++ ++ return ($this->headers = $headers); ++ } ++ ++ /** ++ * @return bool ++ */ ++ private function isValid() ++ { ++ if ($this->valid === null) { ++ $this->processData(); ++ } ++ ++ return $this->valid; ++ } ++ ++ /** ++ * Process data and set validity and raw data ++ */ ++ private function processData() ++ { ++ $this->rawData = $this->data['out_response']; ++ $this->valid = ( ++ !is_null($this->rawData) && ++ strpos($this->rawData, self::HEADER_SEPARATOR) ++ ); ++ if ($this->valid) { ++ list ($this->rawHeaders, $this->rawBody) = array_map( ++ 'trim', ++ explode(self::HEADER_SEPARATOR, $this->rawData) ++ ); ++ } ++ } ++ ++ /** ++ * Emit error message ++ * ++ * @param string $message ++ * @return bool ++ */ ++ private function error($message) ++ { ++ echo "ERROR: $message\n"; ++ ++ return false; ++ } ++} +diff --git a/sapi/fpm/tests/tester.inc b/sapi/fpm/tests/tester.inc +new file mode 100644 +index 00000000000..c1384133f4f +--- /dev/null ++++ b/sapi/fpm/tests/tester.inc +@@ -0,0 +1,1279 @@ ++<?php ++ ++namespace FPM; ++ ++use Adoy\FastCGI\Client; ++ ++require_once 'fcgi.inc'; ++require_once 'logtool.inc'; ++require_once 'response.inc'; ++ ++class Tester ++{ ++ /** ++ * Config directory for included files. ++ */ ++ const CONF_DIR = __DIR__ . '/conf.d'; ++ ++ /** ++ * File extension for access log. ++ */ ++ const FILE_EXT_LOG_ACC = 'acc.log'; ++ ++ /** ++ * File extension for error log. ++ */ ++ const FILE_EXT_LOG_ERR = 'err.log'; ++ ++ /** ++ * File extension for slow log. ++ */ ++ const FILE_EXT_LOG_SLOW = 'slow.log'; ++ ++ /** ++ * File extension for PID file. ++ */ ++ const FILE_EXT_PID = 'pid'; ++ ++ /** ++ * @var array ++ */ ++ static private $supportedFiles = [ ++ self::FILE_EXT_LOG_ACC, ++ self::FILE_EXT_LOG_ERR, ++ self::FILE_EXT_LOG_SLOW, ++ self::FILE_EXT_PID, ++ 'src.php', ++ 'ini', ++ 'skip.ini', ++ '*.sock', ++ ]; ++ ++ /** ++ * @var array ++ */ ++ static private $filesToClean = ['.user.ini']; ++ ++ /** ++ * @var bool ++ */ ++ private $debug; ++ ++ /** ++ * @var array ++ */ ++ private $clients; ++ ++ /** ++ * @var LogTool ++ */ ++ private $logTool; ++ ++ /** ++ * Configuration template ++ * ++ * @var string ++ */ ++ private $configTemplate; ++ ++ /** ++ * The PHP code to execute ++ * ++ * @var string ++ */ ++ private $code; ++ ++ /** ++ * @var array ++ */ ++ private $options; ++ ++ /** ++ * @var string ++ */ ++ private $fileName; ++ ++ /** ++ * @var resource ++ */ ++ private $masterProcess; ++ ++ /** ++ * @var resource ++ */ ++ private $outDesc; ++ ++ /** ++ * @var array ++ */ ++ private $ports = []; ++ ++ /** ++ * @var string ++ */ ++ private $error; ++ ++ /** ++ * The last response for the request call ++ * ++ * @var Response ++ */ ++ private $response; ++ ++ /** ++ * Clean all the created files up ++ * ++ * @param int $backTraceIndex ++ */ ++ static public function clean($backTraceIndex = 1) ++ { ++ $filePrefix = self::getCallerFileName($backTraceIndex); ++ if (substr($filePrefix, -6) === 'clean.') { ++ $filePrefix = substr($filePrefix, 0, -6); ++ } ++ ++ $filesToClean = array_merge( ++ array_map( ++ function($fileExtension) use ($filePrefix) { ++ return $filePrefix . $fileExtension; ++ }, ++ self::$supportedFiles ++ ), ++ array_map( ++ function($fileExtension) { ++ return __DIR__ . '/' . $fileExtension; ++ }, ++ self::$filesToClean ++ ) ++ ); ++ // clean all the root files ++ foreach ($filesToClean as $filePattern) { ++ foreach (glob($filePattern) as $filePath) { ++ unlink($filePath); ++ } ++ } ++ // clean config files ++ if (is_dir(self::CONF_DIR)) { ++ foreach(glob(self::CONF_DIR . '/*.conf') as $name) { ++ unlink($name); ++ } ++ rmdir(self::CONF_DIR); ++ } ++ } ++ ++ /** ++ * @param int $backTraceIndex ++ * @return string ++ */ ++ static private function getCallerFileName($backTraceIndex = 1) ++ { ++ $backtrace = debug_backtrace(); ++ if (isset($backtrace[$backTraceIndex]['file'])) { ++ $filePath = $backtrace[$backTraceIndex]['file']; ++ } else { ++ $filePath = __FILE__; ++ } ++ ++ return substr($filePath, 0, -strlen(pathinfo($filePath, PATHINFO_EXTENSION))); ++ } ++ ++ /** ++ * @return bool|string ++ */ ++ static public function findExecutable() ++ { ++ $phpPath = getenv("TEST_PHP_EXECUTABLE"); ++ for ($i = 0; $i < 2; $i++) { ++ $slashPosition = strrpos($phpPath, "/"); ++ if ($slashPosition) { ++ $phpPath = substr($phpPath, 0, $slashPosition); ++ } else { ++ break; ++ } ++ } ++ ++ if ($phpPath && is_dir($phpPath)) { ++ if (file_exists($phpPath."/fpm/php-fpm") && is_executable($phpPath."/fpm/php-fpm")) { ++ /* gotcha */ ++ return $phpPath."/fpm/php-fpm"; ++ } ++ $phpSbinFpmi = $phpPath."/sbin/php-fpm"; ++ if (file_exists($phpSbinFpmi) && is_executable($phpSbinFpmi)) { ++ return $phpSbinFpmi; ++ } ++ } ++ ++ // try local php-fpm ++ $fpmPath = dirname(__DIR__) . '/php-fpm'; ++ if (file_exists($fpmPath) && is_executable($fpmPath)) { ++ return $fpmPath; ++ } ++ ++ return false; ++ } ++ ++ /** ++ * Skip test if any of the supplied files does not exist. ++ * ++ * @param mixed $files ++ */ ++ static public function skipIfAnyFileDoesNotExist($files) ++ { ++ if (!is_array($files)) { ++ $files = array($files); ++ } ++ foreach ($files as $file) { ++ if (!file_exists($file)) { ++ die("skip File $file does not exist"); ++ } ++ } ++ } ++ ++ /** ++ * Skip test if config file is invalid. ++ * ++ * @param string $configTemplate ++ * @throws \Exception ++ */ ++ static public function skipIfConfigFails(string $configTemplate) ++ { ++ $tester = new self($configTemplate, '', [], self::getCallerFileName()); ++ $testResult = $tester->testConfig(); ++ if ($testResult !== null) { ++ self::clean(2); ++ die("skip $testResult"); ++ } ++ } ++ ++ /** ++ * Skip test if IPv6 is not supported. ++ */ ++ static public function skipIfIPv6IsNotSupported() ++ { ++ @stream_socket_client('tcp://[::1]:0', $errno); ++ if ($errno != 111) { ++ die('skip IPv6 is not supported.'); ++ } ++ } ++ ++ /** ++ * Skip if running on Travis. ++ * ++ * @param $message ++ */ ++ static public function skipIfTravis($message) ++ { ++ if (getenv("TRAVIS")) { ++ die('skip Travis: ' . $message); ++ } ++ } ++ ++ /** ++ * Tester constructor. ++ * ++ * @param string|array $configTemplate ++ * @param string $code ++ * @param array $options ++ * @param string $fileName ++ */ ++ public function __construct( ++ $configTemplate, ++ string $code = '', ++ array $options = [], ++ $fileName = null ++ ) { ++ $this->configTemplate = $configTemplate; ++ $this->code = $code; ++ $this->options = $options; ++ $this->fileName = $fileName ?: self::getCallerFileName(); ++ $this->logTool = new LogTool(); ++ $this->debug = (bool) getenv('TEST_FPM_DEBUG'); ++ } ++ ++ /** ++ * @param string $ini ++ */ ++ public function setUserIni(string $ini) ++ { ++ $iniFile = __DIR__ . '/.user.ini'; ++ file_put_contents($iniFile, $ini); ++ } ++ ++ /** ++ * Test configuration file. ++ * ++ * @return null|string ++ * @throws \Exception ++ */ ++ public function testConfig() ++ { ++ $configFile = $this->createConfig(); ++ $cmd = self::findExecutable() . ' -t -y ' . $configFile . ' 2>&1'; ++ exec($cmd, $output, $code); ++ if ($code) { ++ return preg_replace("/\[.+?\]/", "", $output[0]); ++ } ++ ++ return null; ++ } ++ ++ /** ++ * Start PHP-FPM master process ++ * ++ * @param string $extraArgs ++ * @return bool ++ * @throws \Exception ++ */ ++ public function start(string $extraArgs = '') ++ { ++ $configFile = $this->createConfig(); ++ $desc = $this->outDesc ? [] : [1 => array('pipe', 'w')]; ++ $asRoot = getenv('TEST_FPM_RUN_AS_ROOT') ? '--allow-to-run-as-root' : ''; ++ $cmd = self::findExecutable() . " $asRoot -F -O -y $configFile $extraArgs"; ++ /* Since it's not possible to spawn a process under linux without using a ++ * shell in php (why?!?) we need a little shell trickery, so that we can ++ * actually kill php-fpm */ ++ $this->masterProcess = proc_open( ++ "killit () { kill \$child 2> /dev/null; }; " . ++ "trap killit TERM; $cmd 2>&1 & child=\$!; wait", ++ $desc, ++ $pipes ++ ); ++ register_shutdown_function( ++ function($masterProcess) use($configFile) { ++ @unlink($configFile); ++ if (is_resource($masterProcess)) { ++ @proc_terminate($masterProcess); ++ while (proc_get_status($masterProcess)['running']) { ++ usleep(10000); ++ } ++ } ++ }, ++ $this->masterProcess ++ ); ++ if (!$this->outDesc !== false) { ++ $this->outDesc = $pipes[1]; ++ } ++ ++ return true; ++ } ++ ++ /** ++ * Run until needle is found in the log. ++ * ++ * @param string $needle ++ * @param int $max ++ * @return bool ++ * @throws \Exception ++ */ ++ public function runTill(string $needle, $max = 10) ++ { ++ $this->start(); ++ $found = false; ++ for ($i = 0; $i < $max; $i++) { ++ $line = $this->getLogLine(); ++ if (is_null($line)) { ++ break; ++ } ++ if (preg_match($needle, $line) === 1) { ++ $found = true; ++ break; ++ } ++ } ++ $this->close(true); ++ ++ if (!$found) { ++ return $this->error("The search pattern not found"); ++ } ++ ++ return true; ++ } ++ ++ /** ++ * Check if connection works. ++ * ++ * @param string $host ++ * @param null|string $successMessage ++ * @param null|string $errorMessage ++ * @param int $attempts ++ * @param int $delay ++ */ ++ public function checkConnection( ++ $host = '127.0.0.1', ++ $successMessage = null, ++ $errorMessage = 'Connection failed', ++ $attempts = 20, ++ $delay = 50000 ++ ) { ++ $i = 0; ++ do { ++ if ($i > 0 && $delay > 0) { ++ usleep($delay); ++ } ++ $fp = @fsockopen($host, $this->getPort()); ++ } while ((++$i < $attempts) && !$fp); ++ ++ if ($fp) { ++ $this->message($successMessage); ++ fclose($fp); ++ } else { ++ $this->message($errorMessage); ++ } ++ } ++ ++ ++ /** ++ * Execute request with parameters ordered for better checking. ++ * ++ * @param string $address ++ * @param string|null $successMessage ++ * @param string|null $errorMessage ++ * @param string $uri ++ * @param string $query ++ * @param array $headers ++ * @return Response ++ */ ++ public function checkRequest( ++ string $address, ++ string $successMessage = null, ++ string $errorMessage = null, ++ $uri = '/ping', ++ $query = '', ++ $headers = [] ++ ) { ++ return $this->request($query, $headers, $uri, $address, $successMessage, $errorMessage); ++ } ++ ++ /** ++ * Execute and check ping request. ++ * ++ * @param string $address ++ * @param string $pingPath ++ * @param string $pingResponse ++ */ ++ public function ping( ++ string $address = '{{ADDR}}', ++ string $pingResponse = 'pong', ++ string $pingPath = '/ping' ++ ) { ++ $response = $this->request('', [], $pingPath, $address); ++ $response->expectBody($pingResponse, 'text/plain'); ++ } ++ ++ /** ++ * Execute and check status request(s). ++ * ++ * @param array $expectedFields ++ * @param string|null $address ++ * @param string $statusPath ++ * @param mixed $formats ++ * @throws \Exception ++ */ ++ public function status( ++ array $expectedFields, ++ string $address = null, ++ string $statusPath = '/status', ++ $formats = ['plain', 'html', 'xml', 'json'] ++ ) { ++ if (!is_array($formats)) { ++ $formats = [$formats]; ++ } ++ ++ require_once "status.inc"; ++ $status = new Status(); ++ foreach ($formats as $format) { ++ $query = $format === 'plain' ? '' : $format; ++ $response = $this->request($query, [], $statusPath, $address); ++ $status->checkStatus($response, $expectedFields, $format); ++ } ++ } ++ ++ /** ++ * Get request params array. ++ * ++ * @param string $query ++ * @param array $headers ++ * @param string|null $uri ++ * @param string|null $address ++ * @param string|null $successMessage ++ * @param string|null $errorMessage ++ * @param bool $connKeepAlive ++ * @return array ++ */ ++ private function getRequestParams( ++ string $query = '', ++ array $headers = [], ++ string $uri = null ++ ) { ++ if (is_null($uri)) { ++ $uri = $this->makeSourceFile(); ++ } ++ ++ $params = array_merge( ++ [ ++ 'GATEWAY_INTERFACE' => 'FastCGI/1.0', ++ 'REQUEST_METHOD' => 'GET', ++ 'SCRIPT_FILENAME' => $uri, ++ 'SCRIPT_NAME' => $uri, ++ 'QUERY_STRING' => $query, ++ 'REQUEST_URI' => $uri . ($query ? '?'.$query : ""), ++ 'DOCUMENT_URI' => $uri, ++ 'SERVER_SOFTWARE' => 'php/fcgiclient', ++ 'REMOTE_ADDR' => '127.0.0.1', ++ 'REMOTE_PORT' => '7777', ++ 'SERVER_ADDR' => '127.0.0.1', ++ 'SERVER_PORT' => '80', ++ 'SERVER_NAME' => php_uname('n'), ++ 'SERVER_PROTOCOL' => 'HTTP/1.1', ++ 'DOCUMENT_ROOT' => __DIR__, ++ 'CONTENT_TYPE' => '', ++ 'CONTENT_LENGTH' => 0 ++ ], ++ $headers ++ ); ++ ++ return array_filter($params, function($value) { ++ return !is_null($value); ++ }); ++ } ++ ++ /** ++ * Execute request. ++ * ++ * @param string $query ++ * @param array $headers ++ * @param string|null $uri ++ * @param string|null $address ++ * @param string|null $successMessage ++ * @param string|null $errorMessage ++ * @param bool $connKeepAlive ++ * @return Response ++ */ ++ public function request( ++ string $query = '', ++ array $headers = [], ++ string $uri = null, ++ string $address = null, ++ string $successMessage = null, ++ string $errorMessage = null, ++ bool $connKeepAlive = false ++ ) { ++ if ($this->hasError()) { ++ return new Response(null, true); ++ } ++ if (is_null($uri)) { ++ $uri = $this->makeSourceFile(); ++ } ++ ++ $params = $this->getRequestParams($query, $headers, $uri); ++ ++ try { ++ $this->response = new Response( ++ $this->getClient($address, $connKeepAlive)->request_data($params, false) ++ ); ++ $this->message($successMessage); ++ } catch (\Exception $exception) { ++ if ($errorMessage === null) { ++ $this->error("Request failed", $exception); ++ } else { ++ $this->message($errorMessage); ++ } ++ $this->response = new Response(); ++ } ++ if ($this->debug) { ++ $this->response->debugOutput(); ++ } ++ return $this->response; ++ } ++ ++ /** ++ * Execute multiple requests in parallel. ++ * ++ * @param array|int $requests ++ * @param string|null $address ++ * @param string|null $successMessage ++ * @param string|null $errorMessage ++ * @param bool $connKeepAlive ++ * @return Response[] ++ * @throws \Exception ++ */ ++ public function multiRequest( ++ $requests, ++ string $address = null, ++ string $successMessage = null, ++ string $errorMessage = null, ++ bool $connKeepAlive = false ++ ) { ++ if ($this->hasError()) { ++ return new Response(null, true); ++ } ++ ++ if (is_numeric($requests)) { ++ $requests = array_fill(0, $requests, []); ++ } elseif (!is_array($requests)) { ++ throw new \Exception('Requests can be either numeric or array'); ++ } ++ ++ try { ++ $connections = array_map(function ($requestData) use ($address, $connKeepAlive) { ++ $client = $this->getClient($address, $connKeepAlive); ++ $params = $this->getRequestParams( ++ $requestData['query'] ?? '', ++ $requestData['headers'] ?? [], ++ $requestData['uri'] ?? null ++ ); ++ return [ ++ 'client' => $client, ++ 'requestId' => $client->async_request($params, false), ++ ]; ++ }, $requests); ++ ++ $responses = array_map(function ($conn) { ++ $response = new Response($conn['client']->wait_for_response_data($conn['requestId'])); ++ if ($this->debug) { ++ $response->debugOutput(); ++ } ++ return $response; ++ }, $connections); ++ $this->message($successMessage); ++ return $responses; ++ } catch (\Exception $exception) { ++ if ($errorMessage === null) { ++ $this->error("Request failed", $exception); ++ } else { ++ $this->message($errorMessage); ++ } ++ } ++ } ++ ++ /** ++ * Get client. ++ * ++ * @param string $address ++ * @param bool $keepAlive ++ * @return Client ++ */ ++ private function getClient(string $address = null, $keepAlive = false) ++ { ++ $address = $address ? $this->processTemplate($address) : $this->getAddr(); ++ if ($address[0] === '/') { // uds ++ $host = 'unix://' . $address; ++ $port = -1; ++ } elseif ($address[0] === '[') { // ipv6 ++ $addressParts = explode(']:', $address); ++ $host = $addressParts[0]; ++ if (isset($addressParts[1])) { ++ $host .= ']'; ++ $port = $addressParts[1]; ++ } else { ++ $port = $this->getPort(); ++ } ++ } else { // ipv4 ++ $addressParts = explode(':', $address); ++ $host = $addressParts[0]; ++ $port = $addressParts[1] ?? $this->getPort(); ++ } ++ ++ if (!$keepAlive) { ++ return new Client($host, $port); ++ } ++ ++ if (!isset($this->clients[$host][$port])) { ++ $client = new Client($host, $port); ++ $client->setKeepAlive(true); ++ $this->clients[$host][$port] = $client; ++ } ++ ++ return $this->clients[$host][$port]; ++ } ++ ++ /** ++ * Display logs ++ * ++ * @param int $number ++ * @param string $ignore ++ */ ++ public function displayLog(int $number = 1, string $ignore = 'systemd') ++ { ++ /* Read $number lines or until EOF */ ++ while ($number > 0 || ($number < 0 && !feof($this->outDesc))) { ++ $a = fgets($this->outDesc); ++ if (empty($ignore) || !strpos($a, $ignore)) { ++ echo $a; ++ $number--; ++ } ++ } ++ } ++ ++ /** ++ * Get a single log line ++ * ++ * @return null|string ++ */ ++ private function getLogLine() ++ { ++ $read = [$this->outDesc]; ++ $write = null; ++ $except = null; ++ if (stream_select($read, $write, $except, 2 )) { ++ return fgets($this->outDesc); ++ } else { ++ return null; ++ } ++ } ++ ++ /** ++ * Get log lines ++ * ++ * @param int $number ++ * @param bool $skipBlank ++ * @param string $ignore ++ * @return array ++ */ ++ public function getLogLines(int $number = 1, bool $skipBlank = false, string $ignore = 'systemd') ++ { ++ $lines = []; ++ /* Read $n lines or until EOF */ ++ while ($number > 0 || ($number < 0 && !feof($this->outDesc))) { ++ $line = $this->getLogLine(); ++ if (is_null($line)) { ++ break; ++ } ++ if ((empty($ignore) || !strpos($line, $ignore)) && (!$skipBlank || strlen(trim($line)) > 0)) { ++ $lines[] = $line; ++ $number--; ++ } ++ } ++ ++ return $lines; ++ } ++ ++ /** ++ * @return mixed|string ++ */ ++ public function getLastLogLine() ++ { ++ $lines = $this->getLogLines(); ++ ++ return $lines[0] ?? ''; ++ } ++ ++ /** ++ * Send signal to the supplied PID or the server PID. ++ * ++ * @param string $signal ++ * @param int|null $pid ++ * @return string ++ */ ++ public function signal($signal, int $pid = null) ++ { ++ if (is_null($pid)) { ++ $pid = $this->getPid(); ++ } ++ ++ return exec("kill -$signal $pid"); ++ } ++ ++ /** ++ * Terminate master process ++ */ ++ public function terminate() ++ { ++ proc_terminate($this->masterProcess); ++ } ++ ++ /** ++ * Close all open descriptors and process resources ++ * ++ * @param bool $terminate ++ */ ++ public function close($terminate = false) ++ { ++ if ($terminate) { ++ $this->terminate(); ++ } ++ fclose($this->outDesc); ++ proc_close($this->masterProcess); ++ } ++ ++ /** ++ * Create a config file. ++ * ++ * @param string $extension ++ * @return string ++ * @throws \Exception ++ */ ++ private function createConfig($extension = 'ini') ++ { ++ if (is_array($this->configTemplate)) { ++ $configTemplates = $this->configTemplate; ++ if (!isset($configTemplates['main'])) { ++ throw new \Exception('The config template array has to have main config'); ++ } ++ $mainTemplate = $configTemplates['main']; ++ unset($configTemplates['main']); ++ if (!is_dir(self::CONF_DIR)) { ++ mkdir(self::CONF_DIR); ++ } ++ foreach ($configTemplates as $name => $configTemplate) { ++ $this->makeFile( ++ 'conf', ++ $this->processTemplate($configTemplate), ++ self::CONF_DIR, ++ $name ++ ); ++ } ++ } else { ++ $mainTemplate = $this->configTemplate; ++ } ++ ++ return $this->makeFile($extension, $this->processTemplate($mainTemplate)); ++ } ++ ++ /** ++ * Process template string. ++ * ++ * @param string $template ++ * @return string ++ */ ++ private function processTemplate(string $template) ++ { ++ $vars = [ ++ 'FILE:LOG:ACC' => ['getAbsoluteFile', self::FILE_EXT_LOG_ACC], ++ 'FILE:LOG:ERR' => ['getAbsoluteFile', self::FILE_EXT_LOG_ERR], ++ 'FILE:LOG:SLOW' => ['getAbsoluteFile', self::FILE_EXT_LOG_SLOW], ++ 'FILE:PID' => ['getAbsoluteFile', self::FILE_EXT_PID], ++ 'RFILE:LOG:ACC' => ['getRelativeFile', self::FILE_EXT_LOG_ACC], ++ 'RFILE:LOG:ERR' => ['getRelativeFile', self::FILE_EXT_LOG_ERR], ++ 'RFILE:LOG:SLOW' => ['getRelativeFile', self::FILE_EXT_LOG_SLOW], ++ 'RFILE:PID' => ['getRelativeFile', self::FILE_EXT_PID], ++ 'ADDR:IPv4' => ['getAddr', 'ipv4'], ++ 'ADDR:IPv4:ANY' => ['getAddr', 'ipv4-any'], ++ 'ADDR:IPv6' => ['getAddr', 'ipv6'], ++ 'ADDR:IPv6:ANY' => ['getAddr', 'ipv6-any'], ++ 'ADDR:UDS' => ['getAddr', 'uds'], ++ 'PORT' => ['getPort', 'ip'], ++ 'INCLUDE:CONF' => self::CONF_DIR . '/*.conf', ++ ]; ++ $aliases = [ ++ 'ADDR' => 'ADDR:IPv4', ++ 'FILE:LOG' => 'FILE:LOG:ERR', ++ ]; ++ foreach ($aliases as $aliasName => $aliasValue) { ++ $vars[$aliasName] = $vars[$aliasValue]; ++ } ++ ++ return preg_replace_callback( ++ '/{{([a-zA-Z0-9:]+)(\[\w+\])?}}/', ++ function ($matches) use ($vars) { ++ $varName = $matches[1]; ++ if (!isset($vars[$varName])) { ++ $this->error("Invalid config variable $varName"); ++ return 'INVALID'; ++ } ++ $pool = $matches[2] ?? 'default'; ++ $varValue = $vars[$varName]; ++ if (is_string($varValue)) { ++ return $varValue; ++ } ++ $functionName = array_shift($varValue); ++ $varValue[] = $pool; ++ return call_user_func_array([$this, $functionName], $varValue); ++ }, ++ $template ++ ); ++ } ++ ++ /** ++ * @param string $type ++ * @param string $pool ++ * @return string ++ */ ++ public function getAddr(string $type = 'ipv4', $pool = 'default') ++ { ++ $port = $this->getPort($type, $pool, true); ++ if ($type === 'uds') { ++ return $this->getFile($port . '.sock'); ++ } ++ ++ return $this->getHost($type) . ':' . $port; ++ } ++ ++ /** ++ * @param string $type ++ * @param string $pool ++ * @param bool $useAsId ++ * @return int ++ */ ++ public function getPort(string $type = 'ip', $pool = 'default', $useAsId = false) ++ { ++ if ($type === 'uds' && !$useAsId) { ++ return -1; ++ } ++ ++ if (isset($this->ports['values'][$pool])) { ++ return $this->ports['values'][$pool]; ++ } ++ $port = ($this->ports['last'] ?? 9000 + PHP_INT_SIZE - 1) + 1; ++ $this->ports['values'][$pool] = $this->ports['last'] = $port; ++ ++ return $port; ++ } ++ ++ /** ++ * @param string $type ++ * @return string ++ */ ++ public function getHost(string $type = 'ipv4') ++ { ++ switch ($type) { ++ case 'ipv6-any': ++ return '[::]'; ++ case 'ipv6': ++ return '[::1]'; ++ case 'ipv4-any': ++ return '0.0.0.0'; ++ default: ++ return '127.0.0.1'; ++ } ++ } ++ ++ /** ++ * Get listen address. ++ * ++ * @param string|null $template ++ * @return string ++ */ ++ public function getListen($template = null) ++ { ++ return $template ? $this->processTemplate($template) : $this->getAddr(); ++ } ++ ++ /** ++ * Get PID. ++ * ++ * @return int ++ */ ++ public function getPid() ++ { ++ $pidFile = $this->getFile('pid'); ++ if (!is_file($pidFile)) { ++ return (int) $this->error("PID file has not been created"); ++ } ++ $pidContent = file_get_contents($pidFile); ++ if (!is_numeric($pidContent)) { ++ return (int) $this->error("PID content '$pidContent' is not integer"); ++ } ++ ++ return (int) $pidContent; ++ } ++ ++ ++ /** ++ * @param string $extension ++ * @param string|null $dir ++ * @param string|null $name ++ * @return string ++ */ ++ private function getFile(string $extension, $dir = null, $name = null) ++ { ++ $fileName = (is_null($name) ? $this->fileName : $name . '.') . $extension; ++ ++ return is_null($dir) ? $fileName : $dir . '/' . $fileName; ++ } ++ ++ /** ++ * @param string $extension ++ * @return string ++ */ ++ private function getAbsoluteFile(string $extension) ++ { ++ return $this->getFile($extension); ++ } ++ ++ /** ++ * @param string $extension ++ * @return string ++ */ ++ private function getRelativeFile(string $extension) ++ { ++ $fileName = rtrim(basename($this->fileName), '.'); ++ ++ return $this->getFile($extension, null, $fileName); ++ } ++ ++ /** ++ * @param string $extension ++ * @param string $prefix ++ * @return string ++ */ ++ private function getPrefixedFile(string $extension, string $prefix = null) ++ { ++ $fileName = rtrim($this->fileName, '.'); ++ if (!is_null($prefix)) { ++ $fileName = $prefix . '/' . basename($fileName); ++ } ++ ++ return $this->getFile($extension, null, $fileName); ++ } ++ ++ /** ++ * @param string $extension ++ * @param string $content ++ * @param string|null $dir ++ * @param string|null $name ++ * @return string ++ */ ++ private function makeFile(string $extension, string $content = '', $dir = null, $name = null) ++ { ++ $filePath = $this->getFile($extension, $dir, $name); ++ file_put_contents($filePath, $content); ++ ++ return $filePath; ++ } ++ ++ /** ++ * @return string ++ */ ++ public function makeSourceFile() ++ { ++ return $this->makeFile('src.php', $this->code); ++ } ++ ++ /** ++ * @param string|null $msg ++ */ ++ private function message($msg) ++ { ++ if ($msg !== null) { ++ echo "$msg\n"; ++ } ++ } ++ ++ /** ++ * @param string $msg ++ * @param \Exception|null $exception ++ */ ++ private function error($msg, \Exception $exception = null) ++ { ++ $this->error = 'ERROR: ' . $msg; ++ if ($exception) { ++ $this->error .= '; EXCEPTION: ' . $exception->getMessage(); ++ } ++ $this->error .= "\n"; ++ ++ echo $this->error; ++ } ++ ++ /** ++ * @return bool ++ */ ++ private function hasError() ++ { ++ return !is_null($this->error) || !is_null($this->logTool->getError()); ++ } ++ ++ /** ++ * Expect file with a supplied extension to exist. ++ * ++ * @param string $extension ++ * @param string $prefix ++ * @return bool ++ */ ++ public function expectFile(string $extension, $prefix = null) ++ { ++ $filePath = $this->getPrefixedFile($extension, $prefix); ++ if (!file_exists($filePath)) { ++ return $this->error("The file $filePath does not exist"); ++ } ++ ++ return true; ++ } ++ ++ /** ++ * Expect file with a supplied extension to not exist. ++ * ++ * @param string $extension ++ * @param string $prefix ++ * @return bool ++ */ ++ public function expectNoFile(string $extension, $prefix = null) ++ { ++ $filePath = $this->getPrefixedFile($extension, $prefix); ++ if (file_exists($filePath)) { ++ return $this->error("The file $filePath exists"); ++ } ++ ++ return true; ++ } ++ ++ /** ++ * Expect message to be written to FastCGI error stream. ++ * ++ * @param string $message ++ * @param int $limit ++ * @param int $repeat ++ */ ++ public function expectFastCGIErrorMessage( ++ string $message, ++ int $limit = 1024, ++ int $repeat = 0 ++ ) { ++ $this->logTool->setExpectedMessage($message, $limit, $repeat); ++ $this->logTool->checkTruncatedMessage($this->response->getErrorData()); ++ } ++ ++ /** ++ * Expect starting lines to be logged. ++ */ ++ public function expectLogStartNotices() ++ { ++ $this->logTool->expectStartingLines($this->getLogLines(2)); ++ } ++ ++ /** ++ * Expect terminating lines to be logged. ++ */ ++ public function expectLogTerminatingNotices() ++ { ++ $this->logTool->expectTerminatorLines($this->getLogLines(-1)); ++ } ++ ++ /** ++ * Expect log message that can span multiple lines. ++ * ++ * @param string $message ++ * @param int $limit ++ * @param int $repeat ++ * @param bool $decorated ++ * @param bool $wrapped ++ */ ++ public function expectLogMessage( ++ string $message, ++ int $limit = 1024, ++ int $repeat = 0, ++ bool $decorated = true, ++ bool $wrapped = true ++ ) { ++ $this->logTool->setExpectedMessage($message, $limit, $repeat); ++ if ($wrapped) { ++ $logLines = $this->getLogLines(-1, true); ++ $this->logTool->checkWrappedMessage($logLines, true, $decorated); ++ } else { ++ $logLines = $this->getLogLines(1, true); ++ $this->logTool->checkTruncatedMessage($logLines[0] ?? ''); ++ } ++ if ($this->debug) { ++ $this->message("-------------- LOG LINES: -------------"); ++ var_dump($logLines); ++ $this->message("---------------------------------------\n"); ++ } ++ } ++ ++ /** ++ * Expect a single log line. ++ * ++ * @param string $message ++ * @return bool ++ */ ++ public function expectLogLine(string $message) ++ { ++ $messageLen = strlen($message); ++ $limit = $messageLen > 1024 ? $messageLen + 16 : 1024; ++ $this->logTool->setExpectedMessage($message, $limit); ++ $logLines = $this->getLogLines(1, true); ++ if ($this->debug) { ++ $this->message("LOG LINE: " . ($logLines[0] ?? '')); ++ } ++ ++ return $this->logTool->checkWrappedMessage($logLines, false); ++ } ++ ++ /** ++ * Expect a log debug message. ++ * ++ * @param string $message ++ * @param string|null $pool ++ * @return bool ++ */ ++ public function expectLogDebug(string $message, $pool = null) ++ { ++ return $this->logTool->expectDebug($this->getLastLogLine(), $message, $pool); ++ } ++ ++ /** ++ * Expect a log notice. ++ * ++ * @param string $message ++ * @param string|null $pool ++ * @return bool ++ */ ++ public function expectLogNotice(string $message, $pool = null) ++ { ++ return $this->logTool->expectNotice($this->getLastLogLine(), $message, $pool); ++ } ++ ++ /** ++ * Expect a log warning. ++ * ++ * @param string $message ++ * @param string|null $pool ++ * @return bool ++ */ ++ public function expectLogWarning(string $message, $pool = null) ++ { ++ return $this->logTool->expectWarning($this->getLastLogLine(), $message, $pool); ++ } ++ ++ /** ++ * Expect a log error. ++ * ++ * @param string $message ++ * @param string|null $pool ++ * @return bool ++ */ ++ public function expectLogError(string $message, $pool = null) ++ { ++ return $this->logTool->expectError($this->getLastLogLine(), $message, $pool); ++ } ++ ++ /** ++ * Expect a log alert. ++ * ++ * @param string $message ++ * @param string|null $pool ++ * @return bool ++ */ ++ public function expectLogAlert(string $message, $pool = null) ++ { ++ return $this->logTool->expectAlert($this->getLastLogLine(), $message, $pool); ++ } ++ ++ /** ++ * Expect no log lines to be logged. ++ * ++ * @return bool ++ */ ++ public function expectNoLogMessages() ++ { ++ $logLines = $this->getLogLines(-1, true); ++ if (!empty($logLines)) { ++ return $this->error( ++ "Expected no log lines but following lines logged:\n" . implode("\n", $logLines) ++ ); ++ } ++ ++ return true; ++ } ++ ++ /** ++ * Print content of access log. ++ */ ++ public function printAccessLog() ++ { ++ $accessLog = $this->getFile('acc.log'); ++ if (is_file($accessLog)) { ++ print file_get_contents($accessLog); ++ } ++ } ++} +-- +2.54.0 + +From 3ef1cb1937145019ef836cba3a5736a37384c752 Mon Sep 17 00:00:00 2001 +From: Remi Collet <remi@remirepo.net> +Date: Thu, 7 May 2026 09:01:35 +0200 +Subject: [PATCH 6/6] NEWS from 8.2.31 + +(cherry picked from commit 7dff10e9a31d469fcd436e10b06f8b2bf2758a68) +(cherry picked from commit 1cbf0c27044bd54fb77de8a6bf993a7ab53892a4) +(cherry picked from commit 6b9f5d1673522bb3cf5d77889919084024565c7f) +(cherry picked from commit 5be222339cd6d299aa9170e6fa9edd51a5c42f39) +(cherry picked from commit 8884e113e8351693eb4b5f1c58485ad0e4508d3a) +(cherry picked from commit 5cf6ff5fcde53a1a941fea374b483e9ff89a9f9f) +--- + NEWS | 18 ++++++++++++++++++ + 1 file changed, 18 insertions(+) + +diff --git a/NEWS b/NEWS +index 7096b787738..b5014af12e4 100644 +--- a/NEWS ++++ b/NEWS +@@ -1,6 +1,24 @@ + PHP NEWS + ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| + ++Backported from 8.2.31 ++ ++- FPM: ++ . Fixed GHSA-7qg2-v9fj-4mwv (XSS within status endpoint). (CVE-2026-6735) ++ (Jakub Zelenka) ++ ++- SOAP: ++ . Fixed GHSA-85c2-q967-79q5 (Stale SOAP_GLOBAL(ref_map) pointer with Apache ++ Map). (CVE-2026-6722) (ilutov) ++ . Fixed GHSA-m33r-qmcv-p97q (Use-after-free after header parsing failure with ++ SOAP_PERSISTENCE_SESSION). (CVE-2026-7261) (ilutov) ++ . Fixed GHSA-hmxp-6pc4-f3vv (Broken Apache map value NULL check). ++ (CVE-2026-7262) (ilutov) ++ ++- Standard: ++ . Fixed GHSA-96wq-48vp-hh57 (Signed integer overflow of char array offset). ++ (CVE-2026-7568) (TimWolla) ++ + Backported from 8.1.31 + + - CLI: +-- +2.54.0 + diff --git a/php-cve-2026-7261.patch b/php-cve-2026-7261.patch new file mode 100644 index 0000000..1d4e8ae --- /dev/null +++ b/php-cve-2026-7261.patch @@ -0,0 +1,122 @@ +From f91ab4e04bc2f254ea1e49e1b76ff55adbbe3892 Mon Sep 17 00:00:00 2001 +From: Ilija Tovilo <ilija.tovilo@me.com> +Date: Sun, 3 May 2026 19:57:16 +0200 +Subject: [PATCH 2/6] GHSA-m33r-qmcv-p97q: [soap] Fix use-after-free after + header parsing failure with SOAP_PERSISTENCE_SESSION + +Fixes GHSA-m33r-qmcv-p97q +Fixes CVE-2026-7261 + +(cherry picked from commit db2a7f9348fd5dda5fd162061786a664c417bf5b) +(cherry picked from commit 5dd8dd8493d49bb6fcd810a6e9d2ffb6fdc15714) +(cherry picked from commit 63cf032e9675d7d2bbc007c8c787597187a7567b) +(cherry picked from commit dd14d36e31dd99b7589f917924840fe4f46ca022) +(cherry picked from commit 7b354983a33c314b76c594c9c5b790e3b073dcf1) + +adapt test for 7.2 + +(cherry picked from commit f91bcf961ac15eacabf33f86f62c17dbec4a39ab) +(cherry picked from commit ab6fa685773d4efea4de2df4956c97ffd65637e2) +--- + ext/soap/soap.c | 12 ++++- + ext/soap/tests/GHSA-m33r-qmcv-p97q.phpt | 60 +++++++++++++++++++++++++ + 2 files changed, 70 insertions(+), 2 deletions(-) + create mode 100644 ext/soap/tests/GHSA-m33r-qmcv-p97q.phpt + +diff --git a/ext/soap/soap.c b/ext/soap/soap.c +index 62b119fb2bf..e436c278760 100644 +--- a/ext/soap/soap.c ++++ b/ext/soap/soap.c +@@ -1839,13 +1839,21 @@ PHP_METHOD(SoapServer, handle) + php_output_discard(); + soap_server_fault_ex(function, &h->retval, h); + efree(fn_name); +- if (service->type == SOAP_CLASS && soap_obj) {zval_ptr_dtor(soap_obj);} ++ if (service->type == SOAP_CLASS && soap_obj) { ++ if (service->soap_class.persistence != SOAP_PERSISTENCE_SESSION) { ++ zval_ptr_dtor(soap_obj); ++ } ++ } + goto fail; + } else if (EG(exception)) { + php_output_discard(); + _soap_server_exception(service, function, getThis()); + efree(fn_name); +- if (service->type == SOAP_CLASS && soap_obj) {zval_ptr_dtor(soap_obj);} ++ if (service->type == SOAP_CLASS && soap_obj) { ++ if (service->soap_class.persistence != SOAP_PERSISTENCE_SESSION) { ++ zval_ptr_dtor(soap_obj); ++ } ++ } + goto fail; + } + } else if (h->mustUnderstand) { +diff --git a/ext/soap/tests/GHSA-m33r-qmcv-p97q.phpt b/ext/soap/tests/GHSA-m33r-qmcv-p97q.phpt +new file mode 100644 +index 00000000000..6e4e9e75fb6 +--- /dev/null ++++ b/ext/soap/tests/GHSA-m33r-qmcv-p97q.phpt +@@ -0,0 +1,60 @@ ++--TEST-- ++GHSA-m33r-qmcv-p97q: Use-after-free after header parsing failure with SOAP_PERSISTENCE_SESSION ++--CREDITS-- ++Ilia Alshanetsky (iliaal) ++--EXTENSIONS-- ++soap ++session ++--FILE-- ++<?php ++ ++class Handler { ++ public function return() { ++ return new SoapFault('Server', 'denied'); ++ } ++ public function throw() { ++ throw new SoapFault('Server', 'denied'); ++ } ++ public function hello() { ++ return 'ok'; ++ } ++} ++ ++session_start(); ++ ++$srv = new SoapServer(null, ['uri' => 'urn:a']); ++$srv->setClass(Handler::class); ++$srv->setPersistence(SOAP_PERSISTENCE_SESSION); ++ ++$x = <<<XML ++<?xml version="1.0" encoding="UTF-8"?> ++<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:a="urn:a"> ++ <soap:Header> ++ <a:return/> ++ </soap:Header> ++ <soap:Body> ++ <a:hello/> ++ </soap:Body> ++</soap:Envelope> ++XML; ++$srv->handle($x); ++ ++$x = <<<XML ++<?xml version="1.0" encoding="UTF-8"?> ++<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:a="urn:a"> ++ <soap:Header> ++ <a:throw/> ++ </soap:Header> ++ <soap:Body> ++ <a:hello/> ++ </soap:Body> ++</soap:Envelope> ++XML; ++$srv->handle($x); ++ ++?> ++--EXPECT-- ++<?xml version="1.0" encoding="UTF-8"?> ++<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Body><SOAP-ENV:Fault><faultcode>SOAP-ENV:Server</faultcode><faultstring>denied</faultstring></SOAP-ENV:Fault></SOAP-ENV:Body></SOAP-ENV:Envelope> ++<?xml version="1.0" encoding="UTF-8"?> ++<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Body><SOAP-ENV:Fault><faultcode>SOAP-ENV:Server</faultcode><faultstring>denied</faultstring></SOAP-ENV:Fault></SOAP-ENV:Body></SOAP-ENV:Envelope> +-- +2.54.0 + diff --git a/php-cve-2026-7262.patch b/php-cve-2026-7262.patch new file mode 100644 index 0000000..625989f --- /dev/null +++ b/php-cve-2026-7262.patch @@ -0,0 +1,83 @@ +From b1bc3b191eb9ff6ca90f90572ba8fac016163fe9 Mon Sep 17 00:00:00 2001 +From: Ilija Tovilo <ilija.tovilo@me.com> +Date: Sat, 25 Apr 2026 00:44:37 +0200 +Subject: [PATCH 3/6] GHSA-hmxp-6pc4-f3vv: [soap] Fix broken Apache map value + NULL check + +Fixes GHSA-hmxp-6pc4-f3vv +Fixes CVE-2026-7262 + +(cherry picked from commit 79551ab8b1a97760c739e372f9bc359619f3554d) +(cherry picked from commit aed3e63e282235b32a07ca28cc20728eedfcfec3) +(cherry picked from commit 8c897384b867a573d52a04b455fe2da30671d0ea) +(cherry picked from commit b41a11a9786cc5b6b343b47c37ad8c1fdc2dbf33) +(cherry picked from commit 254773b5b1d0ef25409c35e74b87c5ef93459115) +(cherry picked from commit c21561700dcfc3304322845c2d3da028c3c73345) +(cherry picked from commit 16c2b25d363d73d72a3139e747cc9d5c8d5bef2b) +--- + ext/soap/php_encoding.c | 2 +- + ext/soap/tests/GHSA-hmxp-6pc4-f3vv.phpt | 39 +++++++++++++++++++++++++ + 2 files changed, 40 insertions(+), 1 deletion(-) + create mode 100644 ext/soap/tests/GHSA-hmxp-6pc4-f3vv.phpt + +diff --git a/ext/soap/php_encoding.c b/ext/soap/php_encoding.c +index 40fba95980a..d88dba76228 100644 +--- a/ext/soap/php_encoding.c ++++ b/ext/soap/php_encoding.c +@@ -2757,7 +2757,7 @@ static zval *to_zval_map(zval *ret, encodeTypePtr type, xmlNodePtr data) + } + + xmlValue = get_node(item->children, "value"); +- if (!xmlKey) { ++ if (!xmlValue) { + soap_error0(E_ERROR, "Encoding: Can't decode apache map, missing value"); + } + +diff --git a/ext/soap/tests/GHSA-hmxp-6pc4-f3vv.phpt b/ext/soap/tests/GHSA-hmxp-6pc4-f3vv.phpt +new file mode 100644 +index 00000000000..e46ab2e4607 +--- /dev/null ++++ b/ext/soap/tests/GHSA-hmxp-6pc4-f3vv.phpt +@@ -0,0 +1,39 @@ ++--TEST-- ++GHSA-hmxp-6pc4-f3vv: Null pointer dereference on missing Apache map value ++--CREDITS-- ++Ilia Alshanetsky (iliaal) ++--EXTENSIONS-- ++soap ++--FILE-- ++<?php ++ ++$request = <<<XML ++<?xml version="1.0" encoding="UTF-8"?> ++<soap:Envelope ++ xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" ++ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ++ xmlns:xsd="http://www.w3.org/2001/XMLSchema" ++ xmlns:apache="http://xml.apache.org/xml-soap"> ++ ++ <soap:Body> ++ <test> ++ <map xsi:type="apache:Map"> ++ <item><key>hello</key></item> ++ </map> ++ </test> ++ </soap:Body> ++</soap:Envelope> ++XML; ++ ++$server = new SoapServer(null, [ ++ 'uri' => 'urn:test', ++ 'typemap' => [['type_name' => 'anything']], ++]); ++$server->addFunction('test'); ++function test($m) { return null; } ++$server->handle($request); ++ ++?> ++--EXPECT-- ++<?xml version="1.0" encoding="UTF-8"?> ++<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Body><SOAP-ENV:Fault><faultcode>SOAP-ENV:Server</faultcode><faultstring>SOAP-ERROR: Encoding: Can't decode apache map, missing value</faultstring></SOAP-ENV:Fault></SOAP-ENV:Body></SOAP-ENV:Envelope> +-- +2.54.0 + diff --git a/php-cve-2026-7568.patch b/php-cve-2026-7568.patch new file mode 100644 index 0000000..f3e7186 --- /dev/null +++ b/php-cve-2026-7568.patch @@ -0,0 +1,90 @@ +From 99eec43bb407d42855eaa9ff6af64df1ee2c20dc Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= <tim@tideways-gmbh.com> +Date: Sun, 3 May 2026 20:02:57 +0200 +Subject: [PATCH 4/6] GHSA-96wq-48vp-hh57: [metaphone] Fix signed integer + overflow of char array offset + +Fixes GHSA-96wq-48vp-hh57 +Fixes CVE-2026-7568 + +(cherry picked from commit 47def8ce1db1fdbffcfc1f5bb11877a0e22d4b32) +(cherry picked from commit e4fc187a011d91f26178f6dfbccdb07041b99153) +(cherry picked from commit 53de456406a6db5a8bcded8a4b242789ae5b2690) +(cherry picked from commit 909c2acc64d72bd57123b30e711c02aef0c08d14) + +[skip ci] Adjust credits for GHSA-96wq-48vp-hh57.phpt + +As requested by the reporter. + +(cherry picked from commit fee84dd8c7699e4e7f9b2e864a393ee5a372f974) +(cherry picked from commit 101e93900888ef43d42ec0e33866bca3824f51a8) +(cherry picked from commit 41134d0746a524d7265b67d3d8d0fd433fd7479a) +(cherry picked from commit b40b656c0fe8080f9cd097bf77b7a3681ea3e7a0) +(cherry picked from commit 9e4b7c856c57deda7b7887da7978328ec8b57187) +(cherry picked from commit b7702525bc4a540eb36f392a13461971a1bac31a) +(cherry picked from commit b6affc4bc51768aec7ad8737f4486597939b0bd4) +--- + ext/standard/metaphone.c | 8 ++++---- + ext/standard/tests/GHSA-96wq-48vp-hh57.phpt | 22 +++++++++++++++++++++ + 2 files changed, 26 insertions(+), 4 deletions(-) + create mode 100644 ext/standard/tests/GHSA-96wq-48vp-hh57.phpt + +diff --git a/ext/standard/metaphone.c b/ext/standard/metaphone.c +index 9bf67bbda89..23ebf144e76 100644 +--- a/ext/standard/metaphone.c ++++ b/ext/standard/metaphone.c +@@ -122,10 +122,10 @@ char _codes[26] = + + /* Allows us to safely look ahead an arbitrary # of letters */ + /* I probably could have just used strlen... */ +-static char Lookahead(char *word, int how_far) ++static char Lookahead(char *word, size_t how_far) + { + char letter_ahead = '\0'; /* null by default */ +- int idx; ++ size_t idx; + for (idx = 0; word[idx] != '\0' && idx < how_far; idx++); + /* Edge forward in the string... */ + +@@ -167,8 +167,8 @@ static char Lookahead(char *word, int how_far) + */ + static int metaphone(unsigned char *word, size_t word_len, zend_long max_phonemes, zend_string **phoned_word, int traditional) + { +- int w_idx = 0; /* point in the phonization we're at. */ +- int p_idx = 0; /* end of the phoned phrase */ ++ size_t w_idx = 0; /* point in the phonization we're at. */ ++ size_t p_idx = 0; /* end of the phoned phrase */ + size_t max_buffer_len = 0; /* maximum length of the destination buffer */ + + /*-- Parameter checks --*/ +diff --git a/ext/standard/tests/GHSA-96wq-48vp-hh57.phpt b/ext/standard/tests/GHSA-96wq-48vp-hh57.phpt +new file mode 100644 +index 00000000000..cf9a40062f8 +--- /dev/null ++++ b/ext/standard/tests/GHSA-96wq-48vp-hh57.phpt +@@ -0,0 +1,22 @@ ++--TEST-- ++GHSA-96wq-48vp-hh57: signed integer overflow of char array offset ++--CREDITS-- ++Aleksey Solovev (Positive Technologies) ++--INI-- ++memory_limit=3G ++--SKIPIF-- ++<?php ++if (!getenv('RUN_RESOURCE_HEAVY_TESTS')) die('skip resource-heavy test'); ++if (getenv('SKIP_SLOW_TESTS')) die('skip slow test'); ++if (PHP_INT_SIZE != 8) echo 'skip 64-bit only'; ++?> ++--FILE-- ++<?php ++ ++$str = str_repeat('0', 2 * (1024 ** 3) - 2) . 'AE'; ++metaphone($str, 1); ++ ++?> ++===DONE=== ++--EXPECT-- ++===DONE=== +-- +2.54.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..98ad60e --- /dev/null +++ b/php-ghsa-4w77-75f9-2c8w.patch @@ -0,0 +1,143 @@ +From 81f2819ec08c6c7ff1f4e2caccb51719ace6a27d 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 8/9] Fix GHSA-4w77-75f9-2c8w + +(cherry picked from commit 7dd336ae838bbf2c62dc47e3c900d657d3534c02) +(cherry picked from commit 462092a48aa0dbad24d9fa8a4a9d418faa14d309) +(cherry picked from commit 56488a8a4ec68e58eecc9e78dd75e41adf56984c) +(cherry picked from commit 6b8357c22f83a93104c2682d5cba9104c8de636d) +(cherry picked from commit b7c951d47acae54aab5ce896b8ec151d661c8fd0) +(cherry picked from commit abd3bf9eb5a1c42fc24b7a0296b09d93ed7d6730) +--- + 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 4c1d443d61..bc488a0d3f 100644 +--- a/sapi/cli/php_cli_server.c ++++ b/sapi/cli/php_cli_server.c +@@ -1815,8 +1815,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; +@@ -1824,9 +1822,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 0000000000..44667e8389 +--- /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 861b62921190c2c29205d6029d33a606b7a47831 Mon Sep 17 00:00:00 2001 +From: Remi Collet <remi@remirepo.net> +Date: Fri, 22 Nov 2024 08:58:10 +0100 +Subject: [PATCH 9/9] NEWS for 8.1.31 backports + +(cherry picked from commit 22bdb43da0ecd6e72d63b63aa6c1f3a25d1bca3a) +(cherry picked from commit d8d682d3d6a4d027771806c8fc77128cae078d29) +(cherry picked from commit b97a41a47f77df92771b3c01fbf7cf445c0e7a1b) +(cherry picked from commit 46f3d442aae8d8caca33a4d4ff9c9470568aee80) +(cherry picked from commit 49783ab65131f0af188ea41a74db4af56a41c323) +--- + NEWS | 24 ++++++++++++++++++++++++ + 1 file changed, 24 insertions(+) + +diff --git a/NEWS b/NEWS +index cf90002253..7096b78773 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 + diff --git a/php-net-snmp.patch b/php-net-snmp.patch new file mode 100644 index 0000000..e9f819e --- /dev/null +++ b/php-net-snmp.patch @@ -0,0 +1,38 @@ +Backported from 8.0 for 7.0 by Remi + + +From f9fd3595ecb36c8dc6add0515782a18f15216d77 Mon Sep 17 00:00:00 2001 +From: Remi Collet <remi@remirepo.net> +Date: Thu, 27 May 2021 14:20:07 +0200 +Subject: [PATCH] Fix snmp build without DES + +--- + ext/snmp/snmp.c | 16 ++++++++++++++-- + 1 file changed, 14 insertions(+), 2 deletions(-) + +diff --git a/ext/snmp/snmp.c b/ext/snmp/snmp.c +index 35d19c8738828..d31995827880d 100644 +--- a/ext/snmp/snmp.c ++++ b/ext/snmp/snmp.c +@@ -1266,15 +1266,19 @@ static int netsnmp_session_set_auth_prot + Set the security protocol in the snmpv3 session */ + static int netsnmp_session_set_sec_protocol(struct snmp_session *s, char *prot) + { ++#ifndef NETSNMP_DISABLE_DES + if (!strcasecmp(prot, "DES")) { + s->securityPrivProto = usmDESPrivProtocol; + s->securityPrivProtoLen = USM_PRIV_PROTO_DES_LEN; ++ } else ++#endif + #ifdef HAVE_AES +- } else if (!strcasecmp(prot, "AES128") || !strcasecmp(prot, "AES")) { ++ if (!strcasecmp(prot, "AES128") || !strcasecmp(prot, "AES")) { + s->securityPrivProto = usmAESPrivProtocol; + s->securityPrivProtoLen = USM_PRIV_PROTO_AES_LEN; ++ } else + #endif +- } else { ++ { + php_error_docref(NULL, E_WARNING, "Unknown security protocol '%s'", prot); + return (-1); + } @@ -55,21 +55,16 @@ %global mysql_sock %(mysql_config --socket 2>/dev/null || echo /var/lib/mysql/mysql.sock) -%if 0%{?rhel} == 6 -%ifarch x86_64 -%global oraclever 18.5 -%else -%global oraclever 18.3 -%endif -%global oraclelib 18.1 - -%else -%ifarch x86_64 -%global oraclever 19.8 -%else -%global oraclever 19.6 -%endif +%ifarch aarch64 +%global oraclever 19.24 +%global oraclemax 20 %global oraclelib 19.1 +%global oracledir 19.24 +%else +%global oraclever 23.26.2 +%global oraclemax 24 +%global oraclelib 23.1 +%global oracledir 23 %endif # Build for LiteSpeed Web Server (LSAPI) @@ -86,12 +81,7 @@ # Optional components; pass "--with mssql" etc to rpmbuild. %global with_oci8 %{?_with_oci8:1}%{!?_with_oci8:0} %global with_imap 1 -# until firebird available in EPEL -%if 0%{?rhel} == 8 -%global with_interbase 0 -%else %global with_interbase 1 -%endif %global with_mcrypt 1 %global with_freetds 1 %global with_tidy 1 @@ -146,7 +136,7 @@ Summary: PHP scripting language for creating dynamic web sites Name: %{?scl_prefix}php Version: %{upver}%{?rcver:~%{rcver}} -Release: 22%{?dist} +Release: 46%{?dist} # All files licensed under PHP version 3.01, except # Zend is licensed under Zend # TSRM is licensed under BSD @@ -188,6 +178,8 @@ Patch7: php-5.3.0-recode.patch Patch8: php-7.0.2-libdb.patch Patch9: php-7.0.7-curl.patch Patch10: php-7.0.31-icu62.patch +# backported from 8.0 +Patch11: php-net-snmp.patch # Functional changes Patch40: php-7.0.17-dlopen.patch @@ -256,6 +248,43 @@ Patch242: php-bug78875.patch Patch243: php-bug78876.patch Patch244: php-bug79797.patch Patch245: php-bug79877.patch +Patch246: php-bug79699.patch +Patch247: php-bug77423.patch +Patch248: php-bug80672.patch +Patch249: php-bug80710.patch +Patch250: php-bug81122.patch +Patch251: php-bug76450.patch +Patch252: php-bug81211.patch +Patch253: php-bug81026.patch +Patch254: php-bug79971.patch +Patch255: php-bug81719.patch +Patch256: php-bug81720.patch +Patch257: php-bug81727.patch +Patch258: php-bug81726.patch +Patch259: php-bug81740.patch +Patch260: php-bug81744.patch +Patch261: php-bug81746.patch +Patch262: php-cve-2023-0662.patch +Patch263: php-cve-2023-3247.patch +Patch264: php-cve-2023-3823.patch +Patch265: php-cve-2023-3824.patch +Patch266: php-cve-2024-2756.patch +Patch267: php-cve-2024-3096.patch +Patch268: php-cve-2024-5458.patch +Patch269: php-cve-2024-8925.patch +Patch270: php-cve-2024-8926.patch +Patch271: php-cve-2024-8927.patch +Patch272: php-cve-2024-11236.patch +Patch273: php-cve-2024-11234.patch +Patch274: php-cve-2024-8932.patch +Patch275: php-cve-2024-11233.patch +Patch276: php-ghsa-4w77-75f9-2c8w.patch +# from 8.2.31 +Patch277: php-cve-2026-6722.patch +Patch278: php-cve-2026-7261.patch +Patch279: php-cve-2026-7262.patch +Patch280: php-cve-2026-6735.patch +Patch281: php-cve-2026-7568.patch # Fixes for tests (300+) # Factory is droped from system tzdata @@ -290,6 +319,7 @@ BuildRequires: bzip2 BuildRequires: perl BuildRequires: autoconf BuildRequires: automake +BuildRequires: make BuildRequires: gcc BuildRequires: gcc-c++ BuildRequires: libtool @@ -363,7 +393,6 @@ The %{?scl_prefix}php-dbg package contains the interactive PHP debugger. Group: Development/Languages Summary: PHP FastCGI Process Manager BuildRequires: libacl-devel -Requires(pre): %{_root_sbindir}/useradd Requires: %{?scl_prefix}php-common%{?_isa} = %{version}-%{release} %if %{with_systemd} BuildRequires: systemd-devel @@ -387,6 +416,8 @@ Requires(pre): httpd-filesystem # For php.conf in /etc/httpd/conf.d # and version 2.4.10 for proxy support in SetHandler Requires: httpd-filesystem >= 2.4.10 +%else +Requires(pre): %{_root_sbindir}/useradd %endif %description fpm @@ -473,6 +504,7 @@ Requires: %{?scl_prefix}php-cli%{?_isa} = %{version}-%{release} # always needed to build extension Requires: autoconf Requires: automake +Requires: make Requires: gcc Requires: gcc-c++ Requires: libtool @@ -676,15 +708,20 @@ Summary: A module for PHP applications that use OCI8 databases Group: Development/Languages # All files licensed under PHP version 3.01 License: PHP -BuildRequires: oracle-instantclient-devel >= %{oraclever} +%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, %{?scl_prefix}php-pdo_oci%{?_isa} Obsoletes: %{?scl_prefix}php-pecl-oci8 < %{oci8ver} Conflicts: %{?scl_prefix}php-pecl-oci8 >= %{oci8ver} Provides: %{?scl_prefix}php-pecl(oci8) = %{oci8ver}, %{?scl_prefix}php-pecl(oci8)%{?_isa} = %{oci8ver} -# Should requires libclntsh.so.12.1, but it's not provided by Oracle RPM. -AutoReq: 0 %description oci8 The %{?scl_prefix}php-oci8 packages provides the OCI8 extension version %{oci8ver} @@ -694,13 +731,9 @@ The extension is linked with Oracle client libraries %{oraclever} (Oracle Instant Client). For details, see Oracle's note "Oracle Client / Server Interoperability Support" (ID 207303.1). -You must install libclntsh.so.%{oraclelib} to use this package, provided -in the database installation, or in the free Oracle Instant Client -available from Oracle. - -Notice: -- %{?scl_prefix}php-oci8 provides oci8 and pdo_oci extensions from php sources. -- %{?scl_prefix}php-pecl-oci8 only provides oci8 extension. +You must install libclntsh.so.%{oraclelib} to use this package, +provided by Oracle Instant Client RPM available from Oracle on: +https://www.oracle.com/database/technologies/instant-client/downloads.html Documentation is at http://php.net/oci8 and http://php.net/pdo_oci %endif @@ -780,12 +813,7 @@ Requires: %{?scl_prefix}php-common%{?_isa} = %{version}-%{release} BuildRequires: libjpeg-devel, libpng-devel, freetype-devel BuildRequires: libXpm-devel %if %{with_libgd} -BuildRequires: gd-devel >= 2.1.1 -%if 0%{?fedora} <= 19 && 0%{?rhel} <= 7 -Requires: gd-last%{?_isa} >= 2.1.1 -%else -Requires: gd%{?_isa} >= 2.1.1 -%endif +BuildRequires: gd-devel >= 2.3.3 %else BuildRequires: libwebp-devel %endif @@ -906,8 +934,12 @@ Group: System Environment/Libraries # All files licensed under PHP version 3.01 License: PHP Requires: %{?scl_prefix}php-common%{?_isa} = %{version}-%{release} -# Upstream requires 4.0, we require 50 to ensure use of libicu-last -BuildRequires: libicu-devel >= 50 +# Upstream requires 4.0, we require 69.1 to ensure use of libicu69 +%if 0%{?rhel} +BuildRequires: libicu-devel = 69.1 +%else +BuildRequires: libicu-devel +%endif %description intl The %{?scl_prefix}php-intl package contains a dynamic shared object that will add @@ -965,104 +997,139 @@ support for JavaScript Object Notation (JSON) to PHP. %setup -q -n php-%{upver}%{?rcver} %endif -%patch1 -p1 -b .mpmcheck -%patch2 -p1 -b .fb_config -%patch5 -p1 -b .includedir -%patch6 -p1 -b .embed -%patch7 -p1 -b .recode -%patch8 -p1 -b .libdb +%patch -P1 -p1 -b .mpmcheck +%patch -P2 -p1 -b .fb_config +%patch -P5 -p1 -b .includedir +%patch -P6 -p1 -b .embed +%patch -P7 -p1 -b .recode +%patch -P8 -p1 -b .libdb %if 0%{?rhel} -%patch9 -p1 -b .curltls +%patch -P9 -p1 -b .curltls %endif %if 0%{?fedora} >= 29 || 0%{?rhel} >= 7 -%patch10 -p1 -b .icu62 +%patch -P10 -p1 -b .icu62 %endif +%patch -P11 -p1 -b .nodes -%patch40 -p1 -b .dlopen +%patch -P40 -p1 -b .dlopen %if 0%{?fedora} >= 28 || 0%{?rhel} >= 6 -%patch42 -p1 -b .systzdata +%patch -P42 -p1 -b .systzdata %endif -%patch43 -p1 -b .headers +%patch -P43 -p1 -b .headers sed -e 's/php-devel/%{?scl_prefix}php-devel/' -i scripts/phpize.in %if 0%{?fedora} >= 18 || 0%{?rhel} >= 7 -%patch45 -p1 -b .ldap_r +%patch -P45 -p1 -b .ldap_r %endif -%patch46 -p1 -b .fixheader -%patch47 -p1 -b .phpinfo +%patch -P46 -p1 -b .fixheader +%patch -P47 -p1 -b .phpinfo -%patch91 -p1 -b .remi-oci8 +%patch -P91 -p1 -b .remi-oci8 # upstream patches -%patch100 -p1 -b .up1 -%patch101 -p1 -b .up2 -%patch103 -p1 -b .bug76846 +%patch -P100 -p1 -b .up1 +%patch -P101 -p1 -b .up2 +%patch -P103 -p1 -b .bug76846 # security patches -%patch200 -p1 -b .bug77242 -%patch201 -p1 -b .bug77247 -%patch202 -p1 -b .bug77370 -%patch203 -p1 -b .bug77371 -%patch204 -p1 -b .bug77380 -%patch205 -p1 -b .bug77381 -%patch206 -p1 -b .bug77369 -%patch207 -p1 -b .bug77418 -%patch208 -p1 -b .bug77396 -%patch209 -p1 -b .bug77431 -%patch210 -p1 -b .bug77540 -%patch211 -p1 -b .bug77563 -%patch212 -p1 -b .bug77586 -%patch213 -p1 -b .bug77630 -%patch214 -p1 -b .backport -%patch215 -p1 -b .sqlite3.defensive -%patch216 -p1 -b .bug77753 -%patch217 -p1 -b .bug77831 -%patch218 -p1 -b .bug77950 -%patch219 -p1 -b .bug78069 -%patch220 -p1 -b .bug77988 -%patch221 -p1 -b .bug77967 -%patch222 -p1 -b .bug78222 -%patch223 -p1 -b .bug78256 -%patch224 -p1 -b .bug77919 -%patch225 -p1 -b .bug75457 -%patch226 -p1 -b .bug78380 -%patch227 -p1 -b .bug78599 -%patch228 -p1 -b .bug78878 -%patch229 -p1 -b .bug78862 -%patch230 -p1 -b .bug78863 -%patch231 -p1 -b .bug78793 -%patch232 -p1 -b .bug78910 -%patch233 -p1 -b .bug79099 -%patch234 -p1 -b .bug79037 -%patch235 -p1 -b .bug77569 -%patch236 -p1 -b .bug79221 -%patch237 -p1 -b .bug79082 -%patch238 -p1 -b .bug79282 -%patch239 -p1 -b .bug79329 -%patch240 -p1 -b .bug79330 -%patch241 -p1 -b .bug79465 -%patch242 -p1 -b .bug78875 -%patch243 -p1 -b .bug78876 -%patch244 -p1 -b .bug79797 -%patch245 -p1 -b .bug79877 -: --------------------------- -#exit 1 +%patch -P200 -p1 -b .bug77242 +%patch -P201 -p1 -b .bug77247 +%patch -P202 -p1 -b .bug77370 +%patch -P203 -p1 -b .bug77371 +%patch -P204 -p1 -b .bug77380 +%patch -P205 -p1 -b .bug77381 +%patch -P206 -p1 -b .bug77369 +%patch -P207 -p1 -b .bug77418 +%patch -P208 -p1 -b .bug77396 +%patch -P209 -p1 -b .bug77431 +%patch -P210 -p1 -b .bug77540 +%patch -P211 -p1 -b .bug77563 +%patch -P212 -p1 -b .bug77586 +%patch -P213 -p1 -b .bug77630 +%patch -P214 -p1 -b .backport +%patch -P215 -p1 -b .sqlite3.defensive +%patch -P216 -p1 -b .bug77753 +%patch -P217 -p1 -b .bug77831 +%patch -P218 -p1 -b .bug77950 +%patch -P219 -p1 -b .bug78069 +%patch -P220 -p1 -b .bug77988 +%patch -P221 -p1 -b .bug77967 +%patch -P222 -p1 -b .bug78222 +%patch -P223 -p1 -b .bug78256 +%patch -P224 -p1 -b .bug77919 +%patch -P225 -p1 -b .bug75457 +%patch -P226 -p1 -b .bug78380 +%patch -P227 -p1 -b .bug78599 +%patch -P228 -p1 -b .bug78878 +%patch -P229 -p1 -b .bug78862 +%patch -P230 -p1 -b .bug78863 +%patch -P231 -p1 -b .bug78793 +%patch -P232 -p1 -b .bug78910 +%patch -P233 -p1 -b .bug79099 +%patch -P234 -p1 -b .bug79037 +%patch -P235 -p1 -b .bug77569 +%patch -P236 -p1 -b .bug79221 +%patch -P237 -p1 -b .bug79082 +%patch -P238 -p1 -b .bug79282 +%patch -P239 -p1 -b .bug79329 +%patch -P240 -p1 -b .bug79330 +%patch -P241 -p1 -b .bug79465 +%patch -P242 -p1 -b .bug78875 +%patch -P243 -p1 -b .bug78876 +%patch -P244 -p1 -b .bug79797 +%patch -P245 -p1 -b .bug79877 +%patch -P246 -p1 -b .bug79699 +%patch -P247 -p1 -b .bug77423 +%patch -P248 -p1 -b .bug80672 +%patch -P249 -p1 -b .bug80710 +%patch -P250 -p1 -b .bug81122 +%patch -P251 -p1 -b .bug76450 +%patch -P252 -p1 -b .bug81211 +%patch -P253 -p1 -b .bug81026 +%patch -P254 -p1 -b .bug79971 +%patch -P255 -p1 -b .bug81719 +%patch -P256 -p1 -b .bug81720 +%patch -P257 -p1 -b .bug81727 +%patch -P258 -p1 -b .bug81726 +%patch -P259 -p1 -b .bug81740 +%patch -P260 -p1 -b .bug81744 +%patch -P261 -p1 -b .bug81746 +%patch -P262 -p1 -b .cve0662 +%patch -P263 -p1 -b .cve3247 +%patch -P264 -p1 -b .cve3823 +%patch -P265 -p1 -b .cve3824 +%patch -P266 -p1 -b .cve2756 +%patch -P267 -p1 -b .cve3096 +%patch -P268 -p1 -b .cve5458 +%patch -P269 -p1 -b .cve8925 +%patch -P270 -p1 -b .cve8926 +%patch -P271 -p1 -b .cve8927 +%patch -P272 -p1 -b .cve11236 +%patch -P273 -p1 -b .cve11234 +%patch -P274 -p1 -b .cve8932 +%patch -P275 -p1 -b .cve11233 +%patch -P276 -p1 -b .ghsa4w77 +%patch -P277 -p1 -b .cve6722 +%patch -P278 -p1 -b .cve7261 +%patch -P279 -p1 -b .cve7262 +%patch -P280 -p1 -b .cve6735 +%patch -P281 -p1 -b .cve7268 # Fixes for tests -%patch300 -p1 -b .datetests +%patch -P300 -p1 -b .datetests %if %{with_libpcre} if ! pkg-config libpcre --atleast-version 8.34 ; then # Only apply when system libpcre < 8.34 -%patch301 -p1 -b .pcre834 +%patch -P301 -p1 -b .pcre834 fi %endif # New openssl certs -%patch302 -p1 -b .renewcert +%patch -P302 -p1 -b .renewcert rm ext/openssl/tests/bug65538_003.phpt # WIP patch # Prevent %%doc confusion over LICENSE files -cp Zend/LICENSE Zend/ZEND_LICENSE +cp Zend/LICENSE ZEND_LICENSE cp TSRM/LICENSE TSRM_LICENSE %if ! %{with_libgd} cp ext/gd/libgd/README libgd_README @@ -1327,7 +1394,7 @@ build --libdir=%{_libdir}/php \ --with-mysqli=shared,mysqlnd \ --with-mysql-sock=%{mysql_sock} \ %if %{with_oci8} - --with-oci8=shared,instantclient,%{_root_libdir}/oracle/%{oraclever}/client64/lib,%{oraclever} \ + --with-oci8=shared,instantclient,%{_root_prefix}/lib/oracle/%{oracledir}/client64/lib,%{oraclever} \ --with-pdo-oci=shared,instantclient,%{_root_prefix},%{oraclever} \ %endif %if %{with_interbase} @@ -1439,8 +1506,7 @@ popd %check %if %runselftest - -cd build-apache +cd build-fpm # Run tests, using the CLI SAPI export NO_INTERACTION=1 REPORT_EXIT_STATUS=1 MALLOC_CHECK_=2 @@ -1553,8 +1619,8 @@ mv $RPM_BUILD_ROOT%{_sysconfdir}/php-fpm.d/www.conf.default . %if %{with_systemd} install -Dm 644 %{SOURCE6} $RPM_BUILD_ROOT%{_unitdir}/%{?scl_prefix}php-fpm.service %if 0%{?fedora} >= 27 || 0%{?rhel} >= 8 -install -Dm 644 %{SOURCE12} $RPM_BUILD_ROOT%{_unitdir}/httpd.service.d/%{?scl_prefix}php-fpm.conf -install -Dm 644 %{SOURCE12} $RPM_BUILD_ROOT%{_unitdir}/nginx.service.d/%{?scl_prefix}php-fpm.conf +install -Dm 644 %{SOURCE12} $RPM_BUILD_ROOT%{_root_sysconfdir}/systemd/system/httpd.service.d/%{?scl_prefix}php-fpm.conf +install -Dm 644 %{SOURCE12} $RPM_BUILD_ROOT%{_root_sysconfdir}/systemd/system/nginx.service.d/%{?scl_prefix}php-fpm.conf %endif sed -e 's:/run:%{_localstatedir}/run:' \ -e 's:/etc/sysconfig:%{_sysconfdir}/sysconfig:' \ @@ -1818,7 +1884,7 @@ cat << EOF WARNING : PHP 7.0 have reached its "End of Life" in December 2018. Even, if this package includes some of - the important security fix, backported from 7.2, the + the important security fixes, backported from 8.2, the UPGRADE to a maintained version is very strongly RECOMMENDED. ===================================================================== @@ -1845,7 +1911,7 @@ EOF %files common -f files.common %doc CODING_STANDARDS CREDITS EXTENSIONS NEWS README* -%license LICENSE TSRM_LICENSE +%license LICENSE TSRM_LICENSE ZEND_LICENSE %license libmagic_LICENSE %license phar_LICENSE %license timelib_LICENSE @@ -1909,8 +1975,8 @@ EOF %if %{with_systemd} %{_unitdir}/%{?scl_prefix}php-fpm.service %if 0%{?fedora} >= 27 || 0%{?rhel} >= 8 -%{_unitdir}/httpd.service.d/%{?scl_prefix}php-fpm.conf -%{_unitdir}/nginx.service.d/%{?scl_prefix}php-fpm.conf +%config(noreplace) %{_root_sysconfdir}/systemd/system/httpd.service.d/%{?scl_prefix}php-fpm.conf +%config(noreplace) %{_root_sysconfdir}/systemd/system/nginx.service.d/%{?scl_prefix}php-fpm.conf %endif %dir %{_root_sysconfdir}/systemd/system/%{?scl_prefix}php-fpm.service.d %else @@ -2002,6 +2068,142 @@ EOF %changelog +* Tue May 12 2026 Remi Collet <remi@remirepo.net> - 7.0.33-46 +- Fix XSS within status endpoint + CVE-2026-6735 +- Fix Stale SOAP_GLOBAL(ref_map) pointer with Apache Map + CVE-2026-6722 +- Fix Use-after-free after header parsing failure with SOAP_PERSISTENCE_SESSION + CVE-2026-7261 +- Fix Broken Apache map value NULL check + CVE-2026-7262 +- Fix Signed integer overflow of char array offset + CVE-2026-7568 + +* Tue Nov 26 2024 Remi Collet <remi@remirepo.net> - 7.0.33-45 +- 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 + +* Thu Sep 26 2024 Remi Collet <remi@remirepo.net> - 7.0.33-44 +- Fix Bypass of CVE-2012-1823, Argument Injection in PHP-CGI + CVE-2024-4577 +- Fix Bypass of CVE-2024-4577, Parameter Injection Vulnerability + CVE-2024-8926 +- Fix cgi.force_redirect configuration is bypassable due to the environment variable collision + CVE-2024-8927 +- Fix Erroneous parsing of multipart form data + CVE-2024-8925 + +* Wed Jul 31 2024 Remi Collet <remi@remirepo.net> - 7.0.33-43 +- use oracle client library version 23.5 on x86_64 + +* Tue Jun 4 2024 Remi Collet <remi@remirepo.net> - 7.0.33-42 +- Fix filter bypass in filter_var FILTER_VALIDATE_URL + CVE-2024-5458 + +* Wed Apr 10 2024 Remi Collet <remi@remirepo.net> - 7.0.33-41 +- use oracle client library version 21.13 +- Fix __Host-/__Secure- cookie bypass due to partial CVE-2022-31629 fix + CVE-2024-2756 +- Fix password_verify can erroneously return true opening ATO risk + CVE-2024-3096 + +* Wed Aug 2 2023 Remi Collet <remi@remirepo.net> - 7.0.33-40 +- Fix Security issue with external entity loading in XML without enabling it + GHSA-3qrf-m4j2-pcrr CVE-2023-3823 +- Fix Buffer mismanagement in phar_dir_read() + GHSA-jqcx-ccgc-xwhv CVE-2023-3824 +- move httpd/nginx wants directive to config files in /etc + +* Wed Jun 21 2023 Remi Collet <remi@remirepo.net> - 7.0.33-39 +- fix possible buffer overflow in date +- define %%php70___phpize and %%php70___phpconfig + +* Wed Jun 7 2023 Remi Collet <remi@remirepo.net> - 7.0.33-38 +- Fix Missing error check and insufficient random bytes in HTTP Digest + authentication for SOAP + GHSA-76gg-c692-v2mw CVE-2023-3247 +- use oracle client library version 21.10 + +* Tue Feb 14 2023 Remi Collet <remi@remirepo.net> - 7.0.33-37 +- fix #81744: Password_verify() always return true with some hash + CVE-2023-0567 +- fix #81746: 1-byte array overrun in common path resolve code + CVE-2023-0568 +- fix DOS vulnerability when parsing multipart request body + CVE-2023-0662 + +* Tue Dec 20 2022 Remi Collet <remi@remirepo.net> - 7.0.33-36 +- pdo: fix #81740: PDO::quote() may return unquoted string + CVE-2022-31631 +- use oracle client library version 21.8 + +* Tue Sep 27 2022 Remi Collet <remi@remirepo.net> - 7.0.33-35 +- phar: fix #81726 DOS when using quine gzip file. CVE-2022-31628 +- core: fix #81727 Don't mangle HTTP variable names that clash with ones + that have a specific semantic meaning. CVE-2022-31629 +- use oracle client library version 21.7 + +* Tue Jun 7 2022 Remi Collet <remi@remirepo.net> - 7.0.33-33 +- use oracle client library version 21.6 +- mysqlnd: fix #81719: mysqlnd/pdo password buffer overflow. CVE-2022-31626 +- pgsql: fix #81720: Uninitialized array in pg_query_params(). CVE-2022-31625 + +* Mon Nov 15 2021 Remi Collet <remi@remirepo.net> - 7.0.33-32 +- Fix #79971 special character is breaking the path in xml function + CVE-2021-21707 + +* Wed Oct 20 2021 Remi Collet <remi@remirepo.net> - 7.0.33-31 +- fix PHP-FPM oob R/W in root process leading to priv escalation + CVE-2021-21703 +- use libicu version 69 +- use oracle client library version 21.3 + +* Tue Sep 7 2021 Remi Collet <remi@remirepo.net> - 7.0.33-30 +- fix intl build on F35 + +* Thu Aug 26 2021 Remi Collet <remi@remirepo.net> - 7.0.33-29 +- Fix #81211 Symlinks are followed when creating PHAR archive + +* Mon Jun 28 2021 Remi Collet <remi@remirepo.net> - 7.0.33-28 +- Fix #81122 SSRF bypass in FILTER_VALIDATE_URL + CVE-2021-21705 +- Fix #76488 Memory leak when fetching a BLOB field +- Fix #76448 Stack buffer overflow in firebird_info_cb +- Fix #76449 SIGSEGV in firebird_handle_doer +- Fix #76450 SIGSEGV in firebird_stmt_execute +- Fix #76452 Crash while parsing blob data in firebird_fetch_blob + CVE-2021-21704 + +* Thu May 27 2021 Remi Collet <remi@remirepo.net> - 7.0.33-27 +- fix snmp extension build with net-snmp without DES + +* Wed Apr 28 2021 Remi Collet <remi@remirepo.net> - 7.0.33-26 +- Fix #80710 imap_mail_compose() header injection +- use oracle client library version 21.1 + +* Wed Feb 3 2021 Remi Collet <remi@remirepo.net> - 7.0.33-25 +- Fix #80672 Null Dereference in SoapClient + CVE-2021-21702 +- better fix for #77423 + +* Mon Jan 4 2021 Remi Collet <remi@remirepo.net> - 7.0.33-24 +- Fix #77423 FILTER_VALIDATE_URL accepts URLs with invalid userinfo + CVE-2020-7071 + +* Tue Sep 29 2020 Remi Collet <remi@remirepo.net> - 7.0.33-23 +- Core: + Fix #79699 PHP parses encoded cookie names so malicious `__Host-` cookies can be sent + CVE-2020-7070 + * Tue Aug 4 2020 Remi Collet <remi@remirepo.net> - 7.0.33-22 - Core: Fix #79877 getimagesize function silently truncates after a null byte |
