From 11849a07807555b52119a716ba59c087a0b82d07 Mon Sep 17 00:00:00 2001 From: Matthieu Napoli Date: Wed, 31 Jan 2024 14:59:12 +0100 Subject: [PATCH] Support malformed multipart body For example body containing broken array keys like `key0[key1][key2][` --- src/Event/Http/Psr7Bridge.php | 61 ++++++++++++++--------- tests/Event/Http/CommonHttpTest.php | 4 +- tests/Event/Http/HttpRequestEventTest.php | 2 +- tests/Event/Http/Psr7BridgeTest.php | 2 +- 4 files changed, 41 insertions(+), 28 deletions(-) diff --git a/src/Event/Http/Psr7Bridge.php b/src/Event/Http/Psr7Bridge.php index 2db1406d0..fee385f39 100644 --- a/src/Event/Http/Psr7Bridge.php +++ b/src/Event/Http/Psr7Bridge.php @@ -91,36 +91,49 @@ public static function convertResponse(ResponseInterface $response): HttpRespons return new HttpResponse($body, $response->getHeaders(), $response->getStatusCode()); } + /** + * @return array{0: array, 1: array|null} + */ private static function parseBodyAndUploadedFiles(HttpRequestEvent $event): array { - $bodyString = $event->getBody(); - $files = []; - $parsedBody = null; $contentType = $event->getContentType(); - if ($contentType !== null && $event->getMethod() === 'POST') { - if (str_starts_with($contentType, 'application/x-www-form-urlencoded')) { - parse_str($bodyString, $parsedBody); - } else { - $document = new Part("Content-type: $contentType\r\n\r\n" . $bodyString); - if ($document->isMultiPart()) { - $parsedBody = []; - foreach ($document->getParts() as $part) { - if ($part->isFile()) { - $tmpPath = tempnam(sys_get_temp_dir(), 'bref_upload_'); - if ($tmpPath === false) { - throw new RuntimeException('Unable to create a temporary directory'); - } - file_put_contents($tmpPath, $part->getBody()); - $file = new UploadedFile($tmpPath, filesize($tmpPath), UPLOAD_ERR_OK, $part->getFileName(), $part->getMimeType()); - - self::parseKeyAndInsertValueInArray($files, $part->getName(), $file); - } else { - self::parseKeyAndInsertValueInArray($parsedBody, $part->getName(), $part->getBody()); - } - } + if ($contentType === null || $event->getMethod() !== 'POST') { + return [[], null]; + } + + if (str_starts_with($contentType, 'application/x-www-form-urlencoded')) { + $parsedBody = []; + parse_str($event->getBody(), $parsedBody); + return [[], $parsedBody]; + } + + // Parse the body as multipart/form-data + $document = new Part("Content-type: $contentType\r\n\r\n" . $event->getBody()); + if (!$document->isMultiPart()) { + return [[], null]; + } + $files = []; + $queryString = ''; + foreach ($document->getParts() as $part) { + if ($part->isFile()) { + $tmpPath = tempnam(sys_get_temp_dir(), 'bref_upload_'); + if ($tmpPath === false) { + throw new RuntimeException('Unable to create a temporary directory'); } + file_put_contents($tmpPath, $part->getBody()); + $file = new UploadedFile($tmpPath, filesize($tmpPath), UPLOAD_ERR_OK, $part->getFileName(), $part->getMimeType()); + self::parseKeyAndInsertValueInArray($files, $part->getName(), $file); + } else { + // Temporarily store as a query string so that we can use PHP's native parse_str function to parse keys + $queryString .= urlencode($part->getName()) . '=' . urlencode($part->getBody()) . '&'; } } + if ($queryString !== '') { + $parsedBody = []; + parse_str($queryString, $parsedBody); + } else { + $parsedBody = null; + } return [$files, $parsedBody]; } diff --git a/tests/Event/Http/CommonHttpTest.php b/tests/Event/Http/CommonHttpTest.php index 63fd9b4ed..737653633 100644 --- a/tests/Event/Http/CommonHttpTest.php +++ b/tests/Event/Http/CommonHttpTest.php @@ -401,7 +401,7 @@ public function test POST request with multipart file uploads(int $version --testBoundary--\r "; $this->assertBody($body); - $this->assertParsedBody([]); + $this->assertParsedBody(null); $this->assertUploadedFile( 'foo', 'lorem.txt', @@ -554,7 +554,7 @@ abstract protected function assertUri(string $expected): void; abstract protected function assertHasMultiHeader(bool $expected): void; - abstract protected function assertParsedBody(array $expected): void; + abstract protected function assertParsedBody(array|null $expected): void; abstract protected function assertSourceIp(string $expected): void; diff --git a/tests/Event/Http/HttpRequestEventTest.php b/tests/Event/Http/HttpRequestEventTest.php index d1f2f516d..74259e12f 100644 --- a/tests/Event/Http/HttpRequestEventTest.php +++ b/tests/Event/Http/HttpRequestEventTest.php @@ -112,7 +112,7 @@ protected function assertSourceIp(string $expected): void $this->assertEquals($expected, $this->event->getSourceIp()); } - protected function assertParsedBody(array $expected): void + protected function assertParsedBody(array|null $expected): void { // Not applicable here since the class doesn't parse the body } diff --git a/tests/Event/Http/Psr7BridgeTest.php b/tests/Event/Http/Psr7BridgeTest.php index e2ab8de1a..d60a69eb9 100644 --- a/tests/Event/Http/Psr7BridgeTest.php +++ b/tests/Event/Http/Psr7BridgeTest.php @@ -123,7 +123,7 @@ protected function assertHasMultiHeader(bool $expected): void // Not applicable here } - protected function assertParsedBody(array $expected): void + protected function assertParsedBody(array|null $expected): void { $this->assertEquals($expected, $this->request->getParsedBody()); }