import UBI php-8.0.30-3.el9_6

This commit is contained in:
eabdullin 2025-05-13 15:08:42 +00:00
parent c04b3047f7
commit bd3a652006
20 changed files with 7816 additions and 196 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,118 @@
From bc1f192102dd8cbda028e40aa31604c4885d387c Mon Sep 17 00:00:00 2001
From: Jakub Zelenka <bukka@php.net>
Date: Fri, 8 Nov 2024 23:43:47 +0100
Subject: [PATCH 3/8] Fix GHSA-c5f2-jwm7-mmq2: stream HTTP fulluri CRLF
injection
(cherry picked from commit 426a6d4539ebee34879ac5de857036bb6ff0e732)
---
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 45677c396ac..6859a4e5181 100644
--- a/ext/standard/http_fopen_wrapper.c
+++ b/ext/standard/http_fopen_wrapper.c
@@ -184,6 +184,11 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper,
return NULL;
}
+ /* Should we send the entire path in the request line, default to no. */
+ if (context && (tmpzval = php_stream_context_get_option(context, "http", "request_fulluri")) != NULL) {
+ request_fulluri = zend_is_true(tmpzval);
+ }
+
use_ssl = resource->scheme && (ZSTR_LEN(resource->scheme) > 4) && ZSTR_VAL(resource->scheme)[4] == 's';
/* choose default ports */
if (use_ssl && resource->port == 0)
@@ -203,6 +208,13 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper,
}
}
+ if (request_fulluri && (strchr(path, '\n') != NULL || strchr(path, '\r') != NULL)) {
+ php_stream_wrapper_log_error(wrapper, options, "HTTP wrapper full URI path does not allow CR or LF characters");
+ php_url_free(resource);
+ zend_string_release(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
@@ -383,12 +395,6 @@ finish:
smart_str_appends(&req_buf, "GET ");
}
- /* Should we send the entire path in the request line, default to no. */
- if (!request_fulluri && context &&
- (tmpzval = php_stream_context_get_option(context, "http", "request_fulluri")) != NULL) {
- request_fulluri = zend_is_true(tmpzval);
- }
-
if (request_fulluri) {
/* Ask for everything */
smart_str_appends(&req_buf, path);
diff --git a/ext/standard/tests/http/ghsa-c5f2-jwm7-mmq2.phpt b/ext/standard/tests/http/ghsa-c5f2-jwm7-mmq2.phpt
new file mode 100644
index 00000000000..e7dd194dbbe
--- /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
From 8d130e16fbfda7d154fedfa0f1ff1d5ad5e26815 Mon Sep 17 00:00:00 2001
From: Remi Collet <remi@remirepo.net>
Date: Fri, 22 Nov 2024 09:41:12 +0100
Subject: [PATCH 8/8] fix transport_string release
---
ext/standard/http_fopen_wrapper.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ext/standard/http_fopen_wrapper.c b/ext/standard/http_fopen_wrapper.c
index 6859a4e5181..40e6f3dd4c3 100644
--- a/ext/standard/http_fopen_wrapper.c
+++ b/ext/standard/http_fopen_wrapper.c
@@ -211,7 +211,7 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper,
if (request_fulluri && (strchr(path, '\n') != NULL || strchr(path, '\r') != NULL)) {
php_stream_wrapper_log_error(wrapper, options, "HTTP wrapper full URI path does not allow CR or LF characters");
php_url_free(resource);
- zend_string_release(transport_string);
+ efree(transport_string);
return NULL;
}
--
2.47.0

View File

@ -0,0 +1,117 @@
From 5d9e54065ed18c51e4f25d8900635f90810c7394 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/8] Fix GHSA-5hqh-c84r-qjcv: Integer overflow in the dblib
quoter causing OOB writes
(cherry picked from commit d9baa9fed8c3ba692a36b388c0c7762e5102e2e0)
---
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 7f160a402f7..d7d0901ea1a 100644
--- a/ext/pdo_dblib/dblib_driver.c
+++ b/ext/pdo_dblib/dblib_driver.c
@@ -152,6 +152,7 @@ static int dblib_handle_quoter(pdo_dbh_t *dbh, const char *unquoted, size_t unqu
size_t i;
char * q;
+ size_t extralen = 0;
*quotedlen = 0;
if (H->assume_national_character_set_strings) {
@@ -166,7 +167,7 @@ static int dblib_handle_quoter(pdo_dbh_t *dbh, const char *unquoted, size_t unqu
/* Detect quoted length, adding extra char for doubled single quotes */
for (i = 0; i < unquotedlen; i++) {
- if (unquoted[i] == '\'') ++*quotedlen;
+ if (unquoted[i] == '\'') ++extralen;
++*quotedlen;
}
@@ -174,6 +175,11 @@ static int dblib_handle_quoter(pdo_dbh_t *dbh, const char *unquoted, size_t unqu
if (use_national_character_set) {
++*quotedlen; /* N prefix */
}
+ if (UNEXPECTED(*quotedlen > ZSTR_MAX_LEN - extralen)) {
+ return 0;
+ }
+
+ *quotedlen += extralen;
q = *quoted = emalloc(*quotedlen + 1); /* Add byte for terminal null */
if (use_national_character_set) {
*q++ = 'N';
diff --git a/ext/pdo_dblib/tests/GHSA-5hqh-c84r-qjcv.phpt b/ext/pdo_dblib/tests/GHSA-5hqh-c84r-qjcv.phpt
new file mode 100644
index 00000000000..431c61951ee
--- /dev/null
+++ b/ext/pdo_dblib/tests/GHSA-5hqh-c84r-qjcv.phpt
@@ -0,0 +1,24 @@
+--TEST--
+GHSA-5hqh-c84r-qjcv (Integer overflow in the dblib quoter causing OOB writes)
+--EXTENSIONS--
+pdo_dblib
+--SKIPIF--
+<?php
+if (PHP_INT_SIZE != 4) die("skip for 32bit platforms only");
+if (PHP_OS_FAMILY === "Windows") die("skip not for Windows because the virtual address space for application is only 2GiB");
+if (getenv("SKIP_SLOW_TESTS")) die("skip slow test");
+require __DIR__ . '/config.inc';
+getDbConnection();
+?>
+--INI--
+memory_limit=-1
+--FILE--
+<?php
+
+require __DIR__ . '/config.inc';
+$db = getDbConnection();
+var_dump($db->quote(str_repeat("'", 2147483646)));
+
+?>
+--EXPECT--
+bool(false)
--
2.47.0
From b4f73be75dbdde970a18cc7a636898b10400fb3f 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/8] Fix GHSA-5hqh-c84r-qjcv: Integer overflow in the firebird
quoter causing OOB writes
(cherry picked from commit 69c5f68fdc3deed9ebce2cc44b4bf5e0c47cd28f)
---
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 e0a424c56ab..fb697978503 100644
--- a/ext/pdo_firebird/firebird_driver.c
+++ b/ext/pdo_firebird/firebird_driver.c
@@ -663,7 +663,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;
@@ -678,6 +678,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

View File

@ -0,0 +1,191 @@
From 2e07a3acd7a6b53c55325b94bed97748d7697b53 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)
---
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 00000000000..77fcb680894
--- /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 27a9ad089e7..dc888bdfc64 100644
--- a/main/php_variables.c
+++ b/main/php_variables.c
@@ -54,6 +54,21 @@ static zend_always_inline void php_register_variable_quick(const char *name, siz
zend_string_release_ex(key, 0);
}
+/* 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 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 true;
+ }
+
+ 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 true;
+ }
+
+ return false;
+}
+
PHPAPI void php_register_variable_ex(const char *var_name, zval *val, zval *track_vars_array)
{
char *p = NULL;
@@ -104,20 +119,6 @@ PHPAPI void php_register_variable_ex(const char *var_name, zval *val, zval *trac
}
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_ptr_dtor_nogc(val);
free_alloca(var_orig, use_heap);
@@ -221,6 +222,12 @@ PHPAPI void php_register_variable_ex(const char *var_name, zval *val, zval *trac
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;
@@ -258,6 +265,12 @@ plain_var:
zval_ptr_dtor_nogc(val);
}
} else {
+ if (php_is_forbidden_variable_name(index, index_len, var_name)) {
+ zval_ptr_dtor_nogc(val);
+ free_alloca(var_orig, use_heap);
+ return;
+ }
+
zend_ulong idx;
/*
--
2.44.0
From 366cc249b7d54707572beb7096e8f6c65ee79719 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
---
NEWS | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/NEWS b/NEWS
index 8147a7e517c..14fda3a58b9 100644
--- a/NEWS
+++ b/NEWS
@@ -1,5 +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)
+
03 Aug 2023, PHP 8.0.30
- Libxml:
--
2.44.0

View File

@ -0,0 +1,77 @@
From 81794c73068d9a44bf109bbcc9793e7b56a1c051 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)
---
ext/standard/password.c | 5 +++++
ext/standard/tests/password/password_bcrypt_errors.phpt | 7 +++++++
2 files changed, 12 insertions(+)
diff --git a/ext/standard/password.c b/ext/standard/password.c
index fb29e7bbba4..40117983f70 100644
--- a/ext/standard/password.c
+++ b/ext/standard/password.c
@@ -184,6 +184,11 @@ static zend_string* php_password_bcrypt_hash(const zend_string *password, zend_a
zval *zcost;
zend_long cost = PHP_PASSWORD_BCRYPT_COST;
+ if (memchr(ZSTR_VAL(password), '\0', ZSTR_LEN(password))) {
+ zend_value_error("Bcrypt password must not contain null character");
+ return NULL;
+ }
+
if (options && (zcost = zend_hash_str_find(options, "cost", sizeof("cost")-1)) != NULL) {
cost = zval_get_long(zcost);
}
diff --git a/ext/standard/tests/password/password_bcrypt_errors.phpt b/ext/standard/tests/password/password_bcrypt_errors.phpt
index 10c3483f5a8..5d823cba021 100644
--- a/ext/standard/tests/password/password_bcrypt_errors.phpt
+++ b/ext/standard/tests/password/password_bcrypt_errors.phpt
@@ -14,7 +14,14 @@ try {
} catch (ValueError $exception) {
echo $exception->getMessage() . "\n";
}
+
+try {
+ var_dump(password_hash("null\0password", PASSWORD_BCRYPT));
+} catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
?>
--EXPECT--
Invalid bcrypt cost parameter specified: 3
Invalid bcrypt cost parameter specified: 32
+Bcrypt password must not contain null character
--
2.44.0
From 24f77904ee2259d722559f129f96a1f145a2367b 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
---
NEWS | 2 ++
1 file changed, 2 insertions(+)
diff --git a/NEWS b/NEWS
index 14fda3a58b9..8b4801d707e 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)
03 Aug 2023, PHP 8.0.30
--
2.44.0

View File

@ -0,0 +1,177 @@
From 4066610b47e22c24cbee91be434a94357056a479 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.
---
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 ad011568aac..300c6e2809c 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;
@@ -572,6 +572,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(PHP_INPUT_FILTER_PARAM_DECL) /* {{{ */
{
php_url *url;
@@ -592,7 +600,7 @@ void php_filter_validate_url(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) {
@@ -601,17 +609,14 @@ void php_filter_validate_url(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
}
@@ -745,15 +750,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 00000000000..0092408ee5a
--- /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 a1ff81b786bd519597e770795be114f5171f0648 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
---
NEWS | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/NEWS b/NEWS
index 1300609f189..7a9b6bdae18 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

View File

@ -0,0 +1,188 @@
From 2b0daf421c162376892832588eccdfa9a286ed09 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)
---
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 3086e8da3db..eafe6a67d2e 100644
--- a/main/rfc1867.c
+++ b/main/rfc1867.c
@@ -752,6 +752,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 00000000000..adf72a361a2
--- /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 00000000000..af819163705
--- /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 c75683864f6e4188439e8ca2adbb05824918be12 Mon Sep 17 00:00:00 2001
From: Jakub Zelenka <bukka@php.net>
Date: Mon, 23 Sep 2024 18:54:31 +0100
Subject: [PATCH 7/8] Skip GHSA-9pqp-7h25-4f32 test on Windows
(cherry picked from commit c70e25630832fa10d421328eed2b8e1a36af7a64)
---
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 af819163705..29bcb6557d5 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

View File

@ -0,0 +1,209 @@
From 9f95e17cc0a9a79da82157e34e3effe1bc395037 Mon Sep 17 00:00:00 2001
From: Jan Ehrhardt <github@ehrhardt.nl>
Date: Wed, 5 Jun 2024 20:44:46 +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 0d52941c5a1..0d3b54ed8b8 100644
--- a/sapi/cgi/cgi_main.c
+++ b/sapi/cgi/cgi_main.c
@@ -1798,8 +1798,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));
@@ -1809,6 +1814,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 00000000000..fd2fcdfbf89
--- /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 dc40d2d7960dd35f0178ff52c1f8590b7b1a08b2 Mon Sep 17 00:00:00 2001
From: Jan Ehrhardt <github@ehrhardt.nl>
Date: Sun, 9 Jun 2024 20:10:36 +0200
Subject: [PATCH 2/8] NEWS: Add backports from 8.1.29
---
NEWS | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/NEWS b/NEWS
index 7a9b6bdae18..79133f558af 100644
--- a/NEWS
+++ b/NEWS
@@ -3,10 +3,18 @@ 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)
+- Standard:
+ . Fixed bug GHSA-9fcc-425m-g385 (Bypass of CVE-2024-1874).
+ (CVE-2024-5585) (nielsdos)
+
Backported from 8.1.28
- Standard:
--
2.46.1
From 2d2552e092b6ff32cd823692d512f126ee629842 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)
---
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 0d3b54ed8b8..6e148874e4f 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)
@@ -1803,10 +1802,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 */
@@ -1815,22 +1819,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

