From cfc5c726ec69ef1b9103e9e18ab92694e995dfaf Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 23 Oct 2026 21:01:56 +0200 Subject: [PATCH] fix(HTTP): read request body when post_max_size is unlimited PHP treats post_max_size values of 0 or -1 as unlimited, but IncomingRequest compared Content-Length against the raw value, so the request body was never read. Use ini_parse_quantity() and treat non-positive values as PHP_INT_MAX. --- system/HTTP/IncomingRequest.php | 18 +++++----- tests/system/HTTP/IncomingRequestTest.php | 40 +++++++++++++++++++++ user_guide_src/source/changelogs/v4.7.6.rst | 3 +- 3 files changed, 51 insertions(+), 10 deletions(-) diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index f3063bcb6093..eb77b9277875 100644 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -140,7 +140,7 @@ public function __construct($config, ?URI $uri = null, $body = 'php://input', ?U // php://input is not available with enctype="multipart/form-data". // See https://www.php.net/manual/en/wrappers.php.php#wrappers.php.input && ! str_contains($this->getHeaderLine('Content-Type'), 'multipart/form-data') - && (int) $this->getHeaderLine('Content-Length') <= $this->getPostMaxSize() + && (int) $this->getHeaderLine('Content-Length') <= $this->getPostMaxSize((string) ini_get('post_max_size')) ) { // Get our body from php://input $body = file_get_contents('php://input'); @@ -167,16 +167,16 @@ public function __construct($config, ?URI $uri = null, $body = 'php://input', ?U $this->detectLocale($config); } - private function getPostMaxSize(): int + /** + * Converts the `post_max_size` ini value to bytes. + * + * A value of 0 or less means unlimited. + */ + private function getPostMaxSize(string $postMaxSize): int { - $postMaxSize = ini_get('post_max_size'); + $bytes = ini_parse_quantity($postMaxSize); - return match (strtoupper(substr($postMaxSize, -1))) { - 'G' => (int) str_replace('G', '', $postMaxSize) * 1024 ** 3, - 'M' => (int) str_replace('M', '', $postMaxSize) * 1024 ** 2, - 'K' => (int) str_replace('K', '', $postMaxSize) * 1024, - default => (int) $postMaxSize, - }; + return $bytes <= 0 ? PHP_INT_MAX : $bytes; } /** diff --git a/tests/system/HTTP/IncomingRequestTest.php b/tests/system/HTTP/IncomingRequestTest.php index 89646081e53c..cc0aee29ab2b 100644 --- a/tests/system/HTTP/IncomingRequestTest.php +++ b/tests/system/HTTP/IncomingRequestTest.php @@ -1081,6 +1081,46 @@ public function testSetPath(): void $this->assertSame('foobar', $request->getPath()); } + #[DataProvider('provideGetPostMaxSize')] + public function testGetPostMaxSize(string $value, int $expected): void + { + $getPostMaxSize = $this->getPrivateMethodInvoker($this->request, 'getPostMaxSize'); + + $this->assertSame($expected, $getPostMaxSize($value)); + } + + /** + * @return iterable + */ + public static function provideGetPostMaxSize(): iterable + { + yield 'zero means unlimited' => ['0', PHP_INT_MAX]; + + yield 'zero with suffix means unlimited' => ['0K', PHP_INT_MAX]; + + yield 'negative means unlimited' => ['-1', PHP_INT_MAX]; + + yield 'gigabytes' => ['1G', 1024 ** 3]; + + yield 'lowercased gigabytes' => ['1g', 1024 ** 3]; + + yield 'megabytes' => ['8M', 8 * 1024 ** 2]; + + yield 'lowercased megabytes' => ['8m', 8 * 1024 ** 2]; + + yield 'kilobytes' => ['512K', 512 * 1024]; + + yield 'lowercased kilobytes' => ['512k', 512 * 1024]; + + yield 'hexadecimal' => ['0x10M', 16 * 1024 ** 2]; + + yield 'octal' => ['010M', 8 * 1024 ** 2]; + + yield 'binary' => ['0b10M', 2 * 1024 ** 2]; + + yield 'bytes' => ['12345', 12345]; + } + public function testGetIPAddressNormal(): void { $expected = '123.123.123.123'; diff --git a/user_guide_src/source/changelogs/v4.7.6.rst b/user_guide_src/source/changelogs/v4.7.6.rst index 75e6f890047c..47172cd8ff02 100644 --- a/user_guide_src/source/changelogs/v4.7.6.rst +++ b/user_guide_src/source/changelogs/v4.7.6.rst @@ -35,8 +35,9 @@ Bugs Fixed - **Commands:** Fixed a bug where generator commands did not sort the imports of a template with CRLF line endings. - **Format:** Fixed ``XMLFormatter`` so that an array key that becomes empty after sanitization no longer triggers a ``ValueError``; the fallback tag name ``item`` is used instead. Values that cannot be converted to a string, such as objects not implementing ``Stringable``, now throw a ``FormatException`` instead of a fatal ``Error``. - **Helpers:** Fixed a regression in v4.7.5 where ``set_value()`` threw a ``TypeError`` when the default value was not a string or an array, such as an integer, a float, ``null``, or a ``Stringable`` object. Such values are now converted to a string. -- **HTTP:** Fixed stale ``$_REQUEST`` values after ``SiteURIFactory`` normalizes GET parameters, so ``Validation::withRequest()`` reads the normalized values while unrelated REQUEST entries are preserved. +- **HTTP:** Fixed a bug where ``IncomingRequest`` did not read the request body when PHP's ``post_max_size`` was set to ``0`` or ``-1`` (unlimited). - **HTTP:** Fixed a bug where ``IncomingRequest::isSecure()`` did not recognize ``https`` in the ``X-Forwarded-Proto`` header from a trusted proxy when the value contained uppercase letters or surrounding whitespace, or was represented as a single-element array. +- **HTTP:** Fixed stale ``$_REQUEST`` values after ``SiteURIFactory`` normalizes GET parameters, so ``Validation::withRequest()`` reads the normalized values while unrelated REQUEST entries are preserved. - **Security:** Fixed a bug where a valid JSON request body that is not an object (for example a JSON array) was corrupted after CSRF verification when the token was sent via the ``X-CSRF-TOKEN`` header. See the repo's