From 4fec08542748c25573063ffc53ea89cd5de1edf0 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka 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 (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-- + [ + "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-- + [ + "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-- + [ + "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-- + [ + "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-- + [ + "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-- - ---INI-- -allow_url_fopen=1 ---FILE-- - $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