Compare commits

...

11 commits

Author SHA1 Message Date
Saki Takamachi
23e05bdcf2
Update versions for PHP 8.4.5 2025-03-12 10:55:56 +09:00
Jakub Zelenka
689c019f54
Update NEWS with entries for security fixes 2025-03-07 13:47:38 +01:00
Ilija Tovilo
115a918790
Fix varying pgsql error message 2025-03-05 22:05:09 +01:00
Niels Dossche
3377d4fb4c
Fix GHSA-wg4p-4hqh-c3g9 2025-03-05 20:48:59 +01:00
Jakub Zelenka
789132c74f
Fix incorrectly ported tests for http 2025-03-05 19:01:14 +01:00
Ilija Tovilo
29dd6eb307
Use-after-free for ??= due to incorrect live-range calculation
Fixes GHSA-rwp7-7vc6-8477
2025-03-05 14:24:30 +01:00
Tim Düsterhus
e22b3a3708
Fix GHSA-p3x9-6h7p-cgfc: libxml streams wrong content-type on redirect
libxml streams use wrong content-type header when requesting a
redirected resource.
2025-03-05 14:16:10 +01:00
Jakub Zelenka
963551715a
Fix GHSA-hgf5-96fm-v528: http user header check of crlf 2025-03-05 13:42:52 +01:00
Jakub Zelenka
2488e73d1f
Fix GHSA-52jp-hrpf-2jff: http redirect location truncation
It converts the allocation of location to be on heap instead of stack
and errors if the location length is greater than 8086 bytes.
2025-03-05 13:42:11 +01:00
Jakub Zelenka
61bb8ef240
Fix GHSA-pcmh-g36c-qc44: http headers without colon
The header line must contain colon otherwise it is invalid and it needs
to fail.

Reviewed-by: Tim Düsterhus <tim@tideways-gmbh.com>
2025-03-05 13:41:22 +01:00
Jakub Zelenka
9fe496696d
Fix GHSA-ghsa-v8xr-gpvj-cx9g: http header folding
This adds HTTP header folding support for HTTP wrapper response
headers.

Reviewed-by: Tim Düsterhus <tim@tideways-gmbh.com>
2025-03-05 12:43:37 +01:00
31 changed files with 1326 additions and 211 deletions

17
NEWS
View file

@ -1,6 +1,6 @@
PHP NEWS
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
?? ??? ????, PHP 8.4.5
13 Mar 2025, PHP 8.4.5
- BCMath:
. Fixed bug GH-17398 (bcmul memory leak). (SakiTakamachi)
@ -21,6 +21,8 @@ PHP NEWS
(DanielEScherzer)
. Fixed bug GH-17866 (zend_mm_heap corrupted error after upgrading from
8.4.3 to 8.4.4). (nielsdos)
. Fixed GHSA-rwp7-7vc6-8477 (Reference counting in php_request_shutdown
causes Use-After-Free). (CVE-2024-11235) (ilutov)
- DOM:
. Fixed bug GH-17609 (Typo in error message: Dom\NO_DEFAULT_NS instead of
@ -49,6 +51,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)
@ -90,6 +97,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). (Bob)

View file

@ -0,0 +1,26 @@
--TEST--
GHSA-rwp7-7vc6-8477: Use-after-free for ??= due to incorrect live-range calculation
--FILE--
<?php
class Foo {
public function foo() {
return $this;
}
public function __set($name, $value) {
throw new Exception('Hello');
}
}
$foo = new Foo();
try {
$foo->foo()->baz ??= 1;
} catch (Exception $e) {
echo $e->getMessage();
}
?>
--EXPECT--
Hello

View file

@ -0,0 +1,24 @@
--TEST--
GHSA-rwp7-7vc6-8477: Use-after-free for ??= due to incorrect live-range calculation
--FILE--
<?php
class Foo {
public int $prop;
public function foo() {
return $this;
}
}
$foo = new Foo();
try {
$foo->foo()->prop ??= 'foo';
} catch (Error $e) {
echo $e->getMessage();
}
?>
--EXPECT--
Cannot assign string to property Foo::$prop of type int

View file

@ -0,0 +1,22 @@
--TEST--
GHSA-rwp7-7vc6-8477: Use-after-free for ??= due to incorrect live-range calculation
--FILE--
<?php
class Foo {
public int $prop;
}
function newFoo() {
return new Foo();
}
try {
newFoo()->prop ??= 'foo';
} catch (Error $e) {
echo $e->getMessage();
}
?>
--EXPECT--
Cannot assign string to property Foo::$prop of type int

View file

@ -20,7 +20,7 @@
#ifndef ZEND_H
#define ZEND_H
#define ZEND_VERSION "4.4.5-dev"
#define ZEND_VERSION "4.4.5"
#define ZEND_ENGINE_3

View file

@ -940,6 +940,14 @@ static void zend_calc_live_ranges(
opnum--;
opline--;
/* SEPARATE always redeclares its op1. For the purposes of live-ranges,
* its declaration is irrelevant. Don't terminate the current live-range
* to avoid breaking special handling of COPY_TMP. */
if (opline->opcode == ZEND_SEPARATE) {
ZEND_ASSERT(opline->op1.var == opline->result.var);
continue;
}
if ((opline->result_type & (IS_TMP_VAR|IS_VAR)) && !is_fake_def(opline)) {
uint32_t var_num = EX_VAR_TO_NUM(opline->result.var) - var_offset;
/* Defs without uses can occur for two reasons: Either because the result is

View file

@ -17,7 +17,7 @@ dnl Basic autoconf initialization, generation of config.nice.
dnl ----------------------------------------------------------------------------
AC_PREREQ([2.68])
AC_INIT([PHP],[8.4.5-dev],[https://github.com/php/php-src/issues],[php],[https://www.php.net])
AC_INIT([PHP],[8.4.5],[https://github.com/php/php-src/issues],[php],[https://www.php.net])
AC_CONFIG_SRCDIR([main/php_version.h])
AC_CONFIG_AUX_DIR([build])
AC_PRESERVE_HELP_ORDER

View file

@ -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--
<?php
if (@!include "./ext/standard/tests/http/server.inc") die('skip server.inc not available');
http_server_skipif();
?>
--FILE--
<?php
require "./ext/standard/tests/http/server.inc";
function genResponses($server) {
$uri = 'http://' . stream_socket_get_name($server, false);
yield "data://text/plain,HTTP/1.1 302 Moved Temporarily\r\nLocation: $uri/document.xml\r\nContent-Type: text/html;charset=utf-16\r\n\r\n";
$xml = <<<'EOT'
<!doctype html>
<html>
<head>
<title>GHSA-p3x9-6h7p-cgfc</title>
<meta charset="utf-8" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
</head>
<body>
<h1>GHSA-p3x9-6h7p-cgfc</h1>
</body>
</html>
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) "<!DOCTYPE html>
<html>
<head>
<title>GHSA-p3x9-6h7p-cgfc</title>
<meta charset="utf-8">
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
</head>
<body>
<h1>GHSA-p3x9-6h7p-cgfc</h1>
</body>
</html>
"

View file

@ -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--
<?php
if (@!include "./ext/standard/tests/http/server.inc") die('skip server.inc not available');
http_server_skipif();
?>
--FILE--
<?php
require "./ext/standard/tests/http/server.inc";
function genResponses($server) {
$uri = 'http://' . stream_socket_get_name($server, false);
yield "data://text/plain,HTTP/1.1 302 Moved Temporarily\r\nLocation: $uri/document.xml\r\nContent-Type: text/html;charset=utf-16\r\n\r\n";
$xml = <<<'EOT'
<!doctype html>
<html>
<head>
<title>GHSA-p3x9-6h7p-cgfc</title>
<meta charset="utf-8" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
</head>
<body>
<h1>GHSA-p3x9-6h7p-cgfc</h1>
</body>
</html>
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) "<!DOCTYPE html>
<html>
<head>
<title>GHSA-p3x9-6h7p-cgfc</title>
<meta charset="utf-8">
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
</head>
<body>
<h1>GHSA-p3x9-6h7p-cgfc</h1>
</body>
</html>
"

View file

@ -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--
<?php
if (@!include "./ext/standard/tests/http/server.inc") die('skip server.inc not available');
http_server_skipif();
?>
--FILE--
<?php
require "./ext/standard/tests/http/server.inc";
function genResponses($server) {
$uri = 'http://' . stream_socket_get_name($server, false);
yield "data://text/plain,HTTP/1.1 302 Moved Temporarily\r\nLocation: $uri/document.xml\r\nContent-Type: text/html;charset=utf-16\r\n\r\n";
$xml = <<<'EOT'
<!doctype html>
<html>
<head>
<title>GHSA-p3x9-6h7p-cgfc</title>
<meta charset="utf-8" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
</head>
<body>
<h1>GHSA-p3x9-6h7p-cgfc</h1>
</body>
</html>
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) "<!DOCTYPE html>
<html>
<head>
<title>GHSA-p3x9-6h7p-cgfc</title>
<meta charset="utf-8">
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
</head>
<body>
<h1>GHSA-p3x9-6h7p-cgfc</h1>
</body>
</html>
"

View file

@ -308,11 +308,21 @@ PHP_LIBXML_API zend_string *php_libxml_sniff_charset_from_stream(const php_strea
if (Z_TYPE(s->wrapperdata) == IS_ARRAY) {
zval *header;
ZEND_HASH_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)) {
return php_libxml_sniff_charset_from_string(Z_STRVAL_P(header) + sizeof(buf) - 1, Z_STRVAL_P(header) + Z_STRLEN_P(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) {
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) {
return NULL;
}
if (zend_string_starts_with_literal_ci(Z_STR_P(header), "content-type:")) {
return php_libxml_sniff_charset_from_string(Z_STRVAL_P(header) + strlen("content-type:"), Z_STRVAL_P(header) + Z_STRLEN_P(header));
}
}
} ZEND_HASH_FOREACH_END();
}

View file

@ -27,6 +27,6 @@ try {
echo $e->getMessage() . "\n";
}
--EXPECT--
--EXPECTF--
"This is a quote"""
SQLSTATE[HY000]: General error: 7 incomplete multibyte character
SQLSTATE[HY000]: General error: 7 %r(incomplete|invalid)%r multibyte character

View file

@ -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++;
@ -143,6 +144,214 @@ static zend_result php_stream_handle_proxy_authorization_header(const char *s, s
return FAILURE;
}
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)) {
bool decode = true;
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,
@ -155,11 +364,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;
@ -170,8 +380,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;
@ -360,6 +568,8 @@ finish:
}
}
php_stream_http_response_header_info_init(&header_info);
if (stream == NULL)
goto out;
@ -660,8 +870,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);
}
@ -744,140 +952,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)) {
bool decode = true;
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);
@ -893,29 +1064,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;
}
@ -927,6 +1104,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++; \
@ -949,6 +1127,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);
}
@ -961,6 +1140,10 @@ out:
efree(http_header_line);
}
if (header_info.location != NULL) {
efree(header_info.location);
}
if (resource) {
php_url_free(resource);
}
@ -969,7 +1152,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)
@ -985,8 +1168,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.

View file

@ -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

View file

@ -14,27 +14,14 @@ $responses = array(
['pid' => $pid, 'uri' => $uri] = http_server($responses, $output);
var_dump(http_get_last_response_headers());
var_dump(file_get_contents($uri));
var_dump($http_response_header);
var_dump(http_get_last_response_headers());
http_server_kill($pid);
?>
--EXPECT--
NULL
string(0) ""
array(2) {
array(1) {
[0]=>
string(15) "HTTP/1.0 200 Ok"
[1]=>
string(14) "Content-Length"
}
array(2) {
[0]=>
string(15) "HTTP/1.0 200 Ok"
[1]=>
string(14) "Content-Length"
}

View file

@ -0,0 +1,58 @@
--TEST--
GHSA-52jp-hrpf-2jff: HTTP stream wrapper truncate redirect location to 1024 bytes (success)
--FILE--
<?php
$serverCode = <<<'CODE'
$ctxt = stream_context_create([
"socket" => [
"tcp_nodelay" => true
]
]);
$server = stream_socket_server(
"tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt);
phpt_notify_server_start($server);
$conn = stream_socket_accept($server);
phpt_notify(message:"server-accepted");
$loc = str_repeat("y", 8000);
fwrite($conn, "HTTP/1.0 301 Ok\r\nContent-Type: text/html;\r\nLocation: $loc\r\n\r\nbody\r\n");
CODE;
$clientCode = <<<'CODE'
function stream_notification_callback($notification_code, $severity, $message, $message_code, $bytes_transferred, $bytes_max) {
switch($notification_code) {
case STREAM_NOTIFY_MIME_TYPE_IS:
echo "Found the mime-type: ", $message, PHP_EOL;
break;
case STREAM_NOTIFY_REDIRECTED:
echo "Redirected: ";
var_dump($message);
}
}
$ctx = stream_context_create();
stream_context_set_params($ctx, array("notification" => "stream_notification_callback"));
var_dump(trim(file_get_contents("http://{{ ADDR }}", false, $ctx)));
var_dump($http_response_header);
CODE;
include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__);
ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
?>
--EXPECTF--
Found the mime-type: text/html;
Redirected: string(8000) "%s"
Warning: file_get_contents(http://127.0.0.1:%d): Failed to open stream: %s
string(0) ""
array(3) {
[0]=>
string(15) "HTTP/1.0 301 Ok"
[1]=>
string(24) "Content-Type: text/html;"
[2]=>
string(8010) "Location: %s"
}

View file

@ -0,0 +1,55 @@
--TEST--
GHSA-52jp-hrpf-2jff: HTTP stream wrapper truncate redirect location to 1024 bytes (over limit)
--FILE--
<?php
$serverCode = <<<'CODE'
$ctxt = stream_context_create([
"socket" => [
"tcp_nodelay" => true
]
]);
$server = stream_socket_server(
"tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt);
phpt_notify_server_start($server);
$conn = stream_socket_accept($server);
phpt_notify(message:"server-accepted");
$loc = str_repeat("y", 9000);
fwrite($conn, "HTTP/1.0 301 Ok\r\nContent-Type: text/html;\r\nLocation: $loc\r\n\r\nbody\r\n");
CODE;
$clientCode = <<<'CODE'
function stream_notification_callback($notification_code, $severity, $message, $message_code, $bytes_transferred, $bytes_max) {
switch($notification_code) {
case STREAM_NOTIFY_MIME_TYPE_IS:
echo "Found the mime-type: ", $message, PHP_EOL;
break;
case STREAM_NOTIFY_REDIRECTED:
echo "Redirected: ";
var_dump($message);
}
}
$ctx = stream_context_create();
stream_context_set_params($ctx, array("notification" => "stream_notification_callback"));
var_dump(trim(file_get_contents("http://{{ ADDR }}", false, $ctx)));
var_dump($http_response_header);
CODE;
include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__);
ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
?>
--EXPECTF--
Found the mime-type: text/html;
Warning: file_get_contents(http://127.0.0.1:%d): Failed to open stream: HTTP Location header size is over the limit of 8182 bytes in %s
string(0) ""
array(2) {
[0]=>
string(15) "HTTP/1.0 301 Ok"
[1]=>
string(24) "Content-Type: text/html;"
}

View file

@ -0,0 +1,65 @@
--TEST--
GHSA-hgf5-96fm-v528: Stream HTTP wrapper header check might omit basic auth header (incorrect inside pos)
--FILE--
<?php
$serverCode = <<<'CODE'
$ctxt = stream_context_create([
"socket" => [
"tcp_nodelay" => true
]
]);
$server = stream_socket_server(
"tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt);
phpt_notify_server_start($server);
$conn = stream_socket_accept($server);
phpt_notify(message:"server-accepted");
$result = fread($conn, 1024);
$encoded_result = base64_encode($result);
fwrite($conn, "HTTP/1.0 200 Ok\r\nContent-Type: text/html; charset=utf-8\r\n\r\n$encoded_result\r\n");
CODE;
$clientCode = <<<'CODE'
$opts = [
"http" => [
"method" => "GET",
"header" => "Cookie: foo=bar\nauthorization:x\r\n"
]
];
$ctx = stream_context_create($opts);
var_dump(explode("\r\n", base64_decode(file_get_contents("http://user:pwd@{{ ADDR }}", false, $ctx))));
var_dump($http_response_header);
CODE;
include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__);
ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
?>
--EXPECTF--
array(7) {
[0]=>
string(14) "GET / HTTP/1.1"
[1]=>
string(33) "Authorization: Basic dXNlcjpwd2Q="
[2]=>
string(21) "Host: 127.0.0.1:%d"
[3]=>
string(17) "Connection: close"
[4]=>
string(31) "Cookie: foo=bar
authorization:x"
[5]=>
string(0) ""
[6]=>
string(0) ""
}
array(2) {
[0]=>
string(15) "HTTP/1.0 200 Ok"
[1]=>
string(38) "Content-Type: text/html; charset=utf-8"
}

View file

@ -0,0 +1,62 @@
--TEST--
GHSA-hgf5-96fm-v528: Header parser of http stream wrapper does not handle folded headers (correct start pos)
--FILE--
<?php
$serverCode = <<<'CODE'
$ctxt = stream_context_create([
"socket" => [
"tcp_nodelay" => true
]
]);
$server = stream_socket_server(
"tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt);
phpt_notify_server_start($server);
$conn = stream_socket_accept($server);
phpt_notify(message:"server-accepted");
$result = fread($conn, 1024);
$encoded_result = base64_encode($result);
fwrite($conn, "HTTP/1.0 200 Ok\r\nContent-Type: text/html; charset=utf-8\r\n\r\n$encoded_result\r\n");
CODE;
$clientCode = <<<'CODE'
$opts = [
"http" => [
"method" => "GET",
"header" => "Authorization: Bearer x\r\n"
]
];
$ctx = stream_context_create($opts);
var_dump(explode("\r\n", base64_decode(file_get_contents("http://user:pwd@{{ ADDR }}", false, $ctx))));
var_dump($http_response_header);
CODE;
include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__);
ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
?>
--EXPECTF--
array(6) {
[0]=>
string(14) "GET / HTTP/1.1"
[1]=>
string(21) "Host: 127.0.0.1:%d"
[2]=>
string(17) "Connection: close"
[3]=>
string(23) "Authorization: Bearer x"
[4]=>
string(0) ""
[5]=>
string(0) ""
}
array(2) {
[0]=>
string(15) "HTTP/1.0 200 Ok"
[1]=>
string(38) "Content-Type: text/html; charset=utf-8"
}

View file

@ -0,0 +1,64 @@
--TEST--
GHSA-hgf5-96fm-v528: Header parser of http stream wrapper does not handle folded headers (correct middle pos)
--FILE--
<?php
$serverCode = <<<'CODE'
$ctxt = stream_context_create([
"socket" => [
"tcp_nodelay" => true
]
]);
$server = stream_socket_server(
"tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt);
phpt_notify_server_start($server);
$conn = stream_socket_accept($server);
phpt_notify(message:"server-accepted");
$result = fread($conn, 1024);
$encoded_result = base64_encode($result);
fwrite($conn, "HTTP/1.0 200 Ok\r\nContent-Type: text/html; charset=utf-8\r\n\r\n$encoded_result\r\n");
CODE;
$clientCode = <<<'CODE'
$opts = [
"http" => [
"method" => "GET",
"header" => "Cookie: x=y\r\nAuthorization: Bearer x\r\n"
]
];
$ctx = stream_context_create($opts);
var_dump(explode("\r\n", base64_decode(file_get_contents("http://user:pwd@{{ ADDR }}", false, $ctx))));
var_dump($http_response_header);
CODE;
include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__);
ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
?>
--EXPECTF--
array(7) {
[0]=>
string(14) "GET / HTTP/1.1"
[1]=>
string(21) "Host: 127.0.0.1:%d"
[2]=>
string(17) "Connection: close"
[3]=>
string(11) "Cookie: x=y"
[4]=>
string(23) "Authorization: Bearer x"
[5]=>
string(0) ""
[6]=>
string(0) ""
}
array(2) {
[0]=>
string(15) "HTTP/1.0 200 Ok"
[1]=>
string(38) "Content-Type: text/html; charset=utf-8"
}

View file

@ -0,0 +1,51 @@
--TEST--
GHSA-pcmh-g36c-qc44: Header parser of http stream wrapper does not verify header name and colon (colon)
--FILE--
<?php
$serverCode = <<<'CODE'
$ctxt = stream_context_create([
"socket" => [
"tcp_nodelay" => true
]
]);
$server = stream_socket_server(
"tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt);
phpt_notify_server_start($server);
$conn = stream_socket_accept($server);
phpt_notify(message:"server-accepted");
fwrite($conn, "HTTP/1.0 200 Ok\r\nContent-Type: text/html\r\nWrong-Header\r\nGood-Header: test\r\n\r\nbody\r\n");
CODE;
$clientCode = <<<'CODE'
function stream_notification_callback($notification_code, $severity, $message, $message_code, $bytes_transferred, $bytes_max) {
switch($notification_code) {
case STREAM_NOTIFY_MIME_TYPE_IS:
echo "Found the mime-type: ", $message, PHP_EOL;
break;
}
}
$ctx = stream_context_create();
stream_context_set_params($ctx, array("notification" => "stream_notification_callback"));
var_dump(file_get_contents("http://{{ ADDR }}", false, $ctx));
var_dump($http_response_header);
CODE;
include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__);
ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
?>
--EXPECTF--
Found the mime-type: text/html
Warning: file_get_contents(http://127.0.0.1:%d): Failed to open stream: HTTP invalid response format (no colon in header line)! in %s
bool(false)
array(2) {
[0]=>
string(15) "HTTP/1.0 200 Ok"
[1]=>
string(23) "Content-Type: text/html"
}

View file

@ -0,0 +1,51 @@
--TEST--
GHSA-pcmh-g36c-qc44: Header parser of http stream wrapper does not verify header name and colon (name)
--FILE--
<?php
$serverCode = <<<'CODE'
$ctxt = stream_context_create([
"socket" => [
"tcp_nodelay" => true
]
]);
$server = stream_socket_server(
"tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt);
phpt_notify_server_start($server);
$conn = stream_socket_accept($server);
phpt_notify(message:"server-accepted");
fwrite($conn, "HTTP/1.0 200 Ok\r\nContent-Type: text/html\r\nWrong-Header : test\r\nGood-Header: test\r\n\r\nbody\r\n");
CODE;
$clientCode = <<<'CODE'
function stream_notification_callback($notification_code, $severity, $message, $message_code, $bytes_transferred, $bytes_max) {
switch($notification_code) {
case STREAM_NOTIFY_MIME_TYPE_IS:
echo "Found the mime-type: ", $message, PHP_EOL;
break;
}
}
$ctx = stream_context_create();
stream_context_set_params($ctx, array("notification" => "stream_notification_callback"));
var_dump(file_get_contents("http://{{ ADDR }}", false, $ctx));
var_dump($http_response_header);
CODE;
include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__);
ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
?>
--EXPECTF--
Found the mime-type: text/html
Warning: file_get_contents(http://127.0.0.1:%d): Failed to open stream: HTTP invalid response format (space in header name)! in %s
bool(false)
array(2) {
[0]=>
string(15) "HTTP/1.0 200 Ok"
[1]=>
string(23) "Content-Type: text/html"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -1,41 +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);
var_dump(http_get_last_response_headers());
$f = file_get_contents($uri);
var_dump($f);
var_dump($http_response_header);
var_dump(http_get_last_response_headers());
http_server_kill($pid);
?>
--EXPECT--
NULL
string(4) "Body"
array(2) {
[0]=>
string(15) "HTTP/1.0 200 Ok"
[1]=>
string(0) ""
}
array(2) {
[0]=>
string(15) "HTTP/1.0 200 Ok"
[1]=>
string(0) ""
}

View file

@ -0,0 +1,42 @@
--TEST--
XML_OPTION_SKIP_TAGSTART bounds
--EXTENSIONS--
xml
--FILE--
<?php
$sample = "<?xml version=\"1.0\"?><test><child/></test>";
$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)
}
}

View file

@ -663,9 +663,11 @@ void xml_startElementHandler(void *userData, const XML_Char *name, const XML_Cha
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);
@ -747,12 +749,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);

View file

@ -3,6 +3,6 @@
#define PHP_MAJOR_VERSION 8
#define PHP_MINOR_VERSION 4
#define PHP_RELEASE_VERSION 5
#define PHP_EXTRA_VERSION "-dev"
#define PHP_VERSION "8.4.5-dev"
#define PHP_EXTRA_VERSION ""
#define PHP_VERSION "8.4.5"
#define PHP_VERSION_ID 80405