diff --git a/flight/net/Response.php b/flight/net/Response.php index e3b3cc6..9587ddf 100644 --- a/flight/net/Response.php +++ b/flight/net/Response.php @@ -484,7 +484,7 @@ class Response * Downloads a file. * * @param string $filePath The path to the file to be downloaded. - * @param string $fileName The name the downloaded file should have. If not provided, the name of the file on disk will be used. + * @param string $fileName The name the downloaded file should have. If not provided or is an empty string, the name of the file on disk will be used. * * @throws Exception If the file cannot be found. * @@ -501,6 +501,8 @@ class Response $mimeType = mime_content_type($filePath); $mimeType = $mimeType !== false ? $mimeType : 'application/octet-stream'; + // Sanitize filename to prevent header injection + $fileName = str_replace(["\r", "\n", '"'], '', $fileName); if ($fileName === '') { $fileName = basename($filePath); } diff --git a/tests/EngineTest.php b/tests/EngineTest.php index 256efb5..4b3dbc9 100644 --- a/tests/EngineTest.php +++ b/tests/EngineTest.php @@ -1086,11 +1086,13 @@ class EngineTest extends TestCase // doing this so we can overwrite some parts of the response $engine->getLoader()->register('response', function () { return new class extends Response { + public $headersSent = []; public function setRealHeader( string $header_string, bool $replace = true, int $response_code = 0 ): self { + $this->headersSent[] = $header_string; return $this; } }; @@ -1100,6 +1102,37 @@ class EngineTest extends TestCase $streamPath = stream_get_meta_data($tmpfile)['uri']; $this->expectOutputString('I am a teapot'); $engine->download($streamPath); + $this->assertContains('Content-Disposition: attachment; filename="'.basename($streamPath).'"', $engine->response()->headersSent); + } + + public function testDownloadWithDefaultFileName(): void + { + $engine = new class extends Engine { + public function getLoader() + { + return $this->loader; + } + }; + // doing this so we can overwrite some parts of the response + $engine->getLoader()->register('response', function () { + return new class extends Response { + public $headersSent = []; + public function setRealHeader( + string $header_string, + bool $replace = true, + int $response_code = 0 + ): self { + $this->headersSent[] = $header_string; + return $this; + } + }; + }); + $tmpfile = tmpfile(); + fwrite($tmpfile, 'I am a teapot'); + $streamPath = stream_get_meta_data($tmpfile)['uri']; + $this->expectOutputString('I am a teapot'); + $engine->download($streamPath, 'something.txt'); + $this->assertContains('Content-Disposition: attachment; filename="something.txt"', $engine->response()->headersSent); } public function testDownloadBadPath() {