Skip to content

Commit 4058d20

Browse files
committed
Merge branch 'PHP-8.0' into PHP-8.1
2 parents d94ddbe + 937b1e3 commit 4058d20

7 files changed

+266
-16
lines changed

NEWS

+4
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ PHP NEWS
6060
. Fixed bug #81746 (1-byte array overrun in common path resolve code).
6161
(CVE-2023-0568). (Niels Dossche)
6262

63+
- SAPI:
64+
. Fixed bug GHSA-54hq-v5wp-fqgv (DOS vulnerability when parsing multipart
65+
request body). (CVE-2023-0662) (Jakub Zelenka)
66+
6367
02 Feb 2023, PHP 8.1.15
6468

6569
- Apache:

main/main.c

+1
Original file line numberDiff line numberDiff line change
@@ -746,6 +746,7 @@ PHP_INI_BEGIN()
746746
PHP_INI_ENTRY("disable_functions", "", PHP_INI_SYSTEM, NULL)
747747
PHP_INI_ENTRY("disable_classes", "", PHP_INI_SYSTEM, NULL)
748748
PHP_INI_ENTRY("max_file_uploads", "20", PHP_INI_SYSTEM|PHP_INI_PERDIR, NULL)
749+
PHP_INI_ENTRY("max_multipart_body_parts", "-1", PHP_INI_SYSTEM|PHP_INI_PERDIR, NULL)
749750

750751
STD_PHP_INI_BOOLEAN("allow_url_fopen", "1", PHP_INI_SYSTEM, OnUpdateBool, allow_url_fopen, php_core_globals, core_globals)
751752
STD_PHP_INI_BOOLEAN("allow_url_include", "0", PHP_INI_SYSTEM, OnUpdateBool, allow_url_include, php_core_globals, core_globals)

main/rfc1867.c

+15-1
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,7 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */
686686
void *event_extra_data = NULL;
687687
unsigned int llen = 0;
688688
int upload_cnt = INI_INT("max_file_uploads");
689+
int body_parts_cnt = INI_INT("max_multipart_body_parts");
689690
const zend_encoding *internal_encoding = zend_multibyte_get_internal_encoding();
690691
php_rfc1867_getword_t getword;
691692
php_rfc1867_getword_conf_t getword_conf;
@@ -707,6 +708,11 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */
707708
return;
708709
}
709710

711+
if (body_parts_cnt < 0) {
712+
body_parts_cnt = PG(max_input_vars) + upload_cnt;
713+
}
714+
int body_parts_limit = body_parts_cnt;
715+
710716
/* Get the boundary */
711717
boundary = strstr(content_type_dup, "boundary");
712718
if (!boundary) {
@@ -791,6 +797,11 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */
791797
char *pair = NULL;
792798
int end = 0;
793799

800+
if (--body_parts_cnt < 0) {
801+
php_error_docref(NULL, E_WARNING, "Multipart body parts limit exceeded %d. To increase the limit change max_multipart_body_parts in php.ini.", body_parts_limit);
802+
goto fileupload_done;
803+
}
804+
794805
while (isspace(*cd)) {
795806
++cd;
796807
}
@@ -910,7 +921,10 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */
910921
skip_upload = 1;
911922
} else if (upload_cnt <= 0) {
912923
skip_upload = 1;
913-
sapi_module.sapi_error(E_WARNING, "Maximum number of allowable file uploads has been exceeded");
924+
if (upload_cnt == 0) {
925+
--upload_cnt;
926+
sapi_module.sapi_error(E_WARNING, "Maximum number of allowable file uploads has been exceeded");
927+
}
914928
}
915929

