summaryrefslogtreecommitdiffstats
path: root/patches/source/php/CVE-2023-0662.patch
diff options
context:
space:
mode:
Diffstat (limited to 'patches/source/php/CVE-2023-0662.patch')
-rw-r--r--patches/source/php/CVE-2023-0662.patch411
1 files changed, 411 insertions, 0 deletions
diff --git a/patches/source/php/CVE-2023-0662.patch b/patches/source/php/CVE-2023-0662.patch
new file mode 100644
index 000000000..e9cada2c9
--- /dev/null
+++ b/patches/source/php/CVE-2023-0662.patch
@@ -0,0 +1,411 @@
+From 716de0cff539f46294ef70fe75d548cd66766370 Mon Sep 17 00:00:00 2001
+From: Jakub Zelenka <bukka@php.net>
+Date: Thu, 19 Jan 2023 14:31:25 +0000
+Subject: [PATCH] Introduce max_multipart_body_parts INI
+
+This fixes GHSA-54hq-v5wp-fqgv DOS vulnerabality by limitting number of
+parsed multipart body parts as currently all parts were always parsed.
+---
+ main/main.c | 1 +
+ main/rfc1867.c | 11 ++
+ ...-54hq-v5wp-fqgv-max-body-parts-custom.phpt | 53 +++++++++
+ ...54hq-v5wp-fqgv-max-body-parts-default.phpt | 54 +++++++++
+ .../ghsa-54hq-v5wp-fqgv-max-file-uploads.phpt | 52 +++++++++
+ sapi/fpm/tests/tester.inc | 106 +++++++++++++++---
+ 6 files changed, 262 insertions(+), 15 deletions(-)
+ create mode 100644 sapi/fpm/tests/ghsa-54hq-v5wp-fqgv-max-body-parts-custom.phpt
+ create mode 100644 sapi/fpm/tests/ghsa-54hq-v5wp-fqgv-max-body-parts-default.phpt
+ create mode 100644 sapi/fpm/tests/ghsa-54hq-v5wp-fqgv-max-file-uploads.phpt
+
+diff --git a/main/main.c b/main/main.c
+index 40684f32dc14..c58ea58bf5ac 100644
+--- a/main/main.c
++++ b/main/main.c
+@@ -751,6 +751,7 @@ PHP_INI_BEGIN()
+ PHP_INI_ENTRY("disable_functions", "", PHP_INI_SYSTEM, NULL)
+ PHP_INI_ENTRY("disable_classes", "", PHP_INI_SYSTEM, NULL)
+ PHP_INI_ENTRY("max_file_uploads", "20", PHP_INI_SYSTEM|PHP_INI_PERDIR, NULL)
++ PHP_INI_ENTRY("max_multipart_body_parts", "-1", PHP_INI_SYSTEM|PHP_INI_PERDIR, NULL)
+
+ STD_PHP_INI_BOOLEAN("allow_url_fopen", "1", PHP_INI_SYSTEM, OnUpdateBool, allow_url_fopen, php_core_globals, core_globals)
+ STD_PHP_INI_BOOLEAN("allow_url_include", "0", PHP_INI_SYSTEM, OnUpdateBool, allow_url_include, php_core_globals, core_globals)
+diff --git a/main/rfc1867.c b/main/rfc1867.c
+index b43cfae5a1e2..3086e8da3dbe 100644
+--- a/main/rfc1867.c
++++ b/main/rfc1867.c
+@@ -687,6 +687,7 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */
+ void *event_extra_data = NULL;
+ unsigned int llen = 0;
+ int upload_cnt = INI_INT("max_file_uploads");
++ int body_parts_cnt = INI_INT("max_multipart_body_parts");
+ const zend_encoding *internal_encoding = zend_multibyte_get_internal_encoding();
+ php_rfc1867_getword_t getword;
+ php_rfc1867_getword_conf_t getword_conf;
+@@ -708,6 +709,11 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */
+ return;
+ }
+
++ if (body_parts_cnt < 0) {
++ body_parts_cnt = PG(max_input_vars) + upload_cnt;
++ }
++ int body_parts_limit = body_parts_cnt;
++
+ /* Get the boundary */
+ boundary = strstr(content_type_dup, "boundary");
+ if (!boundary) {
+@@ -792,6 +798,11 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */
+ char *pair = NULL;
+ int end = 0;
+
++ if (--body_parts_cnt < 0) {
++ 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);
++ goto fileupload_done;
++ }
++
+ while (isspace(*cd)) {
+ ++cd;
+ }
+#diff --git a/sapi/fpm/tests/ghsa-54hq-v5wp-fqgv-max-body-parts-custom.phpt b/sapi/fpm/tests/ghsa-54hq-v5wp-fqgv-max-body-parts-custom.phpt
+#new file mode 100644
+#index 000000000000..d2239ac3c410
+#--- /dev/null
+#+++ b/sapi/fpm/tests/ghsa-54hq-v5wp-fqgv-max-body-parts-custom.phpt
+#@@ -0,0 +1,53 @@
+#+--TEST--
+#+FPM: GHSA-54hq-v5wp-fqgv - max_multipart_body_parts ini custom value
+#+--SKIPIF--
+#+<?php include "skipif.inc"; ?>
+#+--FILE--
+#+<?php
+#+
+#+require_once "tester.inc";
+#+
+#+$cfg = <<<EOT
+#+[global]
+#+error_log = {{FILE:LOG}}
+#+[unconfined]
+#+listen = {{ADDR}}
+#+pm = dynamic
+#+pm.max_children = 5
+#+pm.start_servers = 1
+#+pm.min_spare_servers = 1
+#+pm.max_spare_servers = 3
+#+php_admin_value[html_errors] = false
+#+php_admin_value[max_input_vars] = 20
+#+php_admin_value[max_file_uploads] = 5
+#+php_admin_value[max_multipart_body_parts] = 10
+#+php_flag[display_errors] = On
+#+EOT;
+#+
+#+$code = <<<EOT
+#+<?php
+#+var_dump(count(\$_POST));
+#+EOT;
+#+
+#+$tester = new FPM\Tester($cfg, $code);
+#+$tester->start();
+#+$tester->expectLogStartNotices();
+#+echo $tester
+#+ ->request(stdin: [
+#+ 'parts' => [
+#+ 'count' => 30,
+#+ ]
+#+ ])
+#+ ->getBody();
+#+$tester->terminate();
+#+$tester->close();
+#+
+#+?>
+#+--EXPECT--
+#+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
+#+int(10)
+#+--CLEAN--
+#+<?php
+#+require_once "tester.inc";
+#+FPM\Tester::clean();
+#+?>
+#diff --git a/sapi/fpm/tests/ghsa-54hq-v5wp-fqgv-max-body-parts-default.phpt b/sapi/fpm/tests/ghsa-54hq-v5wp-fqgv-max-body-parts-default.phpt
+#new file mode 100644
+#index 000000000000..42b5afbf9ee7
+#--- /dev/null
+#+++ b/sapi/fpm/tests/ghsa-54hq-v5wp-fqgv-max-body-parts-default.phpt
+#@@ -0,0 +1,54 @@
+#+--TEST--
+#+FPM: GHSA-54hq-v5wp-fqgv - max_multipart_body_parts ini default
+#+--SKIPIF--
+#+<?php include "skipif.inc"; ?>
+#+--FILE--
+#+<?php
+#+
+#+require_once "tester.inc";
+#+
+#+$cfg = <<<EOT
+#+[global]
+#+error_log = {{FILE:LOG}}
+#+[unconfined]
+#+listen = {{ADDR}}
+#+pm = dynamic
+#+pm.max_children = 5
+#+pm.start_servers = 1
+#+pm.min_spare_servers = 1
+#+pm.max_spare_servers = 3
+#+php_admin_value[html_errors] = false
+#+php_admin_value[max_input_vars] = 20
+#+php_admin_value[max_file_uploads] = 5
+#+php_flag[display_errors] = On
+#+EOT;
+#+
+#+$code = <<<EOT
+#+<?php
+#+var_dump(count(\$_POST));
+#+EOT;
+#+
+#+$tester = new FPM\Tester($cfg, $code);
+#+$tester->start();
+#+$tester->expectLogStartNotices();
+#+echo $tester
+#+ ->request(stdin: [
+#+ 'parts' => [
+#+ 'count' => 30,
+#+ ]
+#+ ])
+#+ ->getBody();
+#+$tester->terminate();
+#+$tester->close();
+#+
+#+?>
+#+--EXPECT--
+#+Warning: Unknown: Input variables exceeded 20. To increase the limit change max_input_vars in php.ini. in Unknown on line 0
+#+
+#+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
+#+int(20)
+#+--CLEAN--
+#+<?php
+#+require_once "tester.inc";
+#+FPM\Tester::clean();
+#+?>
+#diff --git a/sapi/fpm/tests/ghsa-54hq-v5wp-fqgv-max-file-uploads.phpt b/sapi/fpm/tests/ghsa-54hq-v5wp-fqgv-max-file-uploads.phpt
+#new file mode 100644
+#index 000000000000..da81174c7280
+#--- /dev/null
+#+++ b/sapi/fpm/tests/ghsa-54hq-v5wp-fqgv-max-file-uploads.phpt
+#@@ -0,0 +1,52 @@
+#+--TEST--
+#+FPM: GHSA-54hq-v5wp-fqgv - exceeding max_file_uploads
+#+--SKIPIF--
+#+<?php include "skipif.inc"; ?>
+#+--FILE--
+#+<?php
+#+
+#+require_once "tester.inc";
+#+
+#+$cfg = <<<EOT
+#+[global]
+#+error_log = {{FILE:LOG}}
+#+[unconfined]
+#+listen = {{ADDR}}
+#+pm = dynamic
+#+pm.max_children = 5
+#+pm.start_servers = 1
+#+pm.min_spare_servers = 1
+#+pm.max_spare_servers = 3
+#+php_admin_value[html_errors] = false
+#+php_admin_value[max_file_uploads] = 5
+#+php_flag[display_errors] = On
+#+EOT;
+#+
+#+$code = <<<EOT
+#+<?php
+#+var_dump(count(\$_FILES));
+#+EOT;
+#+
+#+$tester = new FPM\Tester($cfg, $code);
+#+$tester->start();
+#+$tester->expectLogStartNotices();
+#+echo $tester
+#+ ->request(stdin: [
+#+ 'parts' => [
+#+ 'count' => 10,
+#+ 'param' => 'filename'
+#+ ]
+#+ ])
+#+ ->getBody();
+#+$tester->terminate();
+#+$tester->close();
+#+
+#+?>
+#+--EXPECT--
+#+Warning: Maximum number of allowable file uploads has been exceeded in Unknown on line 0
+#+int(5)
+#+--CLEAN--
+#+<?php
+#+require_once "tester.inc";
+#+FPM\Tester::clean();
+#+?>
+##diff --git a/sapi/fpm/tests/tester.inc b/sapi/fpm/tests/tester.inc
+##index 6197cdba53f5..e51aa0f69143 100644
+##--- a/sapi/fpm/tests/tester.inc
+##+++ b/sapi/fpm/tests/tester.inc
+#@@ -567,13 +567,17 @@ class Tester
+# * @param string $query
+# * @param array $headers
+# * @param string|null $uri
+#+ * @param string|null $scriptFilename
+#+ * @param string|null $stdin
+# *
+# * @return array
+# */
+# private function getRequestParams(
+# string $query = '',
+# array $headers = [],
+#- string $uri = null
+#+ string $uri = null,
+#+ string $scriptFilename = null,
+#+ ?string $stdin = null
+# ): array {
+# if (is_null($uri)) {
+# $uri = $this->makeSourceFile();
+3@@ -582,8 +586,8 @@ class Tester
+# $params = array_merge(
+# [
+# 'GATEWAY_INTERFACE' => 'FastCGI/1.0',
+#- 'REQUEST_METHOD' => 'GET',
+#- 'SCRIPT_FILENAME' => $uri,
+#+ 'REQUEST_METHOD' => is_null($stdin) ? 'GET' : 'POST',
+#+ 'SCRIPT_FILENAME' => $scriptFilename ?: $uri,
+# 'SCRIPT_NAME' => $uri,
+# 'QUERY_STRING' => $query,
+# 'REQUEST_URI' => $uri . ($query ? '?' . $query : ""),
+#@@ -597,7 +601,7 @@ class Tester
+# 'SERVER_PROTOCOL' => 'HTTP/1.1',
+# 'DOCUMENT_ROOT' => __DIR__,
+# 'CONTENT_TYPE' => '',
+#- 'CONTENT_LENGTH' => 0
+#+ 'CONTENT_LENGTH' => strlen($stdin ?? "") // Default to 0
+# ],
+# $headers
+# );
+#@@ -607,20 +611,86 @@ class Tester
+# });
+# }
+#
+#+ /**
+#+ * Parse stdin and generate data for multipart config.
+#+ *
+#+ * @param array $stdin
+#+ * @param array $headers
+#+ *
+#+ * @return void
+#+ * @throws \Exception
+#+ */
+#+ private function parseStdin(array $stdin, array &$headers)
+#+ {
+#+ $parts = $stdin['parts'] ?? null;
+#+ if (empty($parts)) {
+#+ throw new \Exception('The stdin array needs to contain parts');
+#+ }
+#+ $boundary = $stdin['boundary'] ?? 'AaB03x';
+#+ if ( ! isset($headers['CONTENT_TYPE'])) {
+#+ $headers['CONTENT_TYPE'] = 'multipart/form-data; boundary=' . $boundary;
+#+ }
+#+ $count = $parts['count'] ?? null;
+#+ if ( ! is_null($count)) {
+#+ $dispositionType = $parts['disposition'] ?? 'form-data';
+#+ $dispositionParam = $parts['param'] ?? 'name';
+#+ $namePrefix = $parts['prefix'] ?? 'f';
+#+ $nameSuffix = $parts['suffix'] ?? '';
+#+ $value = $parts['value'] ?? 'test';
+#+ $parts = [];
+#+ for ($i = 0; $i < $count; $i++) {
+#+ $parts[] = [
+#+ 'disposition' => $dispositionType,
+#+ 'param' => $dispositionParam,
+#+ 'name' => "$namePrefix$i$nameSuffix",
+#+ 'value' => $value
+#+ ];
+#+ }
+#+ }
+#+ $out = '';
+#+ $nl = "\r\n";
+#+ foreach ($parts as $part) {
+#+ if (!is_array($part)) {
+#+ $part = ['name' => $part];
+#+ } elseif ( ! isset($part['name'])) {
+#+ throw new \Exception('Each part has to have a name');
+#+ }
+#+ $name = $part['name'];
+#+ $dispositionType = $part['disposition'] ?? 'form-data';
+#+ $dispositionParam = $part['param'] ?? 'name';
+#+ $value = $part['value'] ?? 'test';
+#+ $partHeaders = $part['headers'] ?? [];
+#+
+#+ $out .= "--$boundary$nl";
+#+ $out .= "Content-disposition: $dispositionType; $dispositionParam=\"$name\"$nl";
+#+ foreach ($partHeaders as $headerName => $headerValue) {
+#+ $out .= "$headerName: $headerValue$nl";
+#+ }
+#+ $out .= $nl;
+#+ $out .= "$value$nl";
+#+ }
+#+ $out .= "--$boundary--$nl";
+#+
+#+ return $out;
+#+ }
+#+
+# /**
+# * Execute request.
+# *
+#- * @param string $query
+#- * @param array $headers
+#- * @param string|null $uri
+#- * @param string|null $address
+#- * @param string|null $successMessage
+#- * @param string|null $errorMessage
+#- * @param bool $connKeepAlive
+#- * @param bool $expectError
+#- * @param int $readLimit
+#+ * @param string $query
+#+ * @param array $headers
+#+ * @param string|null $uri
+#+ * @param string|null $address
+#+ * @param string|null $successMessage
+#+ * @param string|null $errorMessage
+#+ * @param bool $connKeepAlive
+#+ * @param string|null $scriptFilename = null
+#+ * @param string|array|null $stdin = null
+#+ * @param bool $expectError
+#+ * @param int $readLimit
+# *
+# * @return Response
+#+ * @throws \Exception
+# */
+# public function request(
+# string $query = '',
+#@@ -630,6 +700,8 @@ class Tester
+# string $successMessage = null,
+# string $errorMessage = null,
+# bool $connKeepAlive = false,
+#+ string $scriptFilename = null,
+#+ string|array $stdin = null,
+# bool $expectError = false,
+# int $readLimit = -1,
+# ): Response {
+#@@ -637,12 +709,16 @@ class Tester
+# return new Response(null, true);
+# }
+#
+#- $params = $this->getRequestParams($query, $headers, $uri);
+#+ if (is_array($stdin)) {
+#+ $stdin = $this->parseStdin($stdin, $headers);
+#+ }
+#+
+#+ $params = $this->getRequestParams($query, $headers, $uri, $scriptFilename, $stdin);
+# $this->trace('Request params', $params);
+#
+# try {
+# $this->response = new Response(
+#- $this->getClient($address, $connKeepAlive)->request_data($params, false, $readLimit)
+#+ $this->getClient($address, $connKeepAlive)->request_data($params, $stdin, $readLimit)
+# );
+# if ($expectError) {
+# $this->error('Expected request error but the request was successful');