diff --git a/src/Event/Http/Psr7Bridge.php b/src/Event/Http/Psr7Bridge.php index fcae50356..c8b1de133 100644 --- a/src/Event/Http/Psr7Bridge.php +++ b/src/Event/Http/Psr7Bridge.php @@ -3,6 +3,7 @@ namespace Bref\Event\Http; use Bref\Context\Context; +use LogicException; use Nyholm\Psr7\ServerRequest; use Nyholm\Psr7\Stream; use Nyholm\Psr7\UploadedFile; @@ -109,8 +110,21 @@ private static function parseBodyAndUploadedFiles(HttpRequestEvent $event): arra return [[], $parsedBody]; } - // Parse the body as multipart/form-data - $document = new Part("Content-type: $contentType\r\n\r\n" . $event->getBody()); + try { + return self::parseMultipartBody($contentType, $event->getBody()); + } catch (LogicException) { + // The parser throws on malformed bodies (empty form, missing boundary, truncated body…) + // PHP ignores these instead of failing the request, so we do the same + return [[], null]; + } + } + + /** + * @return array{0: array, 1: array|null} + */ + private static function parseMultipartBody(string $contentType, string $body): array + { + $document = new Part("Content-type: $contentType\r\n\r\n" . $body); if (! $document->isMultiPart()) { return [[], null]; } diff --git a/tests/Event/Http/Psr7BridgeTest.php b/tests/Event/Http/Psr7BridgeTest.php index 359751120..dfd467f84 100644 --- a/tests/Event/Http/Psr7BridgeTest.php +++ b/tests/Event/Http/Psr7BridgeTest.php @@ -49,6 +49,37 @@ public function test falsy server params are not dropped() self::assertSame('', $serverParams['PHP_AUTH_PW']); } + /** + * @dataProvider provideMalformedMultipartBodies + */ + public function test malformed multipart bodies are ignored like PHP does(string $contentType, string $body) + { + $event = new HttpRequestEvent([ + 'httpMethod' => 'POST', + 'headers' => [ + 'Content-Type' => $contentType, + ], + 'body' => $body, + ]); + $request = Psr7Bridge::convertRequest($event, Context::fake()); + + self::assertNull($request->getParsedBody()); + self::assertSame([], $request->getUploadedFiles()); + self::assertSame($body, $request->getBody()->getContents()); + } + + public static function provideMalformedMultipartBodies(): array + { + return [ + // Sent by browsers for a FormData with no fields + 'empty form' => ['multipart/form-data; boundary=testBoundary', "--testBoundary--\r\n"], + 'empty body' => ['multipart/form-data; boundary=testBoundary', ''], + 'no closing boundary' => ['multipart/form-data; boundary=testBoundary', "--testBoundary\r\nContent-Disposition: form-data; name=\"foo\"\r\n\r\nbar\r\n"], + 'no boundary in the content type' => ['multipart/form-data', "--testBoundary\r\nContent-Disposition: form-data; name=\"foo\"\r\n\r\nbar\r\n--testBoundary--\r\n"], + 'nested multipart/mixed part' => ['multipart/form-data; boundary=testBoundary', "--testBoundary\r\nContent-Disposition: form-data; name=\"foo\"\r\nContent-Type: multipart/mixed; boundary=nested\r\n\r\n--nested\r\nContent-Disposition: file; filename=\"foo.txt\"\r\n\r\nbar\r\n--nested--\r\n\r\n--testBoundary--\r\n"], + ]; + } + protected function fromFixture(string $file): void { $event = new HttpRequestEvent(json_decode(file_get_contents($file), true, 512, JSON_THROW_ON_ERROR));