diff --git a/NEWS b/NEWS index f8f13f016bc..07effa01580 100644 --- a/NEWS +++ b/NEWS @@ -50,6 +50,11 @@ PHP NEWS . Fixed bug GH-17704 (ldap_search fails when $attributes contains a non-packed array with numerical keys). (nielsdos, 7u83) +- LibXML: + . Fixed GHSA-wg4p-4hqh-c3g9 (Reocurrence of #72714). (nielsdos) + . Fixed GHSA-p3x9-6h7p-cgfc (libxml streams use wrong `content-type` header + when requesting a redirected resource). (CVE-2025-1219) (timwolla) + - MBString: . Fixed bug GH-17503 (Undefined float conversion in mb_convert_variables). (cmb) @@ -89,6 +94,14 @@ PHP NEWS - Streams: . Fixed bug GH-17650 (realloc with size 0 in user_filters.c). (nielsdos) . Fix memory leak on overflow in _php_stream_scandir(). (nielsdos) + . Fixed GHSA-hgf54-96fm-v528 (Stream HTTP wrapper header check might omit + basic auth header). (CVE-2025-1736) (Jakub Zelenka) + . Fixed GHSA-52jp-hrpf-2jff (Stream HTTP wrapper truncate redirect location + to 1024 bytes). (CVE-2025-1861) (Jakub Zelenka) + . Fixed GHSA-pcmh-g36c-qc44 (Streams HTTP wrapper does not fail for headers + without colon). (CVE-2025-1734) (Jakub Zelenka) + . Fixed GHSA-v8xr-gpvj-cx9g (Header parser of `http` stream wrapper does not + handle folded headers). (CVE-2025-1217) (Jakub Zelenka) - Windows: . Fixed phpize for Windows 11 (24H2). (bwoebi) diff --git a/ext/dom/tests/ghsa-p3x9-6h7p-cgfc_001.phpt b/ext/dom/tests/ghsa-p3x9-6h7p-cgfc_001.phpt new file mode 100644 index 00000000000..47212cb3410 --- /dev/null +++ b/ext/dom/tests/ghsa-p3x9-6h7p-cgfc_001.phpt @@ -0,0 +1,60 @@ +--TEST-- +GHSA-p3x9-6h7p-cgfc: libxml streams use wrong `content-type` header when requesting a redirected resource (Basic) +--EXTENSIONS-- +dom +--SKIPIF-- + +--FILE-- + + + + GHSA-p3x9-6h7p-cgfc + + + + + + +

GHSA-p3x9-6h7p-cgfc

+ + + EOT; + // Intentionally using non-standard casing for content-type to verify it is matched not case sensitively. + yield "data://text/plain,HTTP/1.1 200 OK\r\nconteNt-tyPe: text/html; charset=utf-8\r\n\r\n{$xml}"; +} + +['pid' => $pid, 'uri' => $uri] = http_server('genResponses', $output); +$document = new \DOMDocument(); +$document->loadHTMLFile($uri); + +$h1 = $document->getElementsByTagName('h1'); +var_dump($h1->length); +var_dump($document->saveHTML()); +http_server_kill($pid); +?> +--EXPECT-- +int(1) +string(266) " + + + GHSA-p3x9-6h7p-cgfc + + + + + + +

GHSA-p3x9-6h7p-cgfc

+ + +" diff --git a/ext/dom/tests/ghsa-p3x9-6h7p-cgfc_002.phpt b/ext/dom/tests/ghsa-p3x9-6h7p-cgfc_002.phpt new file mode 100644 index 00000000000..a7eff3b9a8b --- /dev/null +++ b/ext/dom/tests/ghsa-p3x9-6h7p-cgfc_002.phpt @@ -0,0 +1,60 @@ +--TEST-- +GHSA-p3x9-6h7p-cgfc: libxml streams use wrong `content-type` header when requesting a redirected resource (Missing content-type) +--EXTENSIONS-- +dom +--SKIPIF-- + +--FILE-- + + + + GHSA-p3x9-6h7p-cgfc + + + + + + +

GHSA-p3x9-6h7p-cgfc

+ + + EOT; + // Missing content-type in actual response. + yield "data://text/plain,HTTP/1.1 200 OK\r\n\r\n{$xml}"; +} + +['pid' => $pid, 'uri' => $uri] = http_server('genResponses', $output); +$document = new \DOMDocument(); +$document->loadHTMLFile($uri); + +$h1 = $document->getElementsByTagName('h1'); +var_dump($h1->length); +var_dump($document->saveHTML()); +http_server_kill($pid); +?> +--EXPECT-- +int(1) +string(266) " + + + GHSA-p3x9-6h7p-cgfc + + + + + + +

GHSA-p3x9-6h7p-cgfc

+ + +" diff --git a/ext/dom/tests/ghsa-p3x9-6h7p-cgfc_003.phpt b/ext/dom/tests/ghsa-p3x9-6h7p-cgfc_003.phpt new file mode 100644 index 00000000000..178b35f3525 --- /dev/null +++ b/ext/dom/tests/ghsa-p3x9-6h7p-cgfc_003.phpt @@ -0,0 +1,60 @@ +--TEST-- +GHSA-p3x9-6h7p-cgfc: libxml streams use wrong `content-type` header when requesting a redirected resource (Reason with colon) +--EXTENSIONS-- +dom +--SKIPIF-- + +--FILE-- + + + + GHSA-p3x9-6h7p-cgfc + + + + + + +

GHSA-p3x9-6h7p-cgfc

+ + + EOT; + // Missing content-type in actual response. + yield "data://text/plain,HTTP/1.1 200 OK: This is fine\r\n\r\n{$xml}"; +} + +['pid' => $pid, 'uri' => $uri] = http_server('genResponses', $output); +$document = new \DOMDocument(); +$document->loadHTMLFile($uri); + +$h1 = $document->getElementsByTagName('h1'); +var_dump($h1->length); +var_dump($document->saveHTML()); +http_server_kill($pid); +?> +--EXPECT-- +int(1) +string(266) " + + + GHSA-p3x9-6h7p-cgfc + + + + + + +

GHSA-p3x9-6h7p-cgfc

+ + +" diff --git a/ext/libxml/libxml.c b/ext/libxml/libxml.c index 6590f73f9ed..18ca51e36a0 100644 --- a/ext/libxml/libxml.c +++ b/ext/libxml/libxml.c @@ -525,41 +525,52 @@ php_libxml_input_buffer_create_filename(const char *URI, xmlCharEncoding enc) if (Z_TYPE(s->wrapperdata) == IS_ARRAY) { zval *header; - ZEND_HASH_FOREACH_VAL_IND(Z_ARRVAL(s->wrapperdata), header) { + /* Scan backwards: The header array might contain the headers for multiple responses, if + * a redirect was followed. + */ + ZEND_HASH_REVERSE_FOREACH_VAL_IND(Z_ARRVAL(s->wrapperdata), header) { const char buf[] = "Content-Type:"; - if (Z_TYPE_P(header) == IS_STRING && - !zend_binary_strncasecmp(Z_STRVAL_P(header), Z_STRLEN_P(header), buf, sizeof(buf)-1, sizeof(buf)-1)) { - char needle[] = "charset="; - char *haystack = estrndup(Z_STRVAL_P(header), Z_STRLEN_P(header)); - char *encoding = php_stristr(haystack, needle, Z_STRLEN_P(header), strlen(needle)); - - if (encoding) { - char *end; - - encoding += sizeof("charset=")-1; - if (*encoding == '"') { - encoding++; - } - end = strchr(encoding, ';'); - if (end == NULL) { - end = encoding + strlen(encoding); - } - end--; /* end == encoding-1 isn't a buffer underrun */ - while (*end == ' ' || *end == '\t') { - end--; - } - if (*end == '"') { - end--; - } - if (encoding >= end) continue; - *(end+1) = '\0'; - enc = xmlParseCharEncoding(encoding); - if (enc <= XML_CHAR_ENCODING_NONE) { - enc = XML_CHAR_ENCODING_NONE; - } + if (Z_TYPE_P(header) == IS_STRING) { + /* If no colon is found in the header, we assume it's the HTTP status line and bail out. */ + char *colon = memchr(Z_STRVAL_P(header), ':', Z_STRLEN_P(header)); + char *space = memchr(Z_STRVAL_P(header), ' ', Z_STRLEN_P(header)); + if (colon == NULL || space < colon) { + break; + } + + if (!zend_binary_strncasecmp(Z_STRVAL_P(header), Z_STRLEN_P(header), buf, sizeof(buf)-1, sizeof(buf)-1)) { + char needle[] = "charset="; + char *haystack = estrndup(Z_STRVAL_P(header), Z_STRLEN_P(header)); + char *encoding = php_stristr(haystack, needle, Z_STRLEN_P(header), sizeof("charset=")-1); + + if (encoding) { + char *end; + + encoding += sizeof("charset=")-1; + if (*encoding == '"') { + encoding++; + } + end = strchr(encoding, ';'); + if (end == NULL) { + end = encoding + strlen(encoding); + } + end--; /* end == encoding-1 isn't a buffer underrun */ + while (*end == ' ' || *end == '\t') { + end--; + } + if (*end == '"') { + end--; + } + if (encoding >= end) continue; + *(end+1) = '\0'; + enc = xmlParseCharEncoding(encoding); + if (enc <= XML_CHAR_ENCODING_NONE) { + enc = XML_CHAR_ENCODING_NONE; + } + } + efree(haystack); + break; /* found content-type */ } - efree(haystack); - break; /* found content-type */ } } ZEND_HASH_FOREACH_END(); } diff --git a/ext/standard/http_fopen_wrapper.c b/ext/standard/http_fopen_wrapper.c index fb987c82e5c..b4d065dd0b6 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 @@ -107,7 +108,7 @@ static inline void strip_header(char *header_bag, char *lc_header_bag, static 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++; @@ -115,6 +116,214 @@ static 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 error; + bool follow_location; + 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) +{ + memset(header_info, 0, sizeof(php_stream_http_response_header_info)); + header_info->follow_location = 1; +} + +/* 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_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); + 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) { + /* 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. */ + while (last_header_value < last_header_line_end + && (*last_header_value == ' ' || *last_header_value == '\t')) { + last_header_value++; + } + } else { + /* 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; + 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; + } + 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)) { + /* https://www.rfc-editor.org/rfc/rfc9110.html#name-content-length */ + const char *ptr = last_header_value; + /* must contain only digits, no + or - symbols */ + if (*ptr >= '0' && *ptr <= '9') { + char *endptr = NULL; + size_t parsed = ZEND_STRTOUL(ptr, &endptr, 10); + /* check whether there was no garbage in the header value and the conversion was successful */ + if (endptr && !*endptr) { + /* truncate for 32-bit such that no negative file sizes occur */ + header_info->file_size = MIN(parsed, ZEND_LONG_MAX); + 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, @@ -127,11 +336,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; zend_string *transport_string; zend_string *errstr = NULL; @@ -142,8 +352,6 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper, int header_init = ((flags & HTTP_WRAPPER_HEADER_INIT) != 0); int redirected = ((flags & HTTP_WRAPPER_REDIRECTED) != 0); int redirect_keep_method = ((flags & HTTP_WRAPPER_KEEP_METHOD) != 0); - bool follow_location = 1; - php_stream_filter *transfer_encoding = NULL; int response_code; smart_str req_buf = {0}; bool custom_request_method; @@ -366,6 +574,8 @@ finish: } } + php_stream_http_response_header_info_init(&header_info); + if (stream == NULL) goto out; @@ -666,8 +876,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); } @@ -750,140 +958,103 @@ finish: } /* 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; - } - 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)) { - /* https://www.rfc-editor.org/rfc/rfc9110.html#name-content-length */ - const char *ptr = http_header_value; - /* must contain only digits, no + or - symbols */ - if (*ptr >= '0' && *ptr <= '9') { - char *endptr = NULL; - size_t parsed = ZEND_STRTOUL(ptr, &endptr, 10); - /* check whether there was no garbage in the header value and the conversion was successful */ - if (endptr && !*endptr) { - /* truncate for 32-bit such that no negative file sizes occur */ - file_size = MIN(parsed, ZEND_LONG_MAX); - php_stream_notify_file_size(context, file_size, http_header_line, 0); + + if (last_header_line_str != NULL) { + /* Parse last header line. */ + 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; } - } 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(wrapper, stream, context, options, + last_header_line_str, NULL, NULL, response_code, response_header, &header_info); + } + + 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 (location[0] != '\0') - php_stream_notify_info(context, PHP_STREAM_NOTIFY_REDIRECTED, location, 0); + if (header_info.location != NULL) + 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 != NULL) { - char new_path[HTTP_HEADER_BLOCK_SIZE]; - char loc_path[HTTP_HEADER_BLOCK_SIZE]; + char *new_path = NULL; - *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) { + char *loc_path = NULL; + 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); @@ -899,29 +1070,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), 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), location); + spprintf(&loc_path, 0, "%s/%s", ZSTR_VAL(resource->path), header_info.location); } } else { - snprintf(loc_path, sizeof(loc_path) - 1, "/%s", location); + spprintf(&loc_path, 0, "/%s", header_info.location); } } else { - strlcpy(loc_path, 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, 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; } @@ -933,6 +1110,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++; \ @@ -955,6 +1133,7 @@ finish: stream = php_stream_url_wrap_http_ex( wrapper, new_path, mode, options, opened_path, context, --redirect_max, new_flags, response_header STREAMS_CC); + efree(new_path); } else { php_stream_wrapper_log_error(wrapper, options, "HTTP request failed! %s", tmp_line); } @@ -967,6 +1146,10 @@ out: efree(http_header_line); } + if (header_info.location != NULL) { + efree(header_info.location); + } + if (resource) { php_url_free(resource); } @@ -975,7 +1158,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) @@ -991,8 +1174,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); } /* It's possible that the server already sent in more data than just the headers. 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-- -Type='text/plain' -Hello -Size=5 -World +--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 + +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-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-- + [ + "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-- + [ + "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;" +} 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-- + [ + "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-- + [ + "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-- + [ + "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" +} 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-- + [ + "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-- + [ + "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" +} 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) "" -} diff --git a/ext/xml/tests/toffset_bounds.phpt b/ext/xml/tests/toffset_bounds.phpt new file mode 100644 index 00000000000..5a3fd22f86c --- /dev/null +++ b/ext/xml/tests/toffset_bounds.phpt @@ -0,0 +1,42 @@ +--TEST-- +XML_OPTION_SKIP_TAGSTART bounds +--EXTENSIONS-- +xml +--FILE-- +"; +$parser = xml_parser_create(); +xml_parser_set_option($parser, XML_OPTION_SKIP_TAGSTART, 100); +$res = xml_parse_into_struct($parser,$sample,$vals,$index); +var_dump($vals); +?> +--EXPECT-- +array(3) { + [0]=> + array(3) { + ["tag"]=> + string(0) "" + ["type"]=> + string(4) "open" + ["level"]=> + int(1) + } + [1]=> + array(3) { + ["tag"]=> + string(0) "" + ["type"]=> + string(8) "complete" + ["level"]=> + int(2) + } + [2]=> + array(3) { + ["tag"]=> + string(0) "" + ["type"]=> + string(5) "close" + ["level"]=> + int(1) + } +} diff --git a/ext/xml/xml.c b/ext/xml/xml.c index 62507a5d130..c4c1ac31a4b 100644 --- a/ext/xml/xml.c +++ b/ext/xml/xml.c @@ -657,9 +657,11 @@ void _xml_startElementHandler(void *userData, const XML_Char *name, const XML_Ch array_init(&tag); array_init(&atr); - _xml_add_to_info(parser, ZSTR_VAL(tag_name) + parser->toffset); + char *skipped_tag_name = SKIP_TAGSTART(ZSTR_VAL(tag_name)); - add_assoc_string(&tag, "tag", SKIP_TAGSTART(ZSTR_VAL(tag_name))); /* cast to avoid gcc-warning */ + _xml_add_to_info(parser, skipped_tag_name); + + add_assoc_string(&tag, "tag", skipped_tag_name); add_assoc_string(&tag, "type", "open"); add_assoc_long(&tag, "level", parser->level); @@ -741,12 +743,14 @@ void _xml_endElementHandler(void *userData, const XML_Char *name) add_assoc_string(zv, "type", "complete"); } } else { - _xml_add_to_info(parser, ZSTR_VAL(tag_name) + parser->toffset); + char *skipped_tag_name = SKIP_TAGSTART(ZSTR_VAL(tag_name)); + + _xml_add_to_info(parser, skipped_tag_name); zval *data = xml_get_separated_data(parser); if (EXPECTED(data)) { array_init(&tag); - add_assoc_string(&tag, "tag", SKIP_TAGSTART(ZSTR_VAL(tag_name))); /* cast to avoid gcc-warning */ + add_assoc_string(&tag, "tag", skipped_tag_name); add_assoc_string(&tag, "type", "close"); add_assoc_long(&tag, "level", parser->level); zend_hash_next_index_insert(Z_ARRVAL_P(data), &tag);