Skip to content

Commit e81d0cd

Browse files
bukkaremicollet
authored andcommitted
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 <[email protected]> (cherry picked from commit 0548c4c1756724a89ef8310709419b08aadb2b3b)
1 parent 4fec085 commit e81d0cd

File tree

5 files changed

+154
-25
lines changed

5 files changed

+154
-25
lines changed

ext/standard/http_fopen_wrapper.c

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ static zend_bool check_has_header(const char *headers, const char *header) {
117117
typedef struct _php_stream_http_response_header_info {
118118
php_stream_filter *transfer_encoding;
119119
size_t file_size;
120+
bool error;
120121
bool follow_location;
121122
char location[HTTP_HEADER_BLOCK_SIZE];
122123
} php_stream_http_response_header_info;
@@ -126,6 +127,7 @@ static void php_stream_http_response_header_info_init(
126127
{
127128
header_info->transfer_encoding = NULL;
128129
header_info->file_size = 0;
130+
header_info->error = false;
129131
header_info->follow_location = 1;
130132
header_info->location[0] = '\0';
131133
}
@@ -163,10 +165,11 @@ static bool php_stream_http_response_header_trim(char *http_header_line,
163165
/* Process folding headers of the current line and if there are none, parse last full response
164166
* header line. It returns NULL if the last header is finished, otherwise it returns updated
165167
* last header line. */
166-
static zend_string *php_stream_http_response_headers_parse(php_stream *stream,
167-
php_stream_context *context, int options, zend_string *last_header_line_str,
168-
char *header_line, size_t *header_line_length, int response_code,
169-
zval *response_header, php_stream_http_response_header_info *header_info)
168+
static zend_string *php_stream_http_response_headers_parse(php_stream_wrapper *wrapper,
169+
php_stream *stream, php_stream_context *context, int options,
170+
zend_string *last_header_line_str, char *header_line, size_t *header_line_length,
171+
int response_code, zval *response_header,
172+
php_stream_http_response_header_info *header_info)
170173
{
171174
char *last_header_line = ZSTR_VAL(last_header_line_str);
172175
size_t last_header_line_length = ZSTR_LEN(last_header_line_str);
@@ -208,6 +211,19 @@ static zend_string *php_stream_http_response_headers_parse(php_stream *stream,
208211
/* Find header separator position. */
209212
char *last_header_value = memchr(last_header_line, ':', last_header_line_length);
210213
if (last_header_value) {
214+
/* Verify there is no space in header name */
215+
char *last_header_name = last_header_line + 1;
216+
while (last_header_name < last_header_value) {
217+
if (*last_header_name == ' ' || *last_header_name == '\t') {
218+
header_info->error = true;
219+
php_stream_wrapper_log_error(wrapper, options,
220+
"HTTP invalid response format (space in header name)!");
221+
zend_string_efree(last_header_line_str);
222+
return NULL;
223+
}
224+
++last_header_name;
225+
}
226+
211227
last_header_value++; /* Skip ':'. */
212228

213229
/* Strip leading whitespace. */
@@ -216,9 +232,12 @@ static zend_string *php_stream_http_response_headers_parse(php_stream *stream,
216232
last_header_value++;
217233
}
218234
} else {
219-
/* There is no colon. Set the value to the end of the header line, which is effectively
220-
* an empty string. */
221-
last_header_value = last_header_line_end;
235+
/* There is no colon which means invalid response so error. */
236+
header_info->error = true;
237+
php_stream_wrapper_log_error(wrapper, options,
238+
"HTTP invalid response format (no colon in header line)!");
239+
zend_string_efree(last_header_line_str);
240+
return NULL;
222241
}
223242

224243
bool store_header = true;
@@ -928,10 +947,16 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper,
928947

929948
if (last_header_line_str != NULL) {
930949
/* Parse last header line. */
931-
last_header_line_str = php_stream_http_response_headers_parse(stream, context,
932-
options, last_header_line_str, http_header_line, &http_header_line_length,
933-
response_code, response_header, &header_info);
934-
if (last_header_line_str != NULL) {
950+
last_header_line_str = php_stream_http_response_headers_parse(wrapper, stream,
951+
context, options, last_header_line_str, http_header_line,
952+
&http_header_line_length, response_code, response_header, &header_info);
953+
if (EXPECTED(last_header_line_str == NULL)) {
954+
if (UNEXPECTED(header_info.error)) {
955+
php_stream_close(stream);
956+
stream = NULL;
957+
goto out;
958+
}
959+
} else {
935960
/* Folding header present so continue. */
936961
continue;
937962
}
@@ -961,8 +986,8 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper,
961986

962987
/* If the stream was closed early, we still want to process the last line to keep BC. */
963988
if (last_header_line_str != NULL) {
964-
php_stream_http_response_headers_parse(stream, context, options, last_header_line_str,
965-
NULL, NULL, response_code, response_header, &header_info);
989+
php_stream_http_response_headers_parse(wrapper, stream, context, options,
990+
last_header_line_str, NULL, NULL, response_code, response_header, &header_info);
966991
}
967992

968993
if (!reqok || (header_info.location[0] != '\0' && header_info.follow_location)) {

ext/standard/tests/http/bug47021.phpt

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,23 +70,27 @@ do_test(1, true);
7070
echo "\n";
7171

7272
?>
73-
--EXPECT--
73+
--EXPECTF--
74+
7475
Type='text/plain'
7576
Hello
76-
Size=5
77-
World
77+
78+
Warning: file_get_contents(http://%s:%d): Failed to open stream: HTTP invalid response format (no colon in header line)! in %s
79+
7880

7981
Type='text/plain'
8082
Hello
81-
Size=5
82-
World
83+
84+
Warning: file_get_contents(http://%s:%d): Failed to open stream: HTTP invalid response format (no colon in header line)! in %s
85+
8386

8487
Type='text/plain'
8588
Hello
86-
Size=5
87-
World
89+
90+
Warning: file_get_contents(http://%s:%d): Failed to open stream: HTTP invalid response format (no colon in header line)! in %s
91+
8892

8993
Type='text/plain'
9094
Hello
91-
Size=5
92-
World
95+
96+
Warning: file_get_contents(http://%s:%d): Failed to open stream: HTTP invalid response format (no colon in header line)! in %s

ext/standard/tests/http/bug75535.phpt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,7 @@ http_server_kill($pid);
2121

2222
--EXPECT--
2323
string(0) ""
24-
array(2) {
24+
array(1) {
2525
[0]=>
2626
string(15) "HTTP/1.0 200 Ok"
27-
[1]=>
28-
string(14) "Content-Length"
2927
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
--TEST--
2+
GHSA-pcmh-g36c-qc44: Header parser of http stream wrapper does not verify header name and colon (colon)
3+
--FILE--
4+
<?php
5+
$serverCode = <<<'CODE'
6+
$ctxt = stream_context_create([
7+
"socket" => [
8+
"tcp_nodelay" => true
9+
]
10+
]);
11+
12+
$server = stream_socket_server(
13+
"tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt);
14+
phpt_notify_server_start($server);
15+
16+
$conn = stream_socket_accept($server);
17+
18+
phpt_notify(message:"server-accepted");
19+
20+
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");
21+
CODE;
22+
23+
$clientCode = <<<'CODE'
24+
function stream_notification_callback($notification_code, $severity, $message, $message_code, $bytes_transferred, $bytes_max) {
25+
switch($notification_code) {
26+
case STREAM_NOTIFY_MIME_TYPE_IS:
27+
echo "Found the mime-type: ", $message, PHP_EOL;
28+
break;
29+
}
30+
}
31+
32+
$ctx = stream_context_create();
33+
stream_context_set_params($ctx, array("notification" => "stream_notification_callback"));
34+
var_dump(file_get_contents("http://{{ ADDR }}", false, $ctx));
35+
var_dump($http_response_header);
36+
CODE;
37+
38+
include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__);
39+
ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
40+
?>
41+
--EXPECTF--
42+
Found the mime-type: text/html
43+
44+
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
45+
bool(false)
46+
array(2) {
47+
[0]=>
48+
string(15) "HTTP/1.0 200 Ok"
49+
[1]=>
50+
string(23) "Content-Type: text/html"
51+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
--TEST--
2+
GHSA-pcmh-g36c-qc44: Header parser of http stream wrapper does not verify header name and colon (name)
3+
--FILE--
4+
<?php
5+
$serverCode = <<<'CODE'
6+
$ctxt = stream_context_create([
7+
"socket" => [
8+
"tcp_nodelay" => true
9+
]
10+
]);
11+
12+
$server = stream_socket_server(
13+
"tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt);
14+
phpt_notify_server_start($server);
15+
16+
$conn = stream_socket_accept($server);
17+
18+
phpt_notify(message:"server-accepted");
19+
20+
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");
21+
CODE;
22+
23+
$clientCode = <<<'CODE'
24+
function stream_notification_callback($notification_code, $severity, $message, $message_code, $bytes_transferred, $bytes_max) {
25+
switch($notification_code) {
26+
case STREAM_NOTIFY_MIME_TYPE_IS:
27+
echo "Found the mime-type: ", $message, PHP_EOL;
28+
break;
29+
}
30+
}
31+
32+
$ctx = stream_context_create();
33+
stream_context_set_params($ctx, array("notification" => "stream_notification_callback"));
34+
var_dump(file_get_contents("http://{{ ADDR }}", false, $ctx));
35+
var_dump($http_response_header);
36+
CODE;
37+
38+
include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__);
39+
ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
40+
?>
41+
--EXPECTF--
42+
Found the mime-type: text/html
43+
44+
Warning: file_get_contents(http://127.0.0.1:%d): Failed to open stream: HTTP invalid response format (space in header name)! in %s
45+
bool(false)
46+
array(2) {
47+
[0]=>
48+
string(15) "HTTP/1.0 200 Ok"
49+
[1]=>
50+
string(23) "Content-Type: text/html"
51+
}

0 commit comments

Comments
 (0)