916930
/* Return with an error if the posted data is garbled */
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
--TEST--
2+
FPM: GHSA-54hq-v5wp-fqgv - max_multipart_body_parts ini custom value
3+
--SKIPIF--
4+
<?php include "skipif.inc"; ?>
5+
--FILE--
6+
<?php
7+
8+
require_once "tester.inc";
9+
10+
$cfg = <<<EOT
11+
[global]
12+
error_log = {{FILE:LOG}}
13+
[unconfined]
14+
listen = {{ADDR}}
15+
pm = dynamic
16+
pm.max_children = 5
17+
pm.start_servers = 1
18+
pm.min_spare_servers = 1
19+
pm.max_spare_servers = 3
20+
php_admin_value[html_errors] = false
21+
php_admin_value[max_input_vars] = 20
22+
php_admin_value[max_file_uploads] = 5
23+
php_admin_value[max_multipart_body_parts] = 10
24+
php_flag[display_errors] = On
25+
EOT;
26+
27+
$code = <<<EOT
28+
<?php
29+
var_dump(count(\$_POST));
30+
EOT;
31+
32+
$tester = new FPM\Tester($cfg, $code);
33+
$tester->start();
34+
$tester->expectLogStartNotices();
35+
echo $tester
36+
->request(stdin: [
37+
'parts' => [
38+
'count' => 30,
39+
]
40+
])
41+
->getBody();
42+
$tester->terminate();
43+
$tester->close();
44+
45+
?>
46+
--EXPECT--
47+
Warning: Unknown: Multipart body parts limit exceeded 10. To increase the limit change max_multipart_body_parts in php.ini. in Unknown on line 0
48+
int(10)
49+
--CLEAN--
50+
<?php
51+
require_once "tester.inc";
52+
FPM\Tester::clean();
53+
?>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
--TEST--
2+
FPM: GHSA-54hq-v5wp-fqgv - max_multipart_body_parts ini default
3+
--SKIPIF--
4+
<?php include "skipif.inc"; ?>
5+
--FILE--
6+
<?php
7+
8+
require_once "tester.inc";
9+
10+
$cfg = <<<EOT
11+
[global]
12+
error_log = {{FILE:LOG}}
13+
[unconfined]
14+
listen = {{ADDR}}
15+
pm = dynamic
16+
pm.max_children = 5
17+
pm.start_servers = 1
18+
pm.min_spare_servers = 1
19+
pm.max_spare_servers = 3
20+
php_admin_value[html_errors] = false
21+
php_admin_value[max_input_vars] = 20
22+
php_admin_value[max_file_uploads] = 5
23+
php_flag[display_errors] = On
24+
EOT;
25+
26+
$code = <<<EOT
27+
<?php
28+
var_dump(count(\$_POST));
29+
EOT;
30+
31+
$tester = new FPM\Tester($cfg, $code);
32+
$tester->start();
33+
$tester->expectLogStartNotices();
34+
echo $tester
35+
->request(stdin: [
36+
'parts' => [
37+
'count' => 30,
38+
]
39+
])
40+
->getBody();
41+
$tester->terminate();
42+
$tester->close();
43+
44+
?>
45+
--EXPECT--
46+
Warning: Unknown: Input variables exceeded 20. To increase the limit change max_input_vars in php.ini. in Unknown on line 0
47+
48+
Warning: Unknown: Multipart body parts limit exceeded 25. To increase the limit change max_multipart_body_parts in php.ini. in Unknown on line 0
49+
int(20)
50+
--CLEAN--
51+
<?php
52+
require_once "tester.inc";
53+
FPM\Tester::clean();
54+
?>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
--TEST--
2+
FPM: GHSA-54hq-v5wp-fqgv - exceeding max_file_uploads
3+
--SKIPIF--
4+
<?php include "skipif.inc"; ?>
5+
--FILE--
6+
<?php
7+
8+
require_once "tester.inc";
9+
10+
$cfg = <<<EOT
11+
[global]
12+
error_log = {{FILE:LOG}}
13+
[unconfined]
14+
listen = {{ADDR}}
15+
pm = dynamic
16+
pm.max_children = 5
17+
pm.start_servers = 1
18+
pm.min_spare_servers = 1
19+
pm.max_spare_servers = 3
20+
php_admin_value[html_errors] = false
21+
php_admin_value[max_file_uploads] = 5
22+
php_flag[display_errors] = On
23+
EOT;
24+
25+
$code = <<<EOT
26+
<?php
27+
var_dump(count(\$_FILES));
28+
EOT;
29+
30+
$tester = new FPM\Tester($cfg, $code);
31+
$tester->start();
32+
$tester->expectLogStartNotices();
33+
echo $tester
34+
->request(stdin: [
35+
'parts' => [
36+
'count' => 10,
37+
'param' => 'filename'
38+
]
39+
])
40+
->getBody();
41+
$tester->terminate();
42+
$tester->close();
43+
44+
?>
45+
--EXPECT--
46+
Warning: Maximum number of allowable file uploads has been exceeded in Unknown on line 0
47+
int(5)
48+
--CLEAN--
49+
<?php
50+
require_once "tester.inc";
51+
FPM\Tester::clean();
52+
?>

sapi/fpm/tests/tester.inc

+87-15
Original file line numberDiff line numberDiff line change
@@ -574,14 +574,16 @@ class Tester
574574
* @param array $headers
575575
* @param string|null $uri
576576
* @param string|null $scriptFilename
577+
* @param string|null $stdin
577578
*
578579
* @return array
579580
*/
580581
private function getRequestParams(
581582
string $query = '',
582583
array $headers = [],
583584
string $uri = null,
584-
string $scriptFilename = null
585+
string $scriptFilename = null,
586+
?string $stdin = null
585587
): array {
586588
if (is_null($uri)) {
587589
$uri = $this->makeSourceFile();
@@ -590,7 +592,7 @@ class Tester
590592
$params = array_merge(
591593
[
592594
'GATEWAY_INTERFACE' => 'FastCGI/1.0',
593-
'REQUEST_METHOD' => 'GET',
595+
'REQUEST_METHOD' => is_null($stdin) ? 'GET' : 'POST',
594596
'SCRIPT_FILENAME' => $scriptFilename ?: $uri,
595597
'SCRIPT_NAME' => $uri,
596598
'QUERY_STRING' => $query,
@@ -605,7 +607,7 @@ class Tester
605607
'SERVER_PROTOCOL' => 'HTTP/1.1',
606608
'DOCUMENT_ROOT' => __DIR__,
607609
'CONTENT_TYPE' => '',
608-
'CONTENT_LENGTH' => 0
610+
'CONTENT_LENGTH' => strlen($stdin ?? "") // Default to 0
609611
],
610612
$headers
611613
);
@@ -615,21 +617,86 @@ class Tester
615617
});
616618
}
617619