View File

@ -0,0 +1,56 @@
From 8aa748ee0657cdee8d883ba50d04b68bc450f686 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)
---
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 6e148874e4f..5879d0e0f93 100644
--- a/sapi/cgi/cgi_main.c
+++ b/sapi/cgi/cgi_main.c
@@ -1910,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("<b>Security Alert!</b> The PHP CGI cannot be accessed directly.\n\n\
--
2.46.1

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,130 @@
From 9f367d847989b339c33369737daf573e30bab5f1 Mon Sep 17 00:00:00 2001
From: Niels Dossche <7771979+nielsdos@users.noreply.github.com>
Date: Thu, 26 Sep 2024 22:22:27 +0200
Subject: [PATCH 4/8] Fix GHSA-g665-fm4p-vhff: OOB access in ldap_escape
(cherry picked from commit f9ecf90070a11dad09ca7671a712f81cc2a7d52f)
---
ext/ldap/ldap.c | 20 ++++++++++++++--
ext/ldap/tests/GHSA-g665-fm4p-vhff-1.phpt | 28 ++++++++++++++++++++++
ext/ldap/tests/GHSA-g665-fm4p-vhff-2.phpt | 29 +++++++++++++++++++++++
3 files changed, 75 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 c4dfe0c5b07..6661310d055 100644
--- a/ext/ldap/ldap.c
+++ b/ext/ldap/ldap.c
@@ -3760,13 +3760,23 @@ static zend_string* php_ldap_do_escape(const zend_bool *map, const char *value,
zend_string *ret;
for (i = 0; i < valuelen; i++) {
- len += (map[(unsigned char) value[i]]) ? 3 : 1;
+ size_t addend = (map[(unsigned char) value[i]]) ? 3 : 1;
+ if (len > ZSTR_MAX_LEN - addend) {
+ return NULL;
+ }
+ len += addend;
}
/* Per RFC 4514, a leading and trailing space must be escaped */
if ((flags & PHP_LDAP_ESCAPE_DN) && (value[0] == ' ')) {
+ if (len > ZSTR_MAX_LEN - 2) {
+ return NULL;
+ }
len += 2;
}
if ((flags & PHP_LDAP_ESCAPE_DN) && ((valuelen > 1) && (value[valuelen - 1] == ' '))) {
+ if (len > ZSTR_MAX_LEN - 2) {
+ return NULL;
+ }
len += 2;
}
@@ -3833,7 +3843,13 @@ PHP_FUNCTION(ldap_escape)
php_ldap_escape_map_set_chars(map, ignores, ignoreslen, 0);
}
- RETURN_NEW_STR(php_ldap_do_escape(map, value, valuelen, flags));
+ zend_string *result = php_ldap_do_escape(map, value, valuelen, flags);
+ if (UNEXPECTED(!result)) {
+ zend_argument_value_error(1, "is too long");
+ RETURN_THROWS();
+ }
+
+ RETURN_NEW_STR(result);
}
#ifdef STR_TRANSLATION
diff --git a/ext/ldap/tests/GHSA-g665-fm4p-vhff-1.phpt b/ext/ldap/tests/GHSA-g665-fm4p-vhff-1.phpt
new file mode 100644
index 00000000000..8e2c4fb160d
--- /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 (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ ldap_escape(str_repeat("#", 1431655758).' ', "", LDAP_ESCAPE_DN);
+} catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+ldap_escape(): Argument #1 ($value) is too long
+ldap_escape(): Argument #1 ($value) is too long
diff --git a/ext/ldap/tests/GHSA-g665-fm4p-vhff-2.phpt b/ext/ldap/tests/GHSA-g665-fm4p-vhff-2.phpt
new file mode 100644
index 00000000000..a69597084be
--- /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 (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+// would allocate a string of length 2
+try {
+ ldap_escape(str_repeat("*", 1431655766), "", LDAP_ESCAPE_FILTER);
+} catch (ValueError $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

View File

@ -0,0 +1,177 @@
From 22f4d3504d7613ce78bb96aa53cbfe7d672fa036 Mon Sep 17 00:00:00 2001
From: Jakub Zelenka <bukka@php.net>
Date: Thu, 12 Sep 2024 13:11:11 +0100
Subject: [PATCH 6/8] Fix GHSA-865w-9rf3-2wh5: FPM: Logs from childrens may be
altered
(cherry picked from commit 1f8e16172c7961045c2b0f34ba7613e3f21cdee8)
---
sapi/fpm/fpm/fpm_stdio.c | 2 +-
.../log-bwp-msg-flush-split-sep-pos-end.phpt | 47 +++++++++++++++++++
...log-bwp-msg-flush-split-sep-pos-start.phpt | 47 +++++++++++++++++++
3 files changed, 95 insertions(+), 1 deletion(-)
create mode 100644 sapi/fpm/tests/log-bwp-msg-flush-split-sep-pos-end.phpt
create mode 100644 sapi/fpm/tests/log-bwp-msg-flush-split-sep-pos-start.phpt
diff --git a/sapi/fpm/fpm/fpm_stdio.c b/sapi/fpm/fpm/fpm_stdio.c
index d75f9158cda..7983d6217b2 100644
--- a/sapi/fpm/fpm/fpm_stdio.c
+++ b/sapi/fpm/fpm/fpm_stdio.c
@@ -228,7 +228,7 @@ stdio_read:
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 00000000000..52826320080
--- /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--
+<?php include "skipif.inc"; ?>
+--FILE--
+<?php
+
+require_once "tester.inc";
+
+$cfg = <<<EOT
+[global]
+error_log = {{FILE:LOG}}
+[unconfined]
+listen = {{ADDR}}
+pm = dynamic
+pm.max_children = 5
+pm.start_servers = 1
+pm.min_spare_servers = 1
+pm.max_spare_servers = 3
+catch_workers_output = yes
+decorate_workers_output = no
+EOT;
+
+$code = <<<EOT
+<?php
+file_put_contents('php://stderr', str_repeat('a', 1013) . "Quarkslab\0fscf\0Quarkslab");
+EOT;
+
+$tester = new FPM\Tester($cfg, $code);
+$tester->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--
+<?php
+require_once "tester.inc";
+FPM\Tester::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 00000000000..34905938553
--- /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--
+<?php include "skipif.inc"; ?>
+--FILE--
+<?php
+
+require_once "tester.inc";
+
+$cfg = <<<EOT
+[global]
+error_log = {{FILE:LOG}}
+[unconfined]
+listen = {{ADDR}}
+pm = dynamic
+pm.max_children = 5
+pm.start_servers = 1
+pm.min_spare_servers = 1
+pm.max_spare_servers = 3
+catch_workers_output = yes
+decorate_workers_output = no
+EOT;
+
+$code = <<<EOT
+<?php
+file_put_contents('php://stderr', str_repeat('a', 1009) . "Quarkslab\0fscf\0Quarkslab");
+EOT;
+
+$tester = new FPM\Tester($cfg, $code);
+$tester->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--
+<?php
+require_once "tester.inc";
+FPM\Tester::clean();
+?>
--
2.46.1
From af3fb385e7b328ab89db26ec712d89c7096f0743 Mon Sep 17 00:00:00 2001
From: Remi Collet <remi@remirepo.net>
Date: Thu, 26 Sep 2024 11:50:54 +0200
Subject: [PATCH 8/8] NEWS for 8.1.30 backports
---
NEWS | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
diff --git a/NEWS b/NEWS
index 79133f558af..bad0a719aae 100644
--- a/NEWS
+++ b/NEWS
@@ -1,6 +1,23 @@
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)
+
+- 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)
+
Backported from 8.1.29
- CGI:
--
2.46.1

View File

@ -0,0 +1,909 @@
From 4fec08542748c25573063ffc53ea89cd5de1edf0 Mon Sep 17 00:00:00 2001
From: Jakub Zelenka <bukka@php.net>
Date: Tue, 31 Dec 2024 18:57:02 +0100
Subject: [PATCH 01/11] Fix GHSA-ghsa-v8xr-gpvj-cx9g: http header folding
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This adds HTTP header folding support for HTTP wrapper response
headers.
Reviewed-by: Tim Düsterhus <tim@tideways-gmbh.com>
(cherry picked from commit d20b4c97a9f883b62b65b82d939c5af9a2028ef1)
---
ext/openssl/tests/ServerClientTestCase.inc | 65 +++-
ext/standard/http_fopen_wrapper.c | 343 ++++++++++++------
.../tests/http/ghsa-v8xr-gpvj-cx9g-001.phpt | 49 +++
.../tests/http/ghsa-v8xr-gpvj-cx9g-002.phpt | 51 +++
.../tests/http/ghsa-v8xr-gpvj-cx9g-003.phpt | 49 +++
.../tests/http/ghsa-v8xr-gpvj-cx9g-004.phpt | 48 +++
.../tests/http/ghsa-v8xr-gpvj-cx9g-005.phpt | 48 +++
.../tests/http/http_response_header_05.phpt | 30 --
8 files changed, 534 insertions(+), 149 deletions(-)
create mode 100644 ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-001.phpt
create mode 100644 ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-002.phpt
create mode 100644 ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-003.phpt
create mode 100644 ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-004.phpt
create mode 100644 ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-005.phpt
delete mode 100644 ext/standard/tests/http/http_response_header_05.phpt
diff --git a/ext/openssl/tests/ServerClientTestCase.inc b/ext/openssl/tests/ServerClientTestCase.inc
index 753366df6f4..61d45385b62 100644
--- a/ext/openssl/tests/ServerClientTestCase.inc
+++ b/ext/openssl/tests/ServerClientTestCase.inc
@@ -4,14 +4,19 @@ const WORKER_ARGV_VALUE = 'RUN_WORKER';
const WORKER_DEFAULT_NAME = 'server';
-function phpt_notify($worker = WORKER_DEFAULT_NAME)
+function phpt_notify(string $worker = WORKER_DEFAULT_NAME, string $message = ""): void
{
- ServerClientTestCase::getInstance()->notify($worker);
+ ServerClientTestCase::getInstance()->notify($worker, $message);
}
-function phpt_wait($worker = WORKER_DEFAULT_NAME, $timeout = null)
+function phpt_wait($worker = WORKER_DEFAULT_NAME, $timeout = null): ?string
{
- ServerClientTestCase::getInstance()->wait($worker, $timeout);
+ return ServerClientTestCase::getInstance()->wait($worker, $timeout);
+}
+
+function phpt_notify_server_start($server): void
+{
+ ServerClientTestCase::getInstance()->notify_server_start($server);
}
function phpt_has_sslv3() {
@@ -119,43 +124,73 @@ class ServerClientTestCase
eval($code);
}
- public function run($masterCode, $workerCode)
+ /**
+ * Run client and all workers
+ *
+ * @param string $clientCode The client PHP code
+ * @param string|array $workerCode
+ * @param bool $ephemeral Select whether automatic port selection and automatic awaiting is used
+ * @return void
+ * @throws Exception
+ */
+ public function run(string $clientCode, string|array $workerCode, bool $ephemeral = true): void
{
if (!is_array($workerCode)) {
$workerCode = [WORKER_DEFAULT_NAME => $workerCode];
}
- foreach ($workerCode as $worker => $code) {
+ reset($workerCode);
+ $code = current($workerCode);
+ $worker = key($workerCode);
+ while ($worker != null) {
$this->spawnWorkerProcess($worker, $this->stripPhpTagsFromCode($code));
+ $code = next($workerCode);
+ if ($ephemeral) {
+ $addr = trim($this->wait($worker));
+ if (empty($addr)) {
+ throw new \Exception("Failed server start");
+ }
+ if ($code === false) {
+ $clientCode = preg_replace('/{{\s*ADDR\s*}}/', $addr, $clientCode);
+ } else {
+ $code = preg_replace('/{{\s*ADDR\s*}}/', $addr, $code);
+ }
+ }
+ $worker = key($workerCode);
}
- eval($this->stripPhpTagsFromCode($masterCode));
+
+ eval($this->stripPhpTagsFromCode($clientCode));
foreach ($workerCode as $worker => $code) {
$this->cleanupWorkerProcess($worker);
}
}
- public function wait($worker, $timeout = null)
+ public function wait($worker, $timeout = null): ?string
{
$handle = $this->isWorker ? STDIN : $this->workerStdOut[$worker];
if ($timeout === null) {
- fgets($handle);
- return true;
+ return fgets($handle);
}
stream_set_blocking($handle, false);
$read = [$handle];
$result = stream_select($read, $write, $except, $timeout);
if (!$result) {
- return false;
+ return null;
}
- fgets($handle);
+ $result = fgets($handle);
stream_set_blocking($handle, true);
- return true;
+ return $result;
+ }
+
+ public function notify(string $worker, string $message = ""): void
+ {
+ fwrite($this->isWorker ? STDOUT : $this->workerStdIn[$worker], "$message\n");
}
- public function notify($worker)
+ public function notify_server_start($server): void
{
- fwrite($this->isWorker ? STDOUT : $this->workerStdIn[$worker], "\n");
+ echo stream_socket_get_name($server, false) . "\n";
}
}
diff --git a/ext/standard/http_fopen_wrapper.c b/ext/standard/http_fopen_wrapper.c
index 40e6f3dd4c3..bfc88a74545 100644
--- a/ext/standard/http_fopen_wrapper.c
+++ b/ext/standard/http_fopen_wrapper.c
@@ -114,6 +114,171 @@ static zend_bool check_has_header(const char *headers, const char *header) {
return 0;
}
+typedef struct _php_stream_http_response_header_info {
+ php_stream_filter *transfer_encoding;
+ size_t file_size;
+ bool follow_location;
+ char location[HTTP_HEADER_BLOCK_SIZE];
+} php_stream_http_response_header_info;
+
+static void php_stream_http_response_header_info_init(
+ php_stream_http_response_header_info *header_info)
+{
+ header_info->transfer_encoding = NULL;
+ header_info->file_size = 0;
+ header_info->follow_location = 1;
+ header_info->location[0] = '\0';
+}
+
+/* Trim white spaces from response header line and update its length */
+static bool php_stream_http_response_header_trim(char *http_header_line,
+ size_t *http_header_line_length)
+{
+ char *http_header_line_end = http_header_line + *http_header_line_length - 1;
+ while (http_header_line_end >= http_header_line &&
+ (*http_header_line_end == '\n' || *http_header_line_end == '\r')) {
+ http_header_line_end--;
+ }
+
+ /* The primary definition of an HTTP header in RFC 7230 states:
+ * > Each header field consists of a case-insensitive field name followed
+ * > by a colon (":"), optional leading whitespace, the field value, and
+ * > optional trailing whitespace. */
+
+ /* Strip trailing whitespace */
+ bool space_trim = (*http_header_line_end == ' ' || *http_header_line_end == '\t');
+ if (space_trim) {
+ do {
+ http_header_line_end--;
+ } while (http_header_line_end >= http_header_line &&
+ (*http_header_line_end == ' ' || *http_header_line_end == '\t'));
+ }
+ http_header_line_end++;
+ *http_header_line_end = '\0';
+ *http_header_line_length = http_header_line_end - http_header_line;
+
+ return space_trim;
+}
+
+/* Process folding headers of the current line and if there are none, parse last full response
+ * header line. It returns NULL if the last header is finished, otherwise it returns updated
+ * last header line. */
+static zend_string *php_stream_http_response_headers_parse(php_stream *stream,
+ php_stream_context *context, int options, zend_string *last_header_line_str,
+ char *header_line, size_t *header_line_length, int response_code,
+ zval *response_header, php_stream_http_response_header_info *header_info)
+{
+ char *last_header_line = ZSTR_VAL(last_header_line_str);
+ size_t last_header_line_length = ZSTR_LEN(last_header_line_str);
+ char *last_header_line_end = ZSTR_VAL(last_header_line_str) + ZSTR_LEN(last_header_line_str) - 1;
+
+ /* Process non empty header line. */
+ if (header_line && (*header_line != '\n' && *header_line != '\r')) {
+ /* Removing trailing white spaces. */
+ if (php_stream_http_response_header_trim(header_line, header_line_length) &&
+ *header_line_length == 0) {
+ /* Only spaces so treat as an empty folding header. */
+ return last_header_line_str;
+ }
+
+ /* Process folding headers if starting with a space or a tab. */
+ if (header_line && (*header_line == ' ' || *header_line == '\t')) {
+ char *http_folded_header_line = header_line;
+ size_t http_folded_header_line_length = *header_line_length;
+ /* Remove the leading white spaces. */
+ while (*http_folded_header_line == ' ' || *http_folded_header_line == '\t') {
+ http_folded_header_line++;
+ http_folded_header_line_length--;
+ }
+ /* It has to have some characters because it would get returned after the call
+ * php_stream_http_response_header_trim above. */
+ ZEND_ASSERT(http_folded_header_line_length > 0);
+ /* Concatenate last header line, space and current header line. */
+ zend_string *extended_header_str = zend_string_concat3(
+ last_header_line, last_header_line_length,
+ " ", 1,
+ http_folded_header_line, http_folded_header_line_length);
+ zend_string_efree(last_header_line_str);
+ last_header_line_str = extended_header_str;
+ /* Return new header line. */
+ return last_header_line_str;
+ }
+ }
+
+ /* Find header separator position. */
+ char *last_header_value = memchr(last_header_line, ':', last_header_line_length);
+ if (last_header_value) {
+ last_header_value++; /* Skip ':'. */
+
+ /* Strip leading whitespace. */
+ while (last_header_value < last_header_line_end
+ && (*last_header_value == ' ' || *last_header_value == '\t')) {
+ last_header_value++;
+ }
+ } else {
+ /* There is no colon. Set the value to the end of the header line, which is effectively
+ * an empty string. */
+ last_header_value = last_header_line_end;
+ }
+
+ bool store_header = true;
+ zval *tmpzval = NULL;
+
+ if (!strncasecmp(last_header_line, "Location:", sizeof("Location:")-1)) {
+ /* Check if the location should be followed. */
+ if (context && (tmpzval = php_stream_context_get_option(context, "http", "follow_location")) != NULL) {
+ header_info->follow_location = zval_is_true(tmpzval);
+ } else if (!((response_code >= 300 && response_code < 304)
+ || 307 == response_code || 308 == response_code)) {
+ /* The redirection should not be automatic if follow_location is not set and
+ * response_code not in (300, 301, 302, 303 and 307)
+ * see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.1
+ * RFC 7238 defines 308: http://tools.ietf.org/html/rfc7238 */
+ header_info->follow_location = 0;
+ }
+ strlcpy(header_info->location, last_header_value, sizeof(header_info->location));
+ } else if (!strncasecmp(last_header_line, "Content-Type:", sizeof("Content-Type:")-1)) {
+ php_stream_notify_info(context, PHP_STREAM_NOTIFY_MIME_TYPE_IS, last_header_value, 0);
+ } else if (!strncasecmp(last_header_line, "Content-Length:", sizeof("Content-Length:")-1)) {
+ header_info->file_size = atoi(last_header_value);
+ php_stream_notify_file_size(context, header_info->file_size, last_header_line, 0);
+ } else if (
+ !strncasecmp(last_header_line, "Transfer-Encoding:", sizeof("Transfer-Encoding:")-1)
+ && !strncasecmp(last_header_value, "Chunked", sizeof("Chunked")-1)
+ ) {
+ /* Create filter to decode response body. */
+ if (!(options & STREAM_ONLY_GET_HEADERS)) {
+ zend_long decode = 1;
+
+ if (context && (tmpzval = php_stream_context_get_option(context, "http", "auto_decode")) != NULL) {
+ decode = zend_is_true(tmpzval);
+ }
+ if (decode) {
+ if (header_info->transfer_encoding != NULL) {
+ /* Prevent a memory leak in case there are more transfer-encoding headers. */
+ php_stream_filter_free(header_info->transfer_encoding);
+ }
+ header_info->transfer_encoding = php_stream_filter_create(
+ "dechunk", NULL, php_stream_is_persistent(stream));
+ if (header_info->transfer_encoding != NULL) {
+ /* Do not store transfer-encoding header. */
+ store_header = false;
+ }
+ }
+ }
+ }
+
+ if (store_header) {
+ zval http_header;
+ ZVAL_NEW_STR(&http_header, last_header_line_str);
+ zend_hash_next_index_insert(Z_ARRVAL_P(response_header), &http_header);
+ } else {
+ zend_string_efree(last_header_line_str);
+ }
+
+ return NULL;
+}
+
static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper,
const char *path, const char *mode, int options, zend_string **opened_path,
php_stream_context *context, int redirect_max, int flags,
@@ -126,11 +291,12 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper,
zend_string *tmp = NULL;
char *ua_str = NULL;
zval *ua_zval = NULL, *tmpzval = NULL, ssl_proxy_peer_name;
- char location[HTTP_HEADER_BLOCK_SIZE];
int reqok = 0;
char *http_header_line = NULL;
+ zend_string *last_header_line_str = NULL;
+ php_stream_http_response_header_info header_info;
char tmp_line[128];
- size_t chunk_size = 0, file_size = 0;
+ size_t chunk_size = 0;
int eol_detect = 0;
char *transport_string;
zend_string *errstr = NULL;
@@ -141,8 +307,6 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper,
char *user_headers = NULL;
int header_init = ((flags & HTTP_WRAPPER_HEADER_INIT) != 0);
int redirected = ((flags & HTTP_WRAPPER_REDIRECTED) != 0);
- zend_bool follow_location = 1;
- php_stream_filter *transfer_encoding = NULL;
int response_code;
smart_str req_buf = {0};
zend_bool custom_request_method;
@@ -655,8 +819,6 @@ finish:
/* send it */
php_stream_write(stream, ZSTR_VAL(req_buf.s), ZSTR_LEN(req_buf.s));
- location[0] = '\0';
-
if (Z_ISUNDEF_P(response_header)) {
array_init(response_header);
}
@@ -738,130 +900,101 @@ finish:
}
}
- /* read past HTTP headers */
+ php_stream_http_response_header_info_init(&header_info);
+ /* read past HTTP headers */
while (!php_stream_eof(stream)) {
size_t http_header_line_length;
if (http_header_line != NULL) {
efree(http_header_line);
}
- if ((http_header_line = php_stream_get_line(stream, NULL, 0, &http_header_line_length)) && *http_header_line != '\n' && *http_header_line != '\r') {
- char *e = http_header_line + http_header_line_length - 1;
- char *http_header_value;
-
- while (e >= http_header_line && (*e == '\n' || *e == '\r')) {
- e--;
- }
-
- /* The primary definition of an HTTP header in RFC 7230 states:
- * > Each header field consists of a case-insensitive field name followed
- * > by a colon (":"), optional leading whitespace, the field value, and
- * > optional trailing whitespace. */
-
- /* Strip trailing whitespace */
- while (e >= http_header_line && (*e == ' ' || *e == '\t')) {
- e--;
- }
-
- /* Terminate header line */
- e++;
- *e = '\0';
- http_header_line_length = e - http_header_line;
-
- http_header_value = memchr(http_header_line, ':', http_header_line_length);
- if (http_header_value) {
- http_header_value++; /* Skip ':' */
-
- /* Strip leading whitespace */
- while (http_header_value < e
- && (*http_header_value == ' ' || *http_header_value == '\t')) {
- http_header_value++;
+ if ((http_header_line = php_stream_get_line(stream, NULL, 0, &http_header_line_length))) {
+ bool last_line;
+ if (*http_header_line == '\r') {
+ if (http_header_line[1] != '\n') {
+ php_stream_close(stream);
+ stream = NULL;
+ php_stream_wrapper_log_error(wrapper, options,
+ "HTTP invalid header name (cannot start with CR character)!");
+ goto out;
}
+ last_line = true;
+ } else if (*http_header_line == '\n') {
+ last_line = true;
} else {
- /* There is no colon. Set the value to the end of the header line, which is
- * effectively an empty string. */
- http_header_value = e;
+ last_line = false;
}
-
- if (!strncasecmp(http_header_line, "Location:", sizeof("Location:")-1)) {
- if (context && (tmpzval = php_stream_context_get_option(context, "http", "follow_location")) != NULL) {
- follow_location = zval_is_true(tmpzval);
- } else if (!((response_code >= 300 && response_code < 304)
- || 307 == response_code || 308 == response_code)) {
- /* we shouldn't redirect automatically
- if follow_location isn't set and response_code not in (300, 301, 302, 303 and 307)
- see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.1
- RFC 7238 defines 308: http://tools.ietf.org/html/rfc7238 */
- follow_location = 0;
+
+ if (last_header_line_str != NULL) {
+ /* Parse last header line. */
+ last_header_line_str = php_stream_http_response_headers_parse(stream, context,
+ options, last_header_line_str, http_header_line, &http_header_line_length,
+ response_code, response_header, &header_info);
+ if (last_header_line_str != NULL) {
+ /* Folding header present so continue. */
+ continue;
}
- strlcpy(location, http_header_value, sizeof(location));
- } else if (!strncasecmp(http_header_line, "Content-Type:", sizeof("Content-Type:")-1)) {
- php_stream_notify_info(context, PHP_STREAM_NOTIFY_MIME_TYPE_IS, http_header_value, 0);
- } else if (!strncasecmp(http_header_line, "Content-Length:", sizeof("Content-Length:")-1)) {
- file_size = atoi(http_header_value);
- php_stream_notify_file_size(context, file_size, http_header_line, 0);
- } else if (
- !strncasecmp(http_header_line, "Transfer-Encoding:", sizeof("Transfer-Encoding:")-1)
- && !strncasecmp(http_header_value, "Chunked", sizeof("Chunked")-1)
- ) {
-
- /* create filter to decode response body */
- if (!(options & STREAM_ONLY_GET_HEADERS)) {
- zend_long decode = 1;
-
- if (context && (tmpzval = php_stream_context_get_option(context, "http", "auto_decode")) != NULL) {
- decode = zend_is_true(tmpzval);
- }
- if (decode) {
- transfer_encoding = php_stream_filter_create("dechunk", NULL, php_stream_is_persistent(stream));
- if (transfer_encoding) {
- /* don't store transfer-encodeing header */
- continue;
- }
- }
+ } else if (!last_line) {
+ /* The first line cannot start with spaces. */
+ if (*http_header_line == ' ' || *http_header_line == '\t') {
+ php_stream_close(stream);
+ stream = NULL;
+ php_stream_wrapper_log_error(wrapper, options,
+ "HTTP invalid response format (folding header at the start)!");
+ goto out;
}
+ /* Trim the first line if it is not the last line. */
+ php_stream_http_response_header_trim(http_header_line, &http_header_line_length);
}
-
- {
- zval http_header;
- ZVAL_STRINGL(&http_header, http_header_line, http_header_line_length);
- zend_hash_next_index_insert(Z_ARRVAL_P(response_header), &http_header);
+ if (last_line) {
+ /* For the last line the last header line must be NULL. */
+ ZEND_ASSERT(last_header_line_str == NULL);
+ break;
}
+ /* Save current line as the last line so it gets parsed in the next round. */
+ last_header_line_str = zend_string_init(http_header_line, http_header_line_length, 0);
} else {
break;
}
}
- if (!reqok || (location[0] != '\0' && follow_location)) {
- if (!follow_location || (((options & STREAM_ONLY_GET_HEADERS) || ignore_errors) && redirect_max <= 1)) {
+ /* If the stream was closed early, we still want to process the last line to keep BC. */
+ if (last_header_line_str != NULL) {
+ php_stream_http_response_headers_parse(stream, context, options, last_header_line_str,
+ NULL, NULL, response_code, response_header, &header_info);
+ }
+
+ if (!reqok || (header_info.location[0] != '\0' && header_info.follow_location)) {
+ if (!header_info.follow_location || (((options & STREAM_ONLY_GET_HEADERS) || ignore_errors) && redirect_max <= 1)) {
goto out;
}
- if (location[0] != '\0')
- php_stream_notify_info(context, PHP_STREAM_NOTIFY_REDIRECTED, location, 0);
+ if (header_info.location[0] != '\0')
+ php_stream_notify_info(context, PHP_STREAM_NOTIFY_REDIRECTED, header_info.location, 0);
php_stream_close(stream);
stream = NULL;
- if (transfer_encoding) {
- php_stream_filter_free(transfer_encoding);
- transfer_encoding = NULL;
+ if (header_info.transfer_encoding) {
+ php_stream_filter_free(header_info.transfer_encoding);
+ header_info.transfer_encoding = NULL;
}
- if (location[0] != '\0') {
+ if (header_info.location[0] != '\0') {
char new_path[HTTP_HEADER_BLOCK_SIZE];
char loc_path[HTTP_HEADER_BLOCK_SIZE];
*new_path='\0';
- if (strlen(location)<8 || (strncasecmp(location, "http://", sizeof("http://")-1) &&
- strncasecmp(location, "https://", sizeof("https://")-1) &&
- strncasecmp(location, "ftp://", sizeof("ftp://")-1) &&
- strncasecmp(location, "ftps://", sizeof("ftps://")-1)))
+ if (strlen(header_info.location) < 8 ||
+ (strncasecmp(header_info.location, "http://", sizeof("http://")-1) &&
+ strncasecmp(header_info.location, "https://", sizeof("https://")-1) &&
+ strncasecmp(header_info.location, "ftp://", sizeof("ftp://")-1) &&
+ strncasecmp(header_info.location, "ftps://", sizeof("ftps://")-1)))
{
- if (*location != '/') {
- if (*(location+1) != '\0' && resource->path) {
+ if (*header_info.location != '/') {
+ if (*(header_info.location+1) != '\0' && resource->path) {
char *s = strrchr(ZSTR_VAL(resource->path), '/');
if (!s) {
s = ZSTR_VAL(resource->path);
@@ -877,15 +1010,17 @@ finish:
if (resource->path &&
ZSTR_VAL(resource->path)[0] == '/' &&
ZSTR_VAL(resource->path)[1] == '\0') {
- snprintf(loc_path, sizeof(loc_path) - 1, "%s%s", ZSTR_VAL(resource->path), location);
+ snprintf(loc_path, sizeof(loc_path) - 1, "%s%s",
+ ZSTR_VAL(resource->path), header_info.location);
} else {
- snprintf(loc_path, sizeof(loc_path) - 1, "%s/%s", ZSTR_VAL(resource->path), location);
+ snprintf(loc_path, sizeof(loc_path) - 1, "%s/%s",
+ ZSTR_VAL(resource->path), header_info.location);
}
} else {
- snprintf(loc_path, sizeof(loc_path) - 1, "/%s", location);
+ snprintf(loc_path, sizeof(loc_path) - 1, "/%s", header_info.location);
}
} else {
- strlcpy(loc_path, location, sizeof(loc_path));
+ strlcpy(loc_path, header_info.location, sizeof(loc_path));
}
if ((use_ssl && resource->port != 443) || (!use_ssl && resource->port != 80)) {
snprintf(new_path, sizeof(new_path) - 1, "%s://%s:%d%s", ZSTR_VAL(resource->scheme), ZSTR_VAL(resource->host), resource->port, loc_path);
@@ -893,7 +1028,7 @@ finish:
snprintf(new_path, sizeof(new_path) - 1, "%s://%s%s", ZSTR_VAL(resource->scheme), ZSTR_VAL(resource->host), loc_path);
}
} else {
- strlcpy(new_path, location, sizeof(new_path));
+ strlcpy(new_path, header_info.location, sizeof(new_path));
}
php_url_free(resource);
@@ -946,7 +1081,7 @@ out:
if (header_init) {
ZVAL_COPY(&stream->wrapperdata, response_header);
}
- php_stream_notify_progress_init(context, 0, file_size);
+ php_stream_notify_progress_init(context, 0, header_info.file_size);
/* Restore original chunk size now that we're done with headers */
if (options & STREAM_WILL_CAST)
@@ -962,8 +1097,8 @@ out:
/* restore mode */
strlcpy(stream->mode, mode, sizeof(stream->mode));
- if (transfer_encoding) {
- php_stream_filter_append(&stream->readfilters, transfer_encoding);
+ if (header_info.transfer_encoding) {
+ php_stream_filter_append(&stream->readfilters, header_info.transfer_encoding);
}
}
diff --git a/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-001.phpt b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-001.phpt
new file mode 100644
index 00000000000..f935b5a02ca
--- /dev/null
+++ b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-001.phpt
@@ -0,0 +1,49 @@
+--TEST--
+GHSA-v8xr-gpvj-cx9g: Header parser of http stream wrapper does not handle folded headers (single)
+--FILE--
+<?php
+$serverCode = <<<'CODE'
+ $ctxt = stream_context_create([
+ "socket" => [
+ "tcp_nodelay" => true
+ ]
+ ]);
+
+ $server = stream_socket_server(
+ "tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt);
+ phpt_notify_server_start($server);
+
+ $conn = stream_socket_accept($server);
+
+ phpt_notify(message:"server-accepted");
+
+ fwrite($conn, "HTTP/1.0 200 Ok\r\nContent-Type: text/html;\r\n charset=utf-8\r\n\r\nbody\r\n");
+CODE;
+
+$clientCode = <<<'CODE'
+ function stream_notification_callback($notification_code, $severity, $message, $message_code, $bytes_transferred, $bytes_max) {
+ switch($notification_code) {
+ case STREAM_NOTIFY_MIME_TYPE_IS:
+ echo "Found the mime-type: ", $message, PHP_EOL;
+ break;
+ }
+ }
+
+ $ctx = stream_context_create();
+ stream_context_set_params($ctx, array("notification" => "stream_notification_callback"));
+ var_dump(trim(file_get_contents("http://{{ ADDR }}", false, $ctx)));
+ var_dump($http_response_header);
+CODE;
+
+include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__);
+ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
+?>
+--EXPECTF--
+Found the mime-type: text/html; charset=utf-8
+string(4) "body"
+array(2) {
+ [0]=>
+ string(15) "HTTP/1.0 200 Ok"
+ [1]=>
+ string(38) "Content-Type: text/html; charset=utf-8"
+}
diff --git a/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-002.phpt b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-002.phpt
new file mode 100644
index 00000000000..078d605b671
--- /dev/null
+++ b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-002.phpt
@@ -0,0 +1,51 @@
+--TEST--
+GHSA-v8xr-gpvj-cx9g: Header parser of http stream wrapper does not handle folded headers (multiple)
+--FILE--
+<?php
+$serverCode = <<<'CODE'
+ $ctxt = stream_context_create([
+ "socket" => [
+ "tcp_nodelay" => true
+ ]
+ ]);
+
+ $server = stream_socket_server(
+ "tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt);
+ phpt_notify_server_start($server);
+
+ $conn = stream_socket_accept($server);
+
+ phpt_notify(message:"server-accepted");
+
+ fwrite($conn, "HTTP/1.0 200 Ok\r\nContent-Type: text/html;\r\nCustom-Header: somevalue;\r\n param1=value1; \r\n param2=value2\r\n\r\nbody\r\n");
+CODE;
+
+$clientCode = <<<'CODE'
+ function stream_notification_callback($notification_code, $severity, $message, $message_code, $bytes_transferred, $bytes_max) {
+ switch($notification_code) {
+ case STREAM_NOTIFY_MIME_TYPE_IS:
+ echo "Found the mime-type: ", $message, PHP_EOL;
+ break;
+ }
+ }
+
+ $ctx = stream_context_create();
+ stream_context_set_params($ctx, array("notification" => "stream_notification_callback"));
+ var_dump(trim(file_get_contents("http://{{ ADDR }}", false, $ctx)));
+ var_dump($http_response_header);
+CODE;
+
+include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__);
+ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
+?>
+--EXPECTF--
+Found the mime-type: text/html;
+string(4) "body"
+array(3) {
+ [0]=>
+ string(15) "HTTP/1.0 200 Ok"
+ [1]=>
+ string(24) "Content-Type: text/html;"
+ [2]=>
+ string(54) "Custom-Header: somevalue; param1=value1; param2=value2"
+}
diff --git a/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-003.phpt b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-003.phpt
new file mode 100644
index 00000000000..ad5ddc879ce
--- /dev/null
+++ b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-003.phpt
@@ -0,0 +1,49 @@
+--TEST--
+GHSA-v8xr-gpvj-cx9g: Header parser of http stream wrapper does not handle folded headers (empty)
+--FILE--
+<?php
+$serverCode = <<<'CODE'
+ $ctxt = stream_context_create([
+ "socket" => [
+ "tcp_nodelay" => true
+ ]
+ ]);
+
+ $server = stream_socket_server(
+ "tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt);
+ phpt_notify_server_start($server);
+
+ $conn = stream_socket_accept($server);
+
+ phpt_notify(message:"server-accepted");
+
+ fwrite($conn, "HTTP/1.0 200 Ok\r\nContent-Type: text/html;\r\n \r\n charset=utf-8\r\n\r\nbody\r\n");
+CODE;
+
+$clientCode = <<<'CODE'
+ function stream_notification_callback($notification_code, $severity, $message, $message_code, $bytes_transferred, $bytes_max) {
+ switch($notification_code) {
+ case STREAM_NOTIFY_MIME_TYPE_IS:
+ echo "Found the mime-type: ", $message, PHP_EOL;
+ break;
+ }
+ }
+
+ $ctx = stream_context_create();
+ stream_context_set_params($ctx, array("notification" => "stream_notification_callback"));
+ var_dump(trim(file_get_contents("http://{{ ADDR }}", false, $ctx)));
+ var_dump($http_response_header);
+CODE;
+
+include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__);
+ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
+?>
+--EXPECTF--
+Found the mime-type: text/html; charset=utf-8
+string(4) "body"
+array(2) {
+ [0]=>
+ string(15) "HTTP/1.0 200 Ok"
+ [1]=>
+ string(38) "Content-Type: text/html; charset=utf-8"
+}
diff --git a/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-004.phpt b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-004.phpt
new file mode 100644
index 00000000000..d0396e819fb
--- /dev/null
+++ b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-004.phpt
@@ -0,0 +1,48 @@
+--TEST--
+GHSA-v8xr-gpvj-cx9g: Header parser of http stream wrapper does not handle folded headers (first line)
+--FILE--
+<?php
+$serverCode = <<<'CODE'
+ $ctxt = stream_context_create([
+ "socket" => [
+ "tcp_nodelay" => true
+ ]
+ ]);
+
+ $server = stream_socket_server(
+ "tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt);
+ phpt_notify_server_start($server);
+
+ $conn = stream_socket_accept($server);
+
+ phpt_notify(message:"server-accepted");
+
+ fwrite($conn, "HTTP/1.0 200 Ok\r\n Content-Type: text/html;\r\n \r\n charset=utf-8\r\n\r\nbody\r\n");
+CODE;
+
+$clientCode = <<<'CODE'
+ function stream_notification_callback($notification_code, $severity, $message, $message_code, $bytes_transferred, $bytes_max) {
+ switch($notification_code) {
+ case STREAM_NOTIFY_MIME_TYPE_IS:
+ echo "Found the mime-type: ", $message, PHP_EOL;
+ break;
+ }
+ }
+
+ $ctx = stream_context_create();
+ stream_context_set_params($ctx, array("notification" => "stream_notification_callback"));
+ var_dump(file_get_contents("http://{{ ADDR }}", false, $ctx));
+ var_dump($http_response_header);
+CODE;
+
+include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__);
+ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
+?>
+--EXPECTF--
+
+Warning: file_get_contents(http://127.0.0.1:%d): Failed to open stream: HTTP invalid response format (folding header at the start)! in %s
+bool(false)
+array(1) {
+ [0]=>
+ string(15) "HTTP/1.0 200 Ok"
+}
diff --git a/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-005.phpt b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-005.phpt
new file mode 100644
index 00000000000..037d2002cc5
--- /dev/null
+++ b/ext/standard/tests/http/ghsa-v8xr-gpvj-cx9g-005.phpt
@@ -0,0 +1,48 @@
+--TEST--
+GHSA-v8xr-gpvj-cx9g: Header parser of http stream wrapper does not handle folded headers (CR before header name)
+--FILE--
+<?php
+$serverCode = <<<'CODE'
+ $ctxt = stream_context_create([
+ "socket" => [
+ "tcp_nodelay" => true
+ ]
+ ]);
+
+ $server = stream_socket_server(
+ "tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt);
+ phpt_notify_server_start($server);
+
+ $conn = stream_socket_accept($server);
+
+ phpt_notify(message:"server-accepted");
+
+ fwrite($conn, "HTTP/1.0 200 Ok\r\n\rIgnored: ignored\r\n\r\nbody\r\n");
+CODE;
+
+$clientCode = <<<'CODE'
+ function stream_notification_callback($notification_code, $severity, $message, $message_code, $bytes_transferred, $bytes_max) {
+ switch($notification_code) {
+ case STREAM_NOTIFY_MIME_TYPE_IS:
+ echo "Found the mime-type: ", $message, PHP_EOL;
+ break;
+ }
+ }
+
+ $ctx = stream_context_create();
+ stream_context_set_params($ctx, array("notification" => "stream_notification_callback"));
+ var_dump(file_get_contents("http://{{ ADDR }}", false, $ctx));
+ var_dump($http_response_header);
+CODE;
+
+include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__);
+ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
+?>
+--EXPECTF--
+
+Warning: file_get_contents(http://127.0.0.1:%d): Failed to open stream: HTTP invalid header name (cannot start with CR character)! in %s
+bool(false)
+array(1) {
+ [0]=>
+ string(15) "HTTP/1.0 200 Ok"
+}
diff --git a/ext/standard/tests/http/http_response_header_05.phpt b/ext/standard/tests/http/http_response_header_05.phpt
deleted file mode 100644
index c5fe60fa612..00000000000
--- a/ext/standard/tests/http/http_response_header_05.phpt
+++ /dev/null
@@ -1,30 +0,0 @@
---TEST--
-$http_reponse_header (whitespace-only "header")
---SKIPIF--
-<?php require 'server.inc'; http_server_skipif(); ?>
---INI--
-allow_url_fopen=1
---FILE--
-<?php
-require 'server.inc';
-
-$responses = array(
- "data://text/plain,HTTP/1.0 200 Ok\r\n \r\n\r\nBody",
-);
-
-['pid' => $pid, 'uri' => $uri] = http_server($responses, $output);
-
-$f = file_get_contents($uri);
-var_dump($f);
-var_dump($http_response_header);
-
-http_server_kill($pid);
-
---EXPECT--
-string(4) "Body"
-array(2) {
- [0]=>
- string(15) "HTTP/1.0 200 Ok"
- [1]=>
- string(0) ""
-}
--
2.48.1

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,300 @@
From e81d0cd14bfeb17e899c73e3aece4991bbda76af Mon Sep 17 00:00:00 2001
From: Jakub Zelenka <bukka@php.net>
Date: Sun, 19 Jan 2025 17:49:53 +0100
Subject: [PATCH 02/11] Fix GHSA-pcmh-g36c-qc44: http headers without colon
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The header line must contain colon otherwise it is invalid and it needs
to fail.
Reviewed-by: Tim Düsterhus <tim@tideways-gmbh.com>
(cherry picked from commit 0548c4c1756724a89ef8310709419b08aadb2b3b)
---
ext/standard/http_fopen_wrapper.c | 51 ++++++++++++++-----
ext/standard/tests/http/bug47021.phpt | 22 ++++----
ext/standard/tests/http/bug75535.phpt | 4 +-
.../tests/http/ghsa-pcmh-g36c-qc44-001.phpt | 51 +++++++++++++++++++
.../tests/http/ghsa-pcmh-g36c-qc44-002.phpt | 51 +++++++++++++++++++
5 files changed, 154 insertions(+), 25 deletions(-)
create mode 100644 ext/standard/tests/http/ghsa-pcmh-g36c-qc44-001.phpt
create mode 100644 ext/standard/tests/http/ghsa-pcmh-g36c-qc44-002.phpt
diff --git a/ext/standard/http_fopen_wrapper.c b/ext/standard/http_fopen_wrapper.c
index bfc88a74545..7ee22b85f88 100644
--- a/ext/standard/http_fopen_wrapper.c
+++ b/ext/standard/http_fopen_wrapper.c
@@ -117,6 +117,7 @@ static zend_bool check_has_header(const char *headers, const char *header) {
typedef struct _php_stream_http_response_header_info {
php_stream_filter *transfer_encoding;
size_t file_size;
+ bool error;
bool follow_location;
char location[HTTP_HEADER_BLOCK_SIZE];
} php_stream_http_response_header_info;
@@ -126,6 +127,7 @@ static void php_stream_http_response_header_info_init(
{
header_info->transfer_encoding = NULL;
header_info->file_size = 0;
+ header_info->error = false;
header_info->follow_location = 1;
header_info->location[0] = '\0';
}
@@ -163,10 +165,11 @@ static bool php_stream_http_response_header_trim(char *http_header_line,
/* Process folding headers of the current line and if there are none, parse last full response
* header line. It returns NULL if the last header is finished, otherwise it returns updated
* last header line. */
-static zend_string *php_stream_http_response_headers_parse(php_stream *stream,
- php_stream_context *context, int options, zend_string *last_header_line_str,
- char *header_line, size_t *header_line_length, int response_code,
- zval *response_header, php_stream_http_response_header_info *header_info)
+static zend_string *php_stream_http_response_headers_parse(php_stream_wrapper *wrapper,
+ php_stream *stream, php_stream_context *context, int options,
+ zend_string *last_header_line_str, char *header_line, size_t *header_line_length,
+ int response_code, zval *response_header,
+ php_stream_http_response_header_info *header_info)
{
char *last_header_line = ZSTR_VAL(last_header_line_str);
size_t last_header_line_length = ZSTR_LEN(last_header_line_str);
@@ -208,6 +211,19 @@ static zend_string *php_stream_http_response_headers_parse(php_stream *stream,
/* Find header separator position. */
char *last_header_value = memchr(last_header_line, ':', last_header_line_length);
if (last_header_value) {
+ /* Verify there is no space in header name */
+ char *last_header_name = last_header_line + 1;
+ while (last_header_name < last_header_value) {
+ if (*last_header_name == ' ' || *last_header_name == '\t') {
+ header_info->error = true;
+ php_stream_wrapper_log_error(wrapper, options,
+ "HTTP invalid response format (space in header name)!");
+ zend_string_efree(last_header_line_str);
+ return NULL;
+ }
+ ++last_header_name;
+ }
+
last_header_value++; /* Skip ':'. */
/* Strip leading whitespace. */
@@ -216,9 +232,12 @@ static zend_string *php_stream_http_response_headers_parse(php_stream *stream,
last_header_value++;
}
} else {
- /* There is no colon. Set the value to the end of the header line, which is effectively
- * an empty string. */
- last_header_value = last_header_line_end;
+ /* There is no colon which means invalid response so error. */
+ header_info->error = true;
+ php_stream_wrapper_log_error(wrapper, options,
+ "HTTP invalid response format (no colon in header line)!");
+ zend_string_efree(last_header_line_str);
+ return NULL;
}
bool store_header = true;
@@ -928,10 +947,16 @@ finish:
if (last_header_line_str != NULL) {
/* Parse last header line. */
- last_header_line_str = php_stream_http_response_headers_parse(stream, context,
- options, last_header_line_str, http_header_line, &http_header_line_length,
- response_code, response_header, &header_info);
- if (last_header_line_str != NULL) {
+ last_header_line_str = php_stream_http_response_headers_parse(wrapper, stream,
+ context, options, last_header_line_str, http_header_line,
+ &http_header_line_length, response_code, response_header, &header_info);
+ if (EXPECTED(last_header_line_str == NULL)) {
+ if (UNEXPECTED(header_info.error)) {
+ php_stream_close(stream);
+ stream = NULL;
+ goto out;
+ }
+ } else {
/* Folding header present so continue. */
continue;
}
@@ -961,8 +986,8 @@ finish:
/* If the stream was closed early, we still want to process the last line to keep BC. */
if (last_header_line_str != NULL) {
- php_stream_http_response_headers_parse(stream, context, options, last_header_line_str,
- NULL, NULL, response_code, response_header, &header_info);
+ php_stream_http_response_headers_parse(wrapper, stream, context, options,
+ last_header_line_str, NULL, NULL, response_code, response_header, &header_info);
}
if (!reqok || (header_info.location[0] != '\0' && header_info.follow_location)) {
diff --git a/ext/standard/tests/http/bug47021.phpt b/ext/standard/tests/http/bug47021.phpt
index 326eceb687a..168721f4ec1 100644
--- a/ext/standard/tests/http/bug47021.phpt
+++ b/ext/standard/tests/http/bug47021.phpt
@@ -70,23 +70,27 @@ do_test(1, true);
echo "\n";
?>
---EXPECT--
+--EXPECTF--
+
Type='text/plain'
Hello
-Size=5
-World
+
+Warning: file_get_contents(http://%s:%d): Failed to open stream: HTTP invalid response format (no colon in header line)! in %s
+
Type='text/plain'
Hello
-Size=5
-World
+
+Warning: file_get_contents(http://%s:%d): Failed to open stream: HTTP invalid response format (no colon in header line)! in %s
+
Type='text/plain'
Hello
-Size=5
-World
+
+Warning: file_get_contents(http://%s:%d): Failed to open stream: HTTP invalid response format (no colon in header line)! in %s
+
Type='text/plain'
Hello
-Size=5
-World
+
+Warning: file_get_contents(http://%s:%d): Failed to open stream: HTTP invalid response format (no colon in header line)! in %s
diff --git a/ext/standard/tests/http/bug75535.phpt b/ext/standard/tests/http/bug75535.phpt
index 7b015890d2f..94348d1a027 100644
--- a/ext/standard/tests/http/bug75535.phpt
+++ b/ext/standard/tests/http/bug75535.phpt
@@ -21,9 +21,7 @@ http_server_kill($pid);
--EXPECT--
string(0) ""
-array(2) {
+array(1) {
[0]=>
string(15) "HTTP/1.0 200 Ok"
- [1]=>
- string(14) "Content-Length"
}
diff --git a/ext/standard/tests/http/ghsa-pcmh-g36c-qc44-001.phpt b/ext/standard/tests/http/ghsa-pcmh-g36c-qc44-001.phpt
new file mode 100644
index 00000000000..bb7945ce62d
--- /dev/null
+++ b/ext/standard/tests/http/ghsa-pcmh-g36c-qc44-001.phpt
@@ -0,0 +1,51 @@
+--TEST--
+GHSA-pcmh-g36c-qc44: Header parser of http stream wrapper does not verify header name and colon (colon)
+--FILE--
+<?php
+$serverCode = <<<'CODE'
+ $ctxt = stream_context_create([
+ "socket" => [
+ "tcp_nodelay" => true
+ ]
+ ]);
+
+ $server = stream_socket_server(
+ "tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt);
+ phpt_notify_server_start($server);
+
+ $conn = stream_socket_accept($server);
+
+ phpt_notify(message:"server-accepted");
+
+ fwrite($conn, "HTTP/1.0 200 Ok\r\nContent-Type: text/html\r\nWrong-Header\r\nGood-Header: test\r\n\r\nbody\r\n");
+CODE;
+
+$clientCode = <<<'CODE'
+ function stream_notification_callback($notification_code, $severity, $message, $message_code, $bytes_transferred, $bytes_max) {
+ switch($notification_code) {
+ case STREAM_NOTIFY_MIME_TYPE_IS:
+ echo "Found the mime-type: ", $message, PHP_EOL;
+ break;
+ }
+ }
+
+ $ctx = stream_context_create();
+ stream_context_set_params($ctx, array("notification" => "stream_notification_callback"));
+ var_dump(file_get_contents("http://{{ ADDR }}", false, $ctx));
+ var_dump($http_response_header);
+CODE;
+
+include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__);
+ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
+?>
+--EXPECTF--
+Found the mime-type: text/html
+
+Warning: file_get_contents(http://127.0.0.1:%d): Failed to open stream: HTTP invalid response format (no colon in header line)! in %s
+bool(false)
+array(2) {
+ [0]=>
+ string(15) "HTTP/1.0 200 Ok"
+ [1]=>
+ string(23) "Content-Type: text/html"
+}
diff --git a/ext/standard/tests/http/ghsa-pcmh-g36c-qc44-002.phpt b/ext/standard/tests/http/ghsa-pcmh-g36c-qc44-002.phpt
new file mode 100644
index 00000000000..1d0e4fa70a2
--- /dev/null
+++ b/ext/standard/tests/http/ghsa-pcmh-g36c-qc44-002.phpt
@@ -0,0 +1,51 @@
+--TEST--
+GHSA-pcmh-g36c-qc44: Header parser of http stream wrapper does not verify header name and colon (name)
+--FILE--
+<?php
+$serverCode = <<<'CODE'
+ $ctxt = stream_context_create([
+ "socket" => [
+ "tcp_nodelay" => true
+ ]
+ ]);
+
+ $server = stream_socket_server(
+ "tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt);
+ phpt_notify_server_start($server);
+
+ $conn = stream_socket_accept($server);
+
+ phpt_notify(message:"server-accepted");
+
+ fwrite($conn, "HTTP/1.0 200 Ok\r\nContent-Type: text/html\r\nWrong-Header : test\r\nGood-Header: test\r\n\r\nbody\r\n");
+CODE;
+
+$clientCode = <<<'CODE'
+ function stream_notification_callback($notification_code, $severity, $message, $message_code, $bytes_transferred, $bytes_max) {
+ switch($notification_code) {
+ case STREAM_NOTIFY_MIME_TYPE_IS:
+ echo "Found the mime-type: ", $message, PHP_EOL;
+ break;
+ }
+ }
+
+ $ctx = stream_context_create();
+ stream_context_set_params($ctx, array("notification" => "stream_notification_callback"));
+ var_dump(file_get_contents("http://{{ ADDR }}", false, $ctx));
+ var_dump($http_response_header);
+CODE;
+
+include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__);
+ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
+?>
+--EXPECTF--
+Found the mime-type: text/html
+
+Warning: file_get_contents(http://127.0.0.1:%d): Failed to open stream: HTTP invalid response format (space in header name)! in %s
+bool(false)
+array(2) {
+ [0]=>
+ string(15) "HTTP/1.0 200 Ok"
+ [1]=>
+ string(23) "Content-Type: text/html"
+}
--
2.48.1

View File

@ -0,0 +1,241 @@
From 8f65ef50929f6781f4973325f9b619f02cce19d8 Mon Sep 17 00:00:00 2001
From: Jakub Zelenka <bukka@php.net>
Date: Fri, 14 Feb 2025 19:17:22 +0100
Subject: [PATCH 04/11] Fix GHSA-hgf5-96fm-v528: http user header check of crlf
(cherry picked from commit 41d49abbd99dab06cdae4834db664435f8177174)
---
ext/standard/http_fopen_wrapper.c | 2 +-
.../tests/http/ghsa-hgf5-96fm-v528-001.phpt | 65 +++++++++++++++++++
.../tests/http/ghsa-hgf5-96fm-v528-002.phpt | 62 ++++++++++++++++++
.../tests/http/ghsa-hgf5-96fm-v528-003.phpt | 64 ++++++++++++++++++
4 files changed, 192 insertions(+), 1 deletion(-)
create mode 100644 ext/standard/tests/http/ghsa-hgf5-96fm-v528-001.phpt
create mode 100644 ext/standard/tests/http/ghsa-hgf5-96fm-v528-002.phpt
create mode 100644 ext/standard/tests/http/ghsa-hgf5-96fm-v528-003.phpt
diff --git a/ext/standard/http_fopen_wrapper.c b/ext/standard/http_fopen_wrapper.c
index e9b2486a7c9..64703c2f56b 100644
--- a/ext/standard/http_fopen_wrapper.c
+++ b/ext/standard/http_fopen_wrapper.c
@@ -107,7 +107,7 @@ static inline void strip_header(char *header_bag, char *lc_header_bag,
static zend_bool check_has_header(const char *headers, const char *header) {
const char *s = headers;
while ((s = strstr(s, header))) {
- if (s == headers || *(s-1) == '\n') {
+ if (s == headers || (*(s-1) == '\n' && *(s-2) == '\r')) {
return 1;
}
s++;
diff --git a/ext/standard/tests/http/ghsa-hgf5-96fm-v528-001.phpt b/ext/standard/tests/http/ghsa-hgf5-96fm-v528-001.phpt
new file mode 100644
index 00000000000..c40123560ef
--- /dev/null
+++ b/ext/standard/tests/http/ghsa-hgf5-96fm-v528-001.phpt
@@ -0,0 +1,65 @@
+--TEST--
+GHSA-hgf5-96fm-v528: Stream HTTP wrapper header check might omit basic auth header (incorrect inside pos)
+--FILE--
+<?php
+$serverCode = <<<'CODE'
+ $ctxt = stream_context_create([
+ "socket" => [
+ "tcp_nodelay" => true
+ ]
+ ]);
+
+ $server = stream_socket_server(
+ "tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt);
+ phpt_notify_server_start($server);
+
+ $conn = stream_socket_accept($server);
+
+ phpt_notify(message:"server-accepted");
+
+ $result = fread($conn, 1024);
+ $encoded_result = base64_encode($result);
+
+ fwrite($conn, "HTTP/1.0 200 Ok\r\nContent-Type: text/html; charset=utf-8\r\n\r\n$encoded_result\r\n");
+
+CODE;
+
+$clientCode = <<<'CODE'
+ $opts = [
+ "http" => [
+ "method" => "GET",
+ "header" => "Cookie: foo=bar\nauthorization:x\r\n"
+ ]
+ ];
+ $ctx = stream_context_create($opts);
+ var_dump(explode("\r\n", base64_decode(file_get_contents("http://user:pwd@{{ ADDR }}", false, $ctx))));
+ var_dump($http_response_header);
+CODE;
+
+include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__);
+ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
+?>
+--EXPECTF--
+array(7) {
+ [0]=>
+ string(14) "GET / HTTP/1.1"
+ [1]=>
+ string(33) "Authorization: Basic dXNlcjpwd2Q="
+ [2]=>
+ string(21) "Host: 127.0.0.1:%d"
+ [3]=>
+ string(17) "Connection: close"
+ [4]=>
+ string(31) "Cookie: foo=bar
+authorization:x"
+ [5]=>
+ string(0) ""
+ [6]=>
+ string(0) ""
+}
+array(2) {
+ [0]=>
+ string(15) "HTTP/1.0 200 Ok"
+ [1]=>
+ string(38) "Content-Type: text/html; charset=utf-8"
+}
diff --git a/ext/standard/tests/http/ghsa-hgf5-96fm-v528-002.phpt b/ext/standard/tests/http/ghsa-hgf5-96fm-v528-002.phpt
new file mode 100644
index 00000000000..37a47df060a
--- /dev/null
+++ b/ext/standard/tests/http/ghsa-hgf5-96fm-v528-002.phpt
@@ -0,0 +1,62 @@
+--TEST--
+GHSA-hgf5-96fm-v528: Header parser of http stream wrapper does not handle folded headers (correct start pos)
+--FILE--
+<?php
+$serverCode = <<<'CODE'
+ $ctxt = stream_context_create([
+ "socket" => [
+ "tcp_nodelay" => true
+ ]
+ ]);
+
+ $server = stream_socket_server(
+ "tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt);
+ phpt_notify_server_start($server);
+
+ $conn = stream_socket_accept($server);
+
+ phpt_notify(message:"server-accepted");
+
+ $result = fread($conn, 1024);
+ $encoded_result = base64_encode($result);
+
+ fwrite($conn, "HTTP/1.0 200 Ok\r\nContent-Type: text/html; charset=utf-8\r\n\r\n$encoded_result\r\n");
+
+CODE;
+
+$clientCode = <<<'CODE'
+ $opts = [
+ "http" => [
+ "method" => "GET",
+ "header" => "Authorization: Bearer x\r\n"
+ ]
+ ];
+ $ctx = stream_context_create($opts);
+ var_dump(explode("\r\n", base64_decode(file_get_contents("http://user:pwd@{{ ADDR }}", false, $ctx))));
+ var_dump($http_response_header);
+CODE;
+
+include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__);
+ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
+?>
+--EXPECTF--
+array(6) {
+ [0]=>
+ string(14) "GET / HTTP/1.1"
+ [1]=>
+ string(21) "Host: 127.0.0.1:%d"
+ [2]=>
+ string(17) "Connection: close"
+ [3]=>
+ string(23) "Authorization: Bearer x"
+ [4]=>
+ string(0) ""
+ [5]=>
+ string(0) ""
+}
+array(2) {
+ [0]=>
+ string(15) "HTTP/1.0 200 Ok"
+ [1]=>
+ string(38) "Content-Type: text/html; charset=utf-8"
+}
diff --git a/ext/standard/tests/http/ghsa-hgf5-96fm-v528-003.phpt b/ext/standard/tests/http/ghsa-hgf5-96fm-v528-003.phpt
new file mode 100644
index 00000000000..6c84679ff63
--- /dev/null
+++ b/ext/standard/tests/http/ghsa-hgf5-96fm-v528-003.phpt
@@ -0,0 +1,64 @@
+--TEST--
+GHSA-hgf5-96fm-v528: Header parser of http stream wrapper does not handle folded headers (correct middle pos)
+--FILE--
+<?php
+$serverCode = <<<'CODE'
+ $ctxt = stream_context_create([
+ "socket" => [
+ "tcp_nodelay" => true
+ ]
+ ]);
+
+ $server = stream_socket_server(
+ "tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt);
+ phpt_notify_server_start($server);
+
+ $conn = stream_socket_accept($server);
+
+ phpt_notify(message:"server-accepted");
+
+ $result = fread($conn, 1024);
+ $encoded_result = base64_encode($result);
+
+ fwrite($conn, "HTTP/1.0 200 Ok\r\nContent-Type: text/html; charset=utf-8\r\n\r\n$encoded_result\r\n");
+
+CODE;
+
+$clientCode = <<<'CODE'
+ $opts = [
+ "http" => [
+ "method" => "GET",
+ "header" => "Cookie: x=y\r\nAuthorization: Bearer x\r\n"
+ ]
+ ];
+ $ctx = stream_context_create($opts);
+ var_dump(explode("\r\n", base64_decode(file_get_contents("http://user:pwd@{{ ADDR }}", false, $ctx))));
+ var_dump($http_response_header);
+CODE;
+
+include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__);
+ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
+?>
+--EXPECTF--
+array(7) {
+ [0]=>
+ string(14) "GET / HTTP/1.1"
+ [1]=>
+ string(21) "Host: 127.0.0.1:%d"
+ [2]=>
+ string(17) "Connection: close"
+ [3]=>
+ string(11) "Cookie: x=y"
+ [4]=>
+ string(23) "Authorization: Bearer x"
+ [5]=>
+ string(0) ""
+ [6]=>
+ string(0) ""
+}
+array(2) {
+ [0]=>
+ string(15) "HTTP/1.0 200 Ok"
+ [1]=>
+ string(38) "Content-Type: text/html; charset=utf-8"
+}
--
2.48.1

View File

@ -0,0 +1,348 @@
From adc7e9f20c9a9aab9cd23ca47ec3fb96287898ae Mon Sep 17 00:00:00 2001
From: Jakub Zelenka <bukka@php.net>
Date: Tue, 4 Mar 2025 09:01:34 +0100
Subject: [PATCH 03/11] Fix GHSA-52jp-hrpf-2jff: http redirect location
truncation
It converts the allocation of location to be on heap instead of stack
and errors if the location length is greater than 8086 bytes.
(cherry picked from commit ac1a054bb3eb5994a199e8b18cca28cbabf5943e)
---
ext/standard/http_fopen_wrapper.c | 87 ++++++++++++-------
.../tests/http/ghsa-52jp-hrpf-2jff-001.phpt | 58 +++++++++++++
.../tests/http/ghsa-52jp-hrpf-2jff-002.phpt | 55 ++++++++++++
3 files changed, 168 insertions(+), 32 deletions(-)
create mode 100644 ext/standard/tests/http/ghsa-52jp-hrpf-2jff-001.phpt
create mode 100644 ext/standard/tests/http/ghsa-52jp-hrpf-2jff-002.phpt
diff --git a/ext/standard/http_fopen_wrapper.c b/ext/standard/http_fopen_wrapper.c
index 7ee22b85f88..e9b2486a7c9 100644
--- a/ext/standard/http_fopen_wrapper.c
+++ b/ext/standard/http_fopen_wrapper.c
@@ -67,15 +67,16 @@
#include "php_fopen_wrappers.h"
-#define HTTP_HEADER_BLOCK_SIZE 1024
-#define PHP_URL_REDIRECT_MAX 20
-#define HTTP_HEADER_USER_AGENT 1
-#define HTTP_HEADER_HOST 2
-#define HTTP_HEADER_AUTH 4
-#define HTTP_HEADER_FROM 8
-#define HTTP_HEADER_CONTENT_LENGTH 16
-#define HTTP_HEADER_TYPE 32
-#define HTTP_HEADER_CONNECTION 64
+#define HTTP_HEADER_BLOCK_SIZE 1024
+#define HTTP_HEADER_MAX_LOCATION_SIZE 8182 /* 8192 - 10 (size of "Location: ") */
+#define PHP_URL_REDIRECT_MAX 20
+#define HTTP_HEADER_USER_AGENT 1
+#define HTTP_HEADER_HOST 2
+#define HTTP_HEADER_AUTH 4
+#define HTTP_HEADER_FROM 8
+#define HTTP_HEADER_CONTENT_LENGTH 16
+#define HTTP_HEADER_TYPE 32
+#define HTTP_HEADER_CONNECTION 64
#define HTTP_WRAPPER_HEADER_INIT 1
#define HTTP_WRAPPER_REDIRECTED 2
@@ -119,17 +120,15 @@ typedef struct _php_stream_http_response_header_info {
size_t file_size;
bool error;
bool follow_location;
- char location[HTTP_HEADER_BLOCK_SIZE];
+ char *location;
+ size_t location_len;
} php_stream_http_response_header_info;
static void php_stream_http_response_header_info_init(
php_stream_http_response_header_info *header_info)
{
- header_info->transfer_encoding = NULL;
- header_info->file_size = 0;
- header_info->error = false;
+ memset(header_info, 0, sizeof(php_stream_http_response_header_info));
header_info->follow_location = 1;
- header_info->location[0] = '\0';
}
/* Trim white spaces from response header line and update its length */
@@ -255,7 +254,22 @@ static zend_string *php_stream_http_response_headers_parse(php_stream_wrapper *w
* RFC 7238 defines 308: http://tools.ietf.org/html/rfc7238 */
header_info->follow_location = 0;
}
- strlcpy(header_info->location, last_header_value, sizeof(header_info->location));
+ size_t last_header_value_len = strlen(last_header_value);
+ if (last_header_value_len > HTTP_HEADER_MAX_LOCATION_SIZE) {
+ header_info->error = true;
+ php_stream_wrapper_log_error(wrapper, options,
+ "HTTP Location header size is over the limit of %d bytes",
+ HTTP_HEADER_MAX_LOCATION_SIZE);
+ zend_string_efree(last_header_line_str);
+ return NULL;
+ }
+ if (header_info->location_len == 0) {
+ header_info->location = emalloc(last_header_value_len + 1);
+ } else if (header_info->location_len <= last_header_value_len) {
+ header_info->location = erealloc(header_info->location, last_header_value_len + 1);
+ }
+ header_info->location_len = last_header_value_len;
+ memcpy(header_info->location, last_header_value, last_header_value_len + 1);
} else if (!strncasecmp(last_header_line, "Content-Type:", sizeof("Content-Type:")-1)) {
php_stream_notify_info(context, PHP_STREAM_NOTIFY_MIME_TYPE_IS, last_header_value, 0);
} else if (!strncasecmp(last_header_line, "Content-Length:", sizeof("Content-Length:")-1)) {
@@ -538,6 +552,8 @@ finish:
}
}
+ php_stream_http_response_header_info_init(&header_info);
+
if (stream == NULL)
goto out;
@@ -919,8 +935,6 @@ finish:
}
}
- php_stream_http_response_header_info_init(&header_info);
-
/* read past HTTP headers */
while (!php_stream_eof(stream)) {
size_t http_header_line_length;
@@ -990,12 +1004,12 @@ finish:
last_header_line_str, NULL, NULL, response_code, response_header, &header_info);
}
- if (!reqok || (header_info.location[0] != '\0' && header_info.follow_location)) {
+ if (!reqok || (header_info.location != NULL && header_info.follow_location)) {
if (!header_info.follow_location || (((options & STREAM_ONLY_GET_HEADERS) || ignore_errors) && redirect_max <= 1)) {
goto out;
}
- if (header_info.location[0] != '\0')
+ if (header_info.location != NULL)
php_stream_notify_info(context, PHP_STREAM_NOTIFY_REDIRECTED, header_info.location, 0);
php_stream_close(stream);
@@ -1006,18 +1020,17 @@ finish:
header_info.transfer_encoding = NULL;
}
- if (header_info.location[0] != '\0') {
+ if (header_info.location != NULL) {
- char new_path[HTTP_HEADER_BLOCK_SIZE];
- char loc_path[HTTP_HEADER_BLOCK_SIZE];
+ char *new_path = NULL;
- *new_path='\0';
if (strlen(header_info.location) < 8 ||
(strncasecmp(header_info.location, "http://", sizeof("http://")-1) &&
strncasecmp(header_info.location, "https://", sizeof("https://")-1) &&
strncasecmp(header_info.location, "ftp://", sizeof("ftp://")-1) &&
strncasecmp(header_info.location, "ftps://", sizeof("ftps://")-1)))
{
+ char *loc_path = NULL;
if (*header_info.location != '/') {
if (*(header_info.location+1) != '\0' && resource->path) {
char *s = strrchr(ZSTR_VAL(resource->path), '/');
@@ -1035,31 +1048,35 @@ finish:
if (resource->path &&
ZSTR_VAL(resource->path)[0] == '/' &&
ZSTR_VAL(resource->path)[1] == '\0') {
- snprintf(loc_path, sizeof(loc_path) - 1, "%s%s",
- ZSTR_VAL(resource->path), header_info.location);
+ spprintf(&loc_path, 0, "%s%s", ZSTR_VAL(resource->path), header_info.location);
} else {
- snprintf(loc_path, sizeof(loc_path) - 1, "%s/%s",
- ZSTR_VAL(resource->path), header_info.location);
+ spprintf(&loc_path, 0, "%s/%s", ZSTR_VAL(resource->path), header_info.location);
}
} else {
- snprintf(loc_path, sizeof(loc_path) - 1, "/%s", header_info.location);
+ spprintf(&loc_path, 0, "/%s", header_info.location);
}
} else {
- strlcpy(loc_path, header_info.location, sizeof(loc_path));
+ loc_path = header_info.location;
+ header_info.location = NULL;
}
if ((use_ssl && resource->port != 443) || (!use_ssl && resource->port != 80)) {
- snprintf(new_path, sizeof(new_path) - 1, "%s://%s:%d%s", ZSTR_VAL(resource->scheme), ZSTR_VAL(resource->host), resource->port, loc_path);
+ spprintf(&new_path, 0, "%s://%s:%d%s", ZSTR_VAL(resource->scheme),
+ ZSTR_VAL(resource->host), resource->port, loc_path);
} else {
- snprintf(new_path, sizeof(new_path) - 1, "%s://%s%s", ZSTR_VAL(resource->scheme), ZSTR_VAL(resource->host), loc_path);
+ spprintf(&new_path, 0, "%s://%s%s", ZSTR_VAL(resource->scheme),
+ ZSTR_VAL(resource->host), loc_path);
}
+ efree(loc_path);
} else {
- strlcpy(new_path, header_info.location, sizeof(new_path));
+ new_path = header_info.location;
+ header_info.location = NULL;
}
php_url_free(resource);
/* check for invalid redirection URLs */
if ((resource = php_url_parse(new_path)) == NULL) {
php_stream_wrapper_log_error(wrapper, options, "Invalid redirect URL! %s", new_path);
+ efree(new_path);
goto out;
}
@@ -1071,6 +1088,7 @@ finish:
while (s < e) { \
if (iscntrl(*s)) { \
php_stream_wrapper_log_error(wrapper, options, "Invalid redirect URL! %s", new_path); \
+ efree(new_path); \
goto out; \
} \
s++; \
@@ -1086,6 +1104,7 @@ finish:
stream = php_stream_url_wrap_http_ex(
wrapper, new_path, mode, options, opened_path, context,
--redirect_max, HTTP_WRAPPER_REDIRECTED, response_header STREAMS_CC);
+ efree(new_path);
} else {
php_stream_wrapper_log_error(wrapper, options, "HTTP request failed! %s", tmp_line);
}
@@ -1098,6 +1117,10 @@ out:
efree(http_header_line);
}
+ if (header_info.location != NULL) {
+ efree(header_info.location);
+ }
+
if (resource) {
php_url_free(resource);
}
diff --git a/ext/standard/tests/http/ghsa-52jp-hrpf-2jff-001.phpt b/ext/standard/tests/http/ghsa-52jp-hrpf-2jff-001.phpt
new file mode 100644
index 00000000000..744cff9cc72
--- /dev/null
+++ b/ext/standard/tests/http/ghsa-52jp-hrpf-2jff-001.phpt
@@ -0,0 +1,58 @@
+--TEST--
+GHSA-52jp-hrpf-2jff: HTTP stream wrapper truncate redirect location to 1024 bytes (success)
+--FILE--
+<?php
+$serverCode = <<<'CODE'
+$ctxt = stream_context_create([
+ "socket" => [
+ "tcp_nodelay" => true
+ ]
+ ]);
+
+ $server = stream_socket_server(
+ "tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt);
+ phpt_notify_server_start($server);
+
+ $conn = stream_socket_accept($server);
+
+ phpt_notify(message:"server-accepted");
+
+ $loc = str_repeat("y", 8000);
+ fwrite($conn, "HTTP/1.0 301 Ok\r\nContent-Type: text/html;\r\nLocation: $loc\r\n\r\nbody\r\n");
+CODE;
+
+$clientCode = <<<'CODE'
+ function stream_notification_callback($notification_code, $severity, $message, $message_code, $bytes_transferred, $bytes_max) {
+ switch($notification_code) {
+ case STREAM_NOTIFY_MIME_TYPE_IS:
+ echo "Found the mime-type: ", $message, PHP_EOL;
+ break;
+ case STREAM_NOTIFY_REDIRECTED:
+ echo "Redirected: ";
+ var_dump($message);
+ }
+ }
+
+ $ctx = stream_context_create();
+ stream_context_set_params($ctx, array("notification" => "stream_notification_callback"));
+ var_dump(trim(file_get_contents("http://{{ ADDR }}", false, $ctx)));
+ var_dump($http_response_header);
+CODE;
+
+include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__);
+ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
+?>
+--EXPECTF--
+Found the mime-type: text/html;
+Redirected: string(8000) "%s"
+
+Warning: file_get_contents(http://127.0.0.1:%d): Failed to open stream: %s
+string(0) ""
+array(3) {
+ [0]=>
+ string(15) "HTTP/1.0 301 Ok"
+ [1]=>
+ string(24) "Content-Type: text/html;"
+ [2]=>
+ string(8010) "Location: %s"
+}
diff --git a/ext/standard/tests/http/ghsa-52jp-hrpf-2jff-002.phpt b/ext/standard/tests/http/ghsa-52jp-hrpf-2jff-002.phpt
new file mode 100644
index 00000000000..bc71fd4e411
--- /dev/null
+++ b/ext/standard/tests/http/ghsa-52jp-hrpf-2jff-002.phpt
@@ -0,0 +1,55 @@
+--TEST--
+GHSA-52jp-hrpf-2jff: HTTP stream wrapper truncate redirect location to 1024 bytes (over limit)
+--FILE--
+<?php
+$serverCode = <<<'CODE'
+$ctxt = stream_context_create([
+ "socket" => [
+ "tcp_nodelay" => true
+ ]
+ ]);
+
+ $server = stream_socket_server(
+ "tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt);
+ phpt_notify_server_start($server);
+
+ $conn = stream_socket_accept($server);
+
+ phpt_notify(message:"server-accepted");
+
+ $loc = str_repeat("y", 9000);
+ fwrite($conn, "HTTP/1.0 301 Ok\r\nContent-Type: text/html;\r\nLocation: $loc\r\n\r\nbody\r\n");
+CODE;
+
+$clientCode = <<<'CODE'
+ function stream_notification_callback($notification_code, $severity, $message, $message_code, $bytes_transferred, $bytes_max) {
+ switch($notification_code) {
+ case STREAM_NOTIFY_MIME_TYPE_IS:
+ echo "Found the mime-type: ", $message, PHP_EOL;
+ break;
+ case STREAM_NOTIFY_REDIRECTED:
+ echo "Redirected: ";
+ var_dump($message);
+ }
+ }
+
+ $ctx = stream_context_create();
+ stream_context_set_params($ctx, array("notification" => "stream_notification_callback"));
+ var_dump(trim(file_get_contents("http://{{ ADDR }}", false, $ctx)));
+ var_dump($http_response_header);
+CODE;
+
+include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__);
+ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
+?>
+--EXPECTF--
+Found the mime-type: text/html;
+
+Warning: file_get_contents(http://127.0.0.1:%d): Failed to open stream: HTTP Location header size is over the limit of 8182 bytes in %s
+string(0) ""
+array(2) {
+ [0]=>
+ string(15) "HTTP/1.0 301 Ok"
+ [1]=>
+ string(24) "Content-Type: text/html;"
+}
--
2.48.1

View File

@ -0,0 +1,133 @@
From 462092a48aa0dbad24d9fa8a4a9d418faa14d309 Mon Sep 17 00:00:00 2001
From: Niels Dossche <7771979+nielsdos@users.noreply.github.com>
Date: Sat, 9 Nov 2024 15:29:52 +0100
Subject: [PATCH 6/8] Fix GHSA-4w77-75f9-2c8w
(cherry picked from commit 7dd336ae838bbf2c62dc47e3c900d657d3534c02)
---
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 295448f1211..5104318a634 100644
--- a/sapi/cli/php_cli_server.c
+++ b/sapi/cli/php_cli_server.c
@@ -1863,8 +1863,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;
@@ -1872,9 +1870,7 @@ static void php_cli_server_client_populate_request_info(const php_cli_server_cli
request_info->query_string = client->request.query_string;
request_info->content_length = client->request.content_len;
request_info->auth_user = request_info->auth_password = request_info->auth_digest = NULL;
- if (NULL != (val = zend_hash_str_find_ptr(&client->request.headers, "content-type", sizeof("content-type")-1))) {
- request_info->content_type = val;
- }
+ request_info->content_type = zend_hash_str_find_ptr(&client->request.headers, "content-type", sizeof("content-type")-1);
} /* }}} */
static void destroy_request_info(sapi_request_info *request_info) /* {{{ */
diff --git a/sapi/cli/tests/ghsa-4w77-75f9-2c8w.phpt b/sapi/cli/tests/ghsa-4w77-75f9-2c8w.phpt
new file mode 100644
index 00000000000..2c8aeff12d5
--- /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 . "/", context: $context);
+
+$options = [
+ "http" => [
+ "method" => "POST",
+ ],
+];
+$context = stream_context_create($options);
+
+echo file_get_contents("http://" . PHP_CLI_SERVER_ADDRESS . "/", context: $context);
+?>
+--EXPECT--
+string(5) "AAAAA"
+string(0) ""
--
2.47.0
From 22bdb43da0ecd6e72d63b63aa6c1f3a25d1bca3a Mon Sep 17 00:00:00 2001
From: Remi Collet <remi@remirepo.net>
Date: Fri, 22 Nov 2024 08:58:10 +0100
Subject: [PATCH 7/8] NEWS for 8.1.31 backports
---
NEWS | 24 ++++++++++++++++++++++++
1 file changed, 24 insertions(+)
diff --git a/NEWS b/NEWS
index bad0a719aae..0f82a65a44b 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

View File

@ -1,4 +1,4 @@
# Fedora spec file for php
# RHEL / Fedora spec file for php
#
# License: MIT
# http://opensource.org/licenses/MIT
@ -62,7 +62,7 @@
Summary: PHP scripting language for creating dynamic web sites
Name: php
Version: %{upver}%{?rcver:~%{rcver}}
Release: 1%{?dist}
Release: 3%{?dist}
# All files licensed under PHP version 3.01, except
# Zend is licensed under Zend
# TSRM is licensed under BSD
@ -125,6 +125,25 @@ Patch51: php-8.0.13-crypt.patch
# Upstream fixes (100+)
# Security fixes (200+)
# From https://github.com/remicollet/php-src-security
Patch200: php-cve-2024-2756.patch
Patch201: php-cve-2024-3096.patch
Patch202: php-cve-2024-5458.patch
Patch203: php-cve-2024-8925.patch
Patch204: php-cve-2024-8926.patch
Patch205: php-cve-2024-8927.patch
Patch206: php-cve-2024-9026.patch
Patch207: php-cve-2024-11236.patch
Patch208: php-cve-2024-11234.patch
Patch209: php-cve-2024-8932.patch
Patch210: php-cve-2024-11233.patch
Patch211: php-ghsa-4w77-75f9-2c8w.patch
Patch212: php-cve-2024-8929.patch
Patch213: php-cve-2025-1217.patch
Patch214: php-cve-2025-1734.patch
Patch215: php-cve-2025-1861.patch
Patch216: php-cve-2025-1736.patch
Patch217: php-cve-2025-1219.patch
# Fixes for tests (300+)
# Factory is droped from system tzdata
@ -727,6 +746,24 @@ rm ext/openssl/tests/p12_with_extra_certs.p12
# upstream patches
# security patches
%patch -P200 -p1 -b .cve2756
%patch -P201 -p1 -b .cve3096
%patch -P202 -p1 -b .cve5458
%patch -P203 -p1 -b .cve8925
%patch -P204 -p1 -b .cve8926
%patch -P205 -p1 -b .cve8927
%patch -P206 -p1 -b .cve9026
%patch -P207 -p1 -b .cve11236
%patch -P208 -p1 -b .cve11234
%patch -P209 -p1 -b .cve8932
%patch -P210 -p1 -b .cve11233
%patch -P211 -p1 -b .ghsa4w77
%patch -P212 -p1 -b .cve8929
%patch -P213 -p1 -b .cve1217
%patch -P214 -p1 -b .cve1734
%patch -P215 -p1 -b .cve1861
%patch -P216 -p1 -b .cve1736
%patch -P217 -p1 -b .cve1219
# Fixes for tests
%patch -P300 -p1 -b .datetests
@ -1535,6 +1572,38 @@ systemctl try-restart php-fpm.service >/dev/null 2>&1 || :
%changelog
* Thu Mar 13 2025 Remi Collet <rcollet@redhat.com> - 8.0.30-3
- Fix libxml streams use wrong `content-type` header when requesting a redirected resource
CVE-2025-1219
- Fix Stream HTTP wrapper header check might omit basic auth header
CVE-2025-1736
- Fix Stream HTTP wrapper truncate redirect location to 1024 bytes
CVE-2025-1861
- Fix Streams HTTP wrapper does not fail for headers without colon
CVE-2025-1734
- Fix Header parser of `http` stream wrapper does not handle folded headers
CVE-2025-1217
* Tue Jan 21 2025 Remi Collet <rcollet@redhat.com> - 8.0.30-2
- Fix Leak partial content of the heap through heap buffer over-read
CVE-2024-8929
- 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
- Fix cgi.force_redirect configuration is bypassable due to the environment variable collision
CVE-2024-8927
- Fix Logs from childrens may be altered
CVE-2024-9026
- Fix Erroneous parsing of multipart form data
CVE-2024-8925
- Fix filter bypass in filter_var FILTER_VALIDATE_URL
CVE-2024-5458
- 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
* Fri Oct 6 2023 Remi Collet <rcollet@redhat.com> - 8.0.30-1
- rebase to 8.0.30
- Resolves: RHEL-11946