diff options
Diffstat (limited to 'php-cve-2025-1217.patch')
-rw-r--r-- | php-cve-2025-1217.patch | 909 |
1 files changed, 909 insertions, 0 deletions
diff --git a/php-cve-2025-1217.patch b/php-cve-2025-1217.patch new file mode 100644 index 0000000..1778bae --- /dev/null +++ b/php-cve-2025-1217.patch @@ -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 + |