620+
/**
621+
* Parse stdin and generate data for multipart config.
622+
*
623+
* @param array $stdin
624+
* @param array $headers
625+
*
626+
* @return void
627+
* @throws \Exception
628+
*/
629+
private function parseStdin(array $stdin, array &$headers)
630+
{
631+
$parts = $stdin['parts'] ?? null;
632+
if (empty($parts)) {
633+
throw new \Exception('The stdin array needs to contain parts');
634+
}
635+
$boundary = $stdin['boundary'] ?? 'AaB03x';
636+
if ( ! isset($headers['CONTENT_TYPE'])) {
637+
$headers['CONTENT_TYPE'] = 'multipart/form-data; boundary=' . $boundary;
638+
}
639+
$count = $parts['count'] ?? null;
640+
if ( ! is_null($count)) {
641+
$dispositionType = $parts['disposition'] ?? 'form-data';
642+
$dispositionParam = $parts['param'] ?? 'name';
643+
$namePrefix = $parts['prefix'] ?? 'f';
644+
$nameSuffix = $parts['suffix'] ?? '';
645+
$value = $parts['value'] ?? 'test';
646+
$parts = [];
647+
for ($i = 0; $i < $count; $i++) {
648+
$parts[] = [
649+
'disposition' => $dispositionType,
650+
'param' => $dispositionParam,
651+
'name' => "$namePrefix$i$nameSuffix",
652+
'value' => $value
653+
];
654+
}
655+
}
656+
$out = '';
657+
$nl = "\r\n";
658+
foreach ($parts as $part) {
659+
if (!is_array($part)) {
660+
$part = ['name' => $part];
661+
} elseif ( ! isset($part['name'])) {
662+
throw new \Exception('Each part has to have a name');
663+
}
664+
$name = $part['name'];
665+
$dispositionType = $part['disposition'] ?? 'form-data';
666+
$dispositionParam = $part['param'] ?? 'name';
667+
$value = $part['value'] ?? 'test';
668+
$partHeaders = $part['headers'] ?? [];
669+
670+
$out .= "--$boundary$nl";
671+
$out .= "Content-disposition: $dispositionType; $dispositionParam=\"$name\"$nl";
672+
foreach ($partHeaders as $headerName => $headerValue) {
673+
$out .= "$headerName: $headerValue$nl";
674+
}
675+
$out .= $nl;
676+
$out .= "$value$nl";
677+
}
678+
$out .= "--$boundary--$nl";
679+
680+
return $out;
681+
}
682+
618683
/**
619684
* Execute request.
620685
*
621-
* @param string $query
622-
* @param array $headers
623-
* @param string|null $uri
624-
* @param string|null $address
625-
* @param string|null $successMessage
626-
* @param string|null $errorMessagereadLimit
627-
* @param bool $connKeepAlive
628-
* @param string|null $scriptFilename = null
629-
* @param bool $expectError
630-
* @param int $readLimit
686+
* @param string $query
687+
* @param array $headers
688+
* @param string|null $uri
689+
* @param string|null $address
690+
* @param string|null $successMessage
691+
* @param string|null $errorMessage
692+
* @param bool $connKeepAlive
693+
* @param string|null $scriptFilename = null
694+
* @param string|array|null $stdin = null
695+
* @param bool $expectError
696+
* @param int $readLimit
631697
*
632698
* @return Response
699+
* @throws \Exception
633700
*/
634701
public function request(
635702
string $query = '',
@@ -640,19 +707,24 @@ class Tester
640707
string $errorMessage = null,
641708
bool $connKeepAlive = false,
642709
string $scriptFilename = null,
710+
string|array $stdin = null,
643711
bool $expectError = false,
644712
int $readLimit = -1,
645713
): Response {
646714
if ($this->hasError()) {
647715
return new Response(null, true);
648716
}
649717

650-
$params = $this->getRequestParams($query, $headers, $uri, $scriptFilename);
718+
if (is_array($stdin)) {
719+
$stdin = $this->parseStdin($stdin, $headers);
720+
}
721+
722+
$params = $this->getRequestParams($query, $headers, $uri, $scriptFilename, $stdin);
651723
$this->trace('Request params', $params);
652724

653725
try {
654726
$this->response = new Response(
655-
$this->getClient($address, $connKeepAlive)->request_data($params, false, $readLimit)
727+
$this->getClient($address, $connKeepAlive)->request_data($params, $stdin, $readLimit)
656728
);
657729
if ($expectError) {
658730
$this->error('Expected request error but the request was successful');

0 commit comments

Comments
 (0)