diff --git a/NEWS b/NEWS
index f9b8fb276472d..e3e7a5613be3a 100644
--- a/NEWS
+++ b/NEWS
@@ -1,8 +1,45 @@
PHP NEWS
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
-?? ??? ????, PHP 8.1.29
+26 Sep 2024, PHP 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)
+
+- FPM:
+ . Fixed bug GHSA-865w-9rf3-2wh5 (Logs from childrens may be altered).
+ (CVE-2024-9026) (Jakub Zelenka)
+- SAPI:
+ . Fixed bug GHSA-9pqp-7h25-4f32 (Erroneous parsing of multipart form data).
+ (CVE-2024-8925) (Arnaud)
+
+06 Jun 2024, PHP 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)
+
+- OpenSSL:
+ . The openssl_private_decrypt function in PHP, when using PKCS1 padding
+ (OPENSSL_PKCS1_PADDING, which is the default), is vulnerable to the Marvin Attack
+ unless it is used with an OpenSSL version that includes the changes from this pull
+ request: https://github.com/openssl/openssl/pull/13817 (rsa_pkcs1_implicit_rejection).
+ These changes are part of OpenSSL 3.2 and have also been backported to stable
+ versions of various Linux distributions, as well as to the PHP builds provided for
+ Windows since the previous release. All distributors and builders should ensure that
+ this version is used to prevent PHP from being vulnerable. (CVE-2024-2408)
+
+- Standard:
+ . Fixed bug GHSA-9fcc-425m-g385 (Bypass of CVE-2024-1874).
+ (CVE-2024-5585) (nielsdos)
11 Apr 2024, PHP 8.1.28
@@ -31,7 +68,7 @@ PHP NEWS
- FPM:
. Fixed bug GH-12705 (Segmentation fault in fpm_status_export_to_zval).
(Patrick Prasse)
-
+
- Intl:
. Fixed bug GH-12635 (Test bug69398.phpt fails with ICU 74.1). (nielsdos)
diff --git a/Zend/zend.h b/Zend/zend.h
index 9a414960e2a87..c8bad6826e55c 100644
--- a/Zend/zend.h
+++ b/Zend/zend.h
@@ -20,7 +20,7 @@
#ifndef ZEND_H
#define ZEND_H
-#define ZEND_VERSION "4.1.29-dev"
+#define ZEND_VERSION "4.1.30"
#define ZEND_ENGINE_3
diff --git a/configure.ac b/configure.ac
index 83d50445be205..58c290325493a 100644
--- a/configure.ac
+++ b/configure.ac
@@ -17,7 +17,7 @@ dnl Basic autoconf initialization, generation of config.nice.
dnl ----------------------------------------------------------------------------
AC_PREREQ([2.68])
-AC_INIT([PHP],[8.1.29-dev],[https://github.com/php/php-src/issues],[php],[https://www.php.net])
+AC_INIT([PHP],[8.1.30],[https://github.com/php/php-src/issues],[php],[https://www.php.net])
AC_CONFIG_SRCDIR([main/php_version.h])
AC_CONFIG_AUX_DIR([build])
AC_PRESERVE_HELP_ORDER
diff --git a/ext/filter/logical_filters.c b/ext/filter/logical_filters.c
index 182f2b3cd0448..3db0ef4090500 100644
--- a/ext/filter/logical_filters.c
+++ b/ext/filter/logical_filters.c
@@ -89,7 +89,7 @@
#define FORMAT_IPV4 4
#define FORMAT_IPV6 6
-static int _php_filter_validate_ipv6(char *str, size_t str_len, int ip[8]);
+static int _php_filter_validate_ipv6(const char *str, size_t str_len, int ip[8]);
static int php_filter_parse_int(const char *str, size_t str_len, zend_long *ret) { /* {{{ */
zend_long ctx_value;
@@ -580,6 +580,14 @@ static int is_userinfo_valid(zend_string *str)
return 1;
}
+static 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, NULL);
+}
+
void php_filter_validate_url(https://redirect.github.com/PHP_INPUT_FILTER_PARAM_DECL) /* {{{ */
{
php_url *url;
@@ -600,7 +608,7 @@ void php_filter_validate_url(https://redirect.github.com/PHP_INPUT_FILTER_PARAM_DECL) /* {{{ */
if (url->scheme != NULL &&
(zend_string_equals_literal_ci(url->scheme, "http") || zend_string_equals_literal_ci(url->scheme, "https"))) {
- char *e, *s, *t;
+ const char *s;
size_t l;
if (url->host == NULL) {
@@ -609,17 +617,14 @@ void php_filter_validate_url(https://redirect.github.com/PHP_INPUT_FILTER_PARAM_DECL) /* {{{ */
s = ZSTR_VAL(url->host);
l = ZSTR_LEN(url->host);
- e = s + 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, NULL)) {
- php_url_free(url);
- return;
- }
- // Validate domain
- if (!_php_filter_validate_domain(ZSTR_VAL(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(ZSTR_VAL(url->host), l, FILTER_FLAG_HOSTNAME)
+ ) {
php_url_free(url);
RETURN_VALIDATION_FAILED
}
@@ -753,15 +758,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, int ip[8]) /* {{{ */
+static int _php_filter_validate_ipv6(const char *str, size_t str_len, int ip[8]) /* {{{ */
{
int compressed_pos = -1;
int blocks = 0;
int num, n, i;
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 0000000000000..0092408ee5ad6
--- /dev/null
+++ b/ext/filter/tests/ghsa-w8qr-v226-r27w.phpt
@@ -0,0 +1,41 @@
+--TEST--
+GHSA-w8qr-v226-r27w
+--EXTENSIONS--
+filter
+--FILE--
+
+--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:102:304]/"
+string(17) "http://test@[::1]/"
diff --git a/ext/standard/proc_open.c b/ext/standard/proc_open.c
index 8aae54072650e..495af6cba35e5 100644
--- a/ext/standard/proc_open.c
+++ b/ext/standard/proc_open.c
@@ -546,48 +546,39 @@ static void append_win_escaped_arg(smart_str *str, zend_string *arg, bool is_cmd
smart_str_appendc(str, '"');
}
-static inline int stricmp_end(const char* suffix, const char* str) {
- size_t suffix_len = strlen(suffix);
- size_t str_len = strlen(str);
+static bool is_executed_by_cmd(const char *prog_name, size_t prog_name_length)
+{
+ size_t out_len;
+ WCHAR long_name[MAX_PATH];
+ WCHAR full_name[MAX_PATH];
+ LPWSTR file_part = NULL;
- if (suffix_len > str_len) {
- return -1; /* Suffix is longer than string, cannot match. */
- }
+ wchar_t *prog_name_wide = php_win32_cp_conv_any_to_w(prog_name, prog_name_length, &out_len);
- /* Compare the end of the string with the suffix, ignoring case. */
- return _stricmp(str + (str_len - suffix_len), suffix);
-}
+ if (GetLongPathNameW(prog_name_wide, long_name, MAX_PATH) == 0) {
+ /* This can fail for example with ERROR_FILE_NOT_FOUND (short path resolution only works for existing files)
+ * in which case we'll pass the path verbatim to the FullPath transformation. */
+ lstrcpynW(long_name, prog_name_wide, MAX_PATH);
+ }
-static bool is_executed_by_cmd(const char *prog_name)
-{
- /* If program name is cmd.exe, then return true. */
- if (_stricmp("cmd.exe", prog_name) == 0 || _stricmp("cmd", prog_name) == 0
- || stricmp_end("\\cmd.exe", prog_name) == 0 || stricmp_end("\\cmd", prog_name) == 0) {
- return true;
- }
+ free(prog_name_wide);
+ prog_name_wide = NULL;
- /* Find the last occurrence of the directory separator (backslash or forward slash). */
- char *last_separator = strrchr(prog_name, '\\');
- char *last_separator_fwd = strrchr(prog_name, '/');
- if (last_separator_fwd && (!last_separator || last_separator < last_separator_fwd)) {
- last_separator = last_separator_fwd;
+ if (GetFullPathNameW(long_name, MAX_PATH, full_name, &file_part) == 0 || file_part == NULL) {
+ return false;
}
- /* Find the last dot in the filename after the last directory separator. */
- char *extension = NULL;
- if (last_separator != NULL) {
- extension = strrchr(last_separator, '.');
+ bool uses_cmd = false;
+ if (_wcsicmp(file_part, L"cmd.exe") == 0 || _wcsicmp(file_part, L"cmd") == 0) {
+ uses_cmd = true;
} else {
- extension = strrchr(prog_name, '.');
- }
-
- if (extension == NULL || extension == prog_name) {
- /* No file extension found, it is not batch file. */
- return false;
+ const WCHAR *extension_dot = wcsrchr(file_part, L'.');
+ if (extension_dot && (_wcsicmp(extension_dot, L".bat") == 0 || _wcsicmp(extension_dot, L".cmd") == 0)) {
+ uses_cmd = true;
+ }
}
- /* Check if the file extension is ".bat" or ".cmd" which is always executed by cmd.exe. */
- return _stricmp(extension, ".bat") == 0 || _stricmp(extension, ".cmd") == 0;
+ return uses_cmd;
}
static zend_string *create_win_command_from_args(HashTable *args)
@@ -606,7 +597,7 @@ static zend_string *create_win_command_from_args(HashTable *args)
}
if (is_prog_name) {
- is_cmd_execution = is_executed_by_cmd(ZSTR_VAL(arg_str));
+ is_cmd_execution = is_executed_by_cmd(ZSTR_VAL(arg_str), ZSTR_LEN(arg_str));
} else {
smart_str_appendc(&str, ' ');
}
diff --git a/ext/standard/tests/general_functions/ghsa-9fcc-425m-g385_001.phpt b/ext/standard/tests/general_functions/ghsa-9fcc-425m-g385_001.phpt
new file mode 100644
index 0000000000000..2873210608497
--- /dev/null
+++ b/ext/standard/tests/general_functions/ghsa-9fcc-425m-g385_001.phpt
@@ -0,0 +1,56 @@
+--TEST--
+GHSA-9fcc-425m-g385 - bypass CVE-2024-1874 - batch file variation
+--SKIPIF--
+
+--FILE--
+
+--EXPECTF--
+'"%sghsa-9fcc-425m-g385_001.bat."' is not recognized as an internal or external command,
+operable program or batch file.
+%sghsa-9fcc-425m-g385_001.bat
+"¬epad.exe
+%sghsa-9fcc-425m-g385_001.bat.
+"¬epad.exe
+%sghsa-9fcc-425m-g385_001.bat. ...
+"¬epad.exe
+%sghsa-9fcc-425m-g385_001.bat. ... .
+"¬epad.exe
+'"%sghsa-9fcc-425m-g385_001.bat. ... . ."' is not recognized as an internal or external command,
+operable program or batch file.
+
+Warning: proc_open(): CreateProcess failed, error code: 2 in %s on line %d
+--CLEAN--
+
diff --git a/ext/standard/tests/general_functions/ghsa-9fcc-425m-g385_002.phpt b/ext/standard/tests/general_functions/ghsa-9fcc-425m-g385_002.phpt
new file mode 100644
index 0000000000000..714836557af5c
--- /dev/null
+++ b/ext/standard/tests/general_functions/ghsa-9fcc-425m-g385_002.phpt
@@ -0,0 +1,66 @@
+--TEST--
+GHSA-9fcc-425m-g385 - bypass CVE-2024-1874 - cmd.exe variation
+--SKIPIF--
+
+--FILE--
+
+--EXPECTF--
+%sghsa-9fcc-425m-g385_002.bat
+"¬epad.exe
+%sghsa-9fcc-425m-g385_002.bat
+"¬epad.exe
+%sghsa-9fcc-425m-g385_002.bat
+"¬epad.exe
+%sghsa-9fcc-425m-g385_002.bat
+"¬epad.exe
+
+Warning: proc_open(): CreateProcess failed, error code: 2 in %s on line %d
+%sghsa-9fcc-425m-g385_002.bat
+"¬epad.exe
+%sghsa-9fcc-425m-g385_002.bat
+"¬epad.exe
+
+Warning: proc_open(): CreateProcess failed, error code: 2 in %s on line %d
+
+Warning: proc_open(): CreateProcess failed, error code: 2 in %s on line %d
+
+Warning: proc_open(): CreateProcess failed, error code: 2 in %s on line %d
+--CLEAN--
+
diff --git a/ext/standard/tests/general_functions/ghsa-9fcc-425m-g385_003.phpt b/ext/standard/tests/general_functions/ghsa-9fcc-425m-g385_003.phpt
new file mode 100644
index 0000000000000..a632965eb989a
--- /dev/null
+++ b/ext/standard/tests/general_functions/ghsa-9fcc-425m-g385_003.phpt
@@ -0,0 +1,550 @@
+--TEST--
+GHSA-9fcc-425m-g385 - bypass CVE-2024-1874 - exhaustive suffix test
+--SKIPIF--
+
+--FILE--
+ true));
+ var_dump($proc);
+ proc_close($proc);
+ } catch (Error) {}
+}
+
+?>
+--EXPECTF--
+Testing 1
+bool(false)
+Testing 2
+bool(false)
+Testing 3
+bool(false)
+Testing 4
+bool(false)
+Testing 5
+bool(false)
+Testing 6
+bool(false)
+Testing 7
+bool(false)
+Testing 8
+bool(false)
+Testing 9
+bool(false)
+Testing 10
+bool(false)
+Testing 11
+bool(false)
+Testing 12
+bool(false)
+Testing 13
+bool(false)
+Testing 14
+bool(false)
+Testing 15
+bool(false)
+Testing 16
+bool(false)
+Testing 17
+bool(false)
+Testing 18
+bool(false)
+Testing 19
+bool(false)
+Testing 20
+bool(false)
+Testing 21
+bool(false)
+Testing 22
+bool(false)
+Testing 23
+bool(false)
+Testing 24
+bool(false)
+Testing 25
+bool(false)
+Testing 26
+bool(false)
+Testing 27
+bool(false)
+Testing 28
+bool(false)
+Testing 29
+bool(false)
+Testing 30
+bool(false)
+Testing 31
+bool(false)
+Testing 32
+resource(%d) of type (process)
+%s.bat
+"¬epad.exe
+Testing 33
+bool(false)
+Testing 34
+bool(false)
+Testing 35
+bool(false)
+Testing 36
+bool(false)
+Testing 37
+bool(false)
+Testing 38
+bool(false)
+Testing 39
+bool(false)
+Testing 40
+bool(false)
+Testing 41
+bool(false)
+Testing 42
+bool(false)
+Testing 43
+bool(false)
+Testing 44
+bool(false)
+Testing 45
+bool(false)
+Testing 46
+resource(%d) of type (process)
+'"%s.bat."' is not recognized as an internal or external command,
+operable program or batch file.
+Testing 47
+bool(false)
+Testing 48
+bool(false)
+Testing 49
+bool(false)
+Testing 50
+bool(false)
+Testing 51
+bool(false)
+Testing 52
+bool(false)
+Testing 53
+bool(false)
+Testing 54
+bool(false)
+Testing 55
+bool(false)
+Testing 56
+bool(false)
+Testing 57
+bool(false)
+Testing 58
+bool(false)
+Testing 59
+bool(false)
+Testing 60
+bool(false)
+Testing 61
+bool(false)
+Testing 62
+bool(false)
+Testing 63
+bool(false)
+Testing 64
+bool(false)
+Testing 65
+bool(false)
+Testing 66
+bool(false)
+Testing 67
+bool(false)
+Testing 68
+bool(false)
+Testing 69
+bool(false)
+Testing 70
+bool(false)
+Testing 71
+bool(false)
+Testing 72
+bool(false)
+Testing 73
+bool(false)
+Testing 74
+bool(false)
+Testing 75
+bool(false)
+Testing 76
+bool(false)
+Testing 77
+bool(false)
+Testing 78
+bool(false)
+Testing 79
+bool(false)
+Testing 80
+bool(false)
+Testing 81
+bool(false)
+Testing 82
+bool(false)
+Testing 83
+bool(false)
+Testing 84
+bool(false)
+Testing 85
+bool(false)
+Testing 86
+bool(false)
+Testing 87
+bool(false)
+Testing 88
+bool(false)
+Testing 89
+bool(false)
+Testing 90
+bool(false)
+Testing 91
+bool(false)
+Testing 92
+bool(false)
+Testing 93
+bool(false)
+Testing 94
+bool(false)
+Testing 95
+bool(false)
+Testing 96
+bool(false)
+Testing 97
+bool(false)
+Testing 98
+bool(false)
+Testing 99
+bool(false)
+Testing 100
+bool(false)
+Testing 101
+bool(false)
+Testing 102
+bool(false)
+Testing 103
+bool(false)
+Testing 104
+bool(false)
+Testing 105
+bool(false)
+Testing 106
+bool(false)
+Testing 107
+bool(false)
+Testing 108
+bool(false)
+Testing 109
+bool(false)
+Testing 110
+bool(false)
+Testing 111
+bool(false)
+Testing 112
+bool(false)
+Testing 113
+bool(false)
+Testing 114
+bool(false)
+Testing 115
+bool(false)
+Testing 116
+bool(false)
+Testing 117
+bool(false)
+Testing 118
+bool(false)
+Testing 119
+bool(false)
+Testing 120
+bool(false)
+Testing 121
+bool(false)
+Testing 122
+bool(false)
+Testing 123
+bool(false)
+Testing 124
+bool(false)
+Testing 125
+bool(false)
+Testing 126
+bool(false)
+Testing 127
+bool(false)
+Testing 128
+bool(false)
+Testing 129
+bool(false)
+Testing 130
+bool(false)
+Testing 131
+bool(false)
+Testing 132
+bool(false)
+Testing 133
+bool(false)
+Testing 134
+bool(false)
+Testing 135
+bool(false)
+Testing 136
+bool(false)
+Testing 137
+bool(false)
+Testing 138
+bool(false)
+Testing 139
+bool(false)
+Testing 140
+bool(false)
+Testing 141
+bool(false)
+Testing 142
+bool(false)
+Testing 143
+bool(false)
+Testing 144
+bool(false)
+Testing 145
+bool(false)
+Testing 146
+bool(false)
+Testing 147
+bool(false)
+Testing 148
+bool(false)
+Testing 149
+bool(false)
+Testing 150
+bool(false)
+Testing 151
+bool(false)
+Testing 152
+bool(false)
+Testing 153
+bool(false)
+Testing 154
+bool(false)
+Testing 155
+bool(false)
+Testing 156
+bool(false)
+Testing 157
+bool(false)
+Testing 158
+bool(false)
+Testing 159
+bool(false)
+Testing 160
+bool(false)
+Testing 161
+bool(false)
+Testing 162
+bool(false)
+Testing 163
+bool(false)
+Testing 164
+bool(false)
+Testing 165
+bool(false)
+Testing 166
+bool(false)
+Testing 167
+bool(false)
+Testing 168
+bool(false)
+Testing 169
+bool(false)
+Testing 170
+bool(false)
+Testing 171
+bool(false)
+Testing 172
+bool(false)
+Testing 173
+bool(false)
+Testing 174
+bool(false)
+Testing 175
+bool(false)
+Testing 176
+bool(false)
+Testing 177
+bool(false)
+Testing 178
+bool(false)
+Testing 179
+bool(false)
+Testing 180
+bool(false)
+Testing 181
+bool(false)
+Testing 182
+bool(false)
+Testing 183
+bool(false)
+Testing 184
+bool(false)
+Testing 185
+bool(false)
+Testing 186
+bool(false)
+Testing 187
+bool(false)
+Testing 188
+bool(false)
+Testing 189
+bool(false)
+Testing 190
+bool(false)
+Testing 191
+bool(false)
+Testing 192
+bool(false)
+Testing 193
+bool(false)
+Testing 194
+bool(false)
+Testing 195
+bool(false)
+Testing 196
+bool(false)
+Testing 197
+bool(false)
+Testing 198
+bool(false)
+Testing 199
+bool(false)
+Testing 200
+bool(false)
+Testing 201
+bool(false)
+Testing 202
+bool(false)
+Testing 203
+bool(false)
+Testing 204
+bool(false)
+Testing 205
+bool(false)
+Testing 206
+bool(false)
+Testing 207
+bool(false)
+Testing 208
+bool(false)
+Testing 209
+bool(false)
+Testing 210
+bool(false)
+Testing 211
+bool(false)
+Testing 212
+bool(false)
+Testing 213
+bool(false)
+Testing 214
+bool(false)
+Testing 215
+bool(false)
+Testing 216
+bool(false)
+Testing 217
+bool(false)
+Testing 218
+bool(false)
+Testing 219
+bool(false)
+Testing 220
+bool(false)
+Testing 221
+bool(false)
+Testing 222
+bool(false)
+Testing 223
+bool(false)
+Testing 224
+bool(false)
+Testing 225
+bool(false)
+Testing 226
+bool(false)
+Testing 227
+bool(false)
+Testing 228
+bool(false)
+Testing 229
+bool(false)
+Testing 230
+bool(false)
+Testing 231
+bool(false)
+Testing 232
+bool(false)
+Testing 233
+bool(false)
+Testing 234
+bool(false)
+Testing 235
+bool(false)
+Testing 236
+bool(false)
+Testing 237
+bool(false)
+Testing 238
+bool(false)
+Testing 239
+bool(false)
+Testing 240
+bool(false)
+Testing 241
+bool(false)
+Testing 242
+bool(false)
+Testing 243
+bool(false)
+Testing 244
+bool(false)
+Testing 245
+bool(false)
+Testing 246
+bool(false)
+Testing 247
+bool(false)
+Testing 248
+bool(false)
+Testing 249
+bool(false)
+Testing 250
+bool(false)
+Testing 251
+bool(false)
+Testing 252
+bool(false)
+Testing 253
+bool(false)
+Testing 254
+bool(false)
+Testing 255
+bool(false)
+--CLEAN--
+
diff --git a/main/php_version.h b/main/php_version.h
index a7b35a63a9285..876ccc81aa168 100644
--- a/main/php_version.h
+++ b/main/php_version.h
@@ -2,7 +2,7 @@
/* edit configure.ac to change version number */
#define PHP_MAJOR_VERSION 8
#define PHP_MINOR_VERSION 1
-#define PHP_RELEASE_VERSION 29
-#define PHP_EXTRA_VERSION "-dev"
-#define PHP_VERSION "8.1.29-dev"
-#define PHP_VERSION_ID 80129
+#define PHP_RELEASE_VERSION 30
+#define PHP_EXTRA_VERSION ""
+#define PHP_VERSION "8.1.30"
+#define PHP_VERSION_ID 80130
diff --git a/main/rfc1867.c b/main/rfc1867.c
index 2ddad7950fdf0..83d141d38b112 100644
--- a/main/rfc1867.c
+++ b/main/rfc1867.c
@@ -751,6 +751,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/sapi/cgi/cgi_main.c b/sapi/cgi/cgi_main.c
index 499a7932bed17..c7bb7ccabf95d 100644
--- a/sapi/cgi/cgi_main.c
+++ b/sapi/cgi/cgi_main.c
@@ -1748,7 +1748,6 @@ int main(int argc, char *argv[])
int status = 0;
#endif
char *query_string;
- char *decoded_query_string;
int skip_getopt = 0;
#if defined(SIGPIPE) && defined(SIG_IGN)
@@ -1798,10 +1797,20 @@ 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.
+ * 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) {
- /* we've got query string that has no = - apache CGI will pass it to command line */
+#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 */
@@ -1809,7 +1818,9 @@ int main(int argc, char *argv[])
if(*p == '-') {
skip_getopt = 1;
}
+
free(decoded_query_string);
+#endif
}
while (!skip_getopt && (c = php_getopt(argc, argv, OPTIONS, &php_optarg, &php_optind, 0, 2)) != -1) {
@@ -1899,18 +1910,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("Security Alert! The PHP CGI cannot be accessed directly.\n\n\
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 0000000000000..fd2fcdfbf897d
--- /dev/null
+++ b/sapi/cgi/tests/ghsa-3qgc-jrrr-25jv.phpt
@@ -0,0 +1,38 @@
+--TEST--
+GHSA-3qgc-jrrr-25jv
+--SKIPIF--
+
+--FILE--
+';
+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--
+
+--EXPECTF--
+X-Powered-By: PHP/%s
+Content-type: %s
+
+hello world
diff --git a/sapi/fpm/fpm/fpm_stdio.c b/sapi/fpm/fpm/fpm_stdio.c
index 8f71e8cbfcd08..dec540d17aca8 100644
--- a/sapi/fpm/fpm/fpm_stdio.c
+++ b/sapi/fpm/fpm/fpm_stdio.c
@@ -229,7 +229,7 @@ static void fpm_stdio_child_said(struct fpm_event_s *ev, short which, void *arg)
if ((sizeof(FPM_STDIO_CMD_FLUSH) - cmd_pos) <= in_buf &&
!memcmp(buf, &FPM_STDIO_CMD_FLUSH[cmd_pos], sizeof(FPM_STDIO_CMD_FLUSH) - cmd_pos)) {
zlog_stream_finish(log_stream);
- start = cmd_pos;
+ start = sizeof(FPM_STDIO_CMD_FLUSH) - cmd_pos;
} else {
zlog_stream_str(log_stream, &FPM_STDIO_CMD_FLUSH[0], cmd_pos);
}
diff --git a/sapi/fpm/tests/log-bwp-msg-flush-split-sep-pos-end.phpt b/sapi/fpm/tests/log-bwp-msg-flush-split-sep-pos-end.phpt
new file mode 100644
index 0000000000000..528263200803e
--- /dev/null
+++ b/sapi/fpm/tests/log-bwp-msg-flush-split-sep-pos-end.phpt
@@ -0,0 +1,47 @@
+--TEST--
+FPM: Buffered worker output plain log with msg with flush split position towards separator end
+--SKIPIF--
+
+--FILE--
+start();
+$tester->expectLogStartNotices();
+$tester->request()->expectEmptyBody();
+$tester->expectLogLine(str_repeat('a', 1013) . "Quarkslab", decorated: false);
+$tester->expectLogLine("Quarkslab", decorated: false);
+$tester->terminate();
+$tester->expectLogTerminatingNotices();
+$tester->close();
+
+?>
+Done
+--EXPECT--
+Done
+--CLEAN--
+
diff --git a/sapi/fpm/tests/log-bwp-msg-flush-split-sep-pos-start.phpt b/sapi/fpm/tests/log-bwp-msg-flush-split-sep-pos-start.phpt
new file mode 100644
index 0000000000000..3490593855328
--- /dev/null
+++ b/sapi/fpm/tests/log-bwp-msg-flush-split-sep-pos-start.phpt
@@ -0,0 +1,47 @@
+--TEST--
+FPM: Buffered worker output plain log with msg with flush split position towards separator start
+--SKIPIF--
+
+--FILE--
+start();
+$tester->expectLogStartNotices();
+$tester->request()->expectEmptyBody();
+$tester->expectLogLine(str_repeat('a', 1009) . "Quarkslab", decorated: false);
+$tester->expectLogLine("Quarkslab", decorated: false);
+$tester->terminate();
+$tester->expectLogTerminatingNotices();
+$tester->close();
+
+?>
+Done
+--EXPECT--
+Done
+--CLEAN--
+
diff --git a/tests/basic/GHSA-9pqp-7h25-4f32.inc b/tests/basic/GHSA-9pqp-7h25-4f32.inc
new file mode 100644
index 0000000000000..adf72a361a2cb
--- /dev/null
+++ b/tests/basic/GHSA-9pqp-7h25-4f32.inc
@@ -0,0 +1,3 @@
+
+--FILE--
+ '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
+
+
+Warning: Boundary too large in multipart/form-data POST data in Unknown on line 0
+Hello world
+array(0) {
+}
+