CVE-2026-49742
Description
TYPO3 CMS allows backend users to download sensitive files from fallback storage via the Media Module, affecting versions 11 through 14.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
TYPO3 CMS allows backend users to download sensitive files from fallback storage via the Media Module, affecting versions 11 through 14.
Vulnerability
Backend users with file download permissions in TYPO3 CMS could download files from the fallback storage of the file abstraction layer (FAL) through the Media Module. The fallback storage resolves paths relative to the server's document root, potentially exposing sensitive files like logs. This vulnerability affects TYPO3 CMS versions 11.0.0-11.5.50, 12.0.0-12.4.45, 13.0.0-13.4.30, and 14.0.0-14.3.2 [3].
Exploitation
An attacker with backend user credentials and file download permissions can exploit this vulnerability. By navigating to the Media Module, they can initiate a download of files stored in the FAL's fallback storage. Since this storage is resolved relative to the server's document root, the attacker can craft requests to access sensitive files outside of the intended web-accessible directories [3].
Impact
Successful exploitation allows an attacker to read sensitive files from the server's document root, such as log files or configuration files, which could lead to information disclosure. The impact is limited to files accessible via the fallback storage mechanism within the FAL [3].
Mitigation
Update to TYPO3 versions 11.5.51 ELTS, 12.4.46 ELTS, 13.4.31 LTS, or 14.3.3 LTS. These versions contain fixes for the described vulnerability. The release date for these patches was June 9, 2026 [3].
AI Insight generated on Jun 9, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2Patches
2caa6b444d7ab[SECURITY] Avoid download from fallback storage in FileDownloadController
4 files changed · +180 −1
typo3/sysext/filelist/Classes/Controller/FileDownloadController.php+7 −1 modified@@ -101,7 +101,9 @@ public function handleRequest(ServerRequestInterface $request): ResponseInterfac } $zipFile->close(); $response = $this->createResponse($zipFileName, $filesAdded); - unlink($zipFileName); + if ($filesAdded > 0) { + unlink($zipFileName); + } return $response; } @@ -134,6 +136,10 @@ protected function collectFiles(array $items): array if ($fileOrFolderObject === null) { continue; } + // Files from fallback storage must be skipped in general + if ($fileOrFolderObject->getStorage()->isFallbackStorage()) { + continue; + } $baseIdentifier = dirname($fileOrFolderObject->getIdentifier()); if ($fileOrFolderObject instanceof Folder) { // handle file / folder structure
typo3/sysext/filelist/Tests/Functional/Controller/FileDownloadControllerTest.php+165 −0 added@@ -0,0 +1,165 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Filelist\Tests\Functional\Controller; + +use PHPUnit\Framework\Attributes\Test; +use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder; +use TYPO3\CMS\Core\Http\ServerRequest; +use TYPO3\CMS\Core\Localization\LanguageServiceFactory; +use TYPO3\CMS\Filelist\Controller\FileDownloadController; +use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; + +final class FileDownloadControllerTest extends FunctionalTestCase +{ + protected array $coreExtensionsToLoad = ['filelist']; + + /** + * @var array<string, non-empty-string> + */ + protected array $pathsToProvideInTestInstance = [ + 'typo3/sysext/filelist/Tests/Functional/Fixtures/textfile.txt' => 'fileadmin/textfile.txt', + ]; + + public function setUp(): void + { + parent::setUp(); + $this->importCSVDataSet(__DIR__ . '/../Fixtures/be_users.csv'); + $backendUser = $this->setUpBackendUser(1); + $GLOBALS['LANG'] = $this->get(LanguageServiceFactory::class)->createFromUserPreferences($backendUser); + } + + #[Test] + public function handleRequestExitsWithErrorResponseWhenNoItemsGiven(): void + { + $parsedBody = []; + $request = new ServerRequest(); + $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); + $request = $request->withParsedBody($parsedBody); + $response = $this->get(FileDownloadController::class)->handleRequest($request); + self::assertSame(400, $response->getStatusCode()); + } + + #[Test] + public function handleRequestReturnsNoFilesWhenFileNotFound(): void + { + $parsedBody = [ + 'items' => ['non-existing-file.txt'], + ]; + $request = new ServerRequest(); + $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); + $request = $request->withParsedBody($parsedBody); + $response = $this->get(FileDownloadController::class)->handleRequest($request); + self::assertSame(200, $response->getStatusCode()); + self::assertSame('application/json; charset=utf-8', $response->getHeaderLine('Content-Type')); + $body = (string)$response->getBody(); + $jsonArray = json_decode($body, true); + self::assertFalse($jsonArray['success']); + self::assertSame('noFiles', $jsonArray['status']); + } + + #[Test] + public function handleRequestReturnsFileAsZipWhenFileExists(): void + { + $parsedBody = [ + 'items' => ['1:/textfile.txt'], + ]; + $request = new ServerRequest(); + $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); + $request = $request->withParsedBody($parsedBody); + $response = $this->get(FileDownloadController::class)->handleRequest($request); + self::assertSame(200, $response->getStatusCode()); + self::assertSame('application/zip', $response->getHeaderLine('Content-Type')); + self::assertStringContainsString('attachment; filename=typo3_download_', $response->getHeaderLine('Content-Disposition')); + } + + #[Test] + public function handleRequestReturnsNoFilesWhenFilesInFallbackStorage(): void + { + $parsedBody = [ + 'items' => ['typo3temp/var/log/', '.htpasswd'], + ]; + $request = new ServerRequest(); + $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); + $request = $request->withParsedBody($parsedBody); + $response = $this->get(FileDownloadController::class)->handleRequest($request); + self::assertSame(200, $response->getStatusCode()); + self::assertSame('application/json; charset=utf-8', $response->getHeaderLine('Content-Type')); + $body = (string)$response->getBody(); + $jsonArray = json_decode($body, true); + self::assertFalse($jsonArray['success']); + self::assertSame('noFiles', $jsonArray['status']); + } + + #[Test] + public function handleRequestDeniesDownloadWhenDownloadIsDisabledForUser(): void + { + $backendUser = $this->setUpBackendUser(2); + $GLOBALS['LANG'] = $this->get(LanguageServiceFactory::class)->createFromUserPreferences($backendUser); + + $parsedBody = [ + 'items' => ['1:/textfile.txt'], + ]; + $request = new ServerRequest(); + $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); + $request = $request->withParsedBody($parsedBody); + $response = $this->get(FileDownloadController::class)->handleRequest($request); + self::assertSame(403, $response->getStatusCode()); + } + + #[Test] + public function handleRequestReturnsNoFilesWhenFileExistsButDownloadOfGivenFileExtensionIsNotInAllowList(): void + { + $backendUser = $this->setUpBackendUser(3); + $GLOBALS['LANG'] = $this->get(LanguageServiceFactory::class)->createFromUserPreferences($backendUser); + + $parsedBody = [ + 'items' => ['1:/textfile.txt'], + ]; + $request = new ServerRequest(); + $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); + $request = $request->withParsedBody($parsedBody); + $response = $this->get(FileDownloadController::class)->handleRequest($request); + self::assertSame(200, $response->getStatusCode()); + self::assertSame('application/json; charset=utf-8', $response->getHeaderLine('Content-Type')); + $body = (string)$response->getBody(); + $jsonArray = json_decode($body, true); + self::assertFalse($jsonArray['success']); + self::assertSame('noFiles', $jsonArray['status']); + } + + #[Test] + public function handleRequestReturnsNoFilesWhenFileExistsButDownloadOfGivenFileExtensionIsInDenyList(): void + { + $backendUser = $this->setUpBackendUser(4); + $GLOBALS['LANG'] = $this->get(LanguageServiceFactory::class)->createFromUserPreferences($backendUser); + + $parsedBody = [ + 'items' => ['1:/textfile.txt'], + ]; + $request = new ServerRequest(); + $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); + $request = $request->withParsedBody($parsedBody); + $response = $this->get(FileDownloadController::class)->handleRequest($request); + self::assertSame(200, $response->getStatusCode()); + self::assertSame('application/json; charset=utf-8', $response->getHeaderLine('Content-Type')); + $body = (string)$response->getBody(); + $jsonArray = json_decode($body, true); + self::assertFalse($jsonArray['success']); + self::assertSame('noFiles', $jsonArray['status']); + } +}
typo3/sysext/filelist/Tests/Functional/Fixtures/be_users.csv+7 −0 added@@ -0,0 +1,7 @@ +"be_users" +,"uid","pid","tstamp","username","password","admin","TSconfig","description" +# The password is "password" +,1,0,1366642540,"admin1","$1$tCrlLajZ$C0sikFQQ3SWaFAZ1Me0Z/1",1,"","User with all rights" +,2,0,1366642540,"admin2","$1$tCrlLajZ$C0sikFQQ3SWaFAZ1Me0Z/1",1,"options.file_list.fileDownload.enabled = 0","User with denied file download" +,3,0,1366642540,"admin3","$1$tCrlLajZ$C0sikFQQ3SWaFAZ1Me0Z/1",1,"options.file_list.fileDownload.allowedFileExtensions = pdf","User with .pdf as allowed file extension" +,4,0,1366642540,"admin4","$1$tCrlLajZ$C0sikFQQ3SWaFAZ1Me0Z/1",1,"options.file_list.fileDownload.disallowedFileExtensions = txt","User with .txt as disallowed file extension"
typo3/sysext/filelist/Tests/Functional/Fixtures/textfile.txt+1 −0 added@@ -0,0 +1 @@ +This is a simple text file.
ad636b618384[SECURITY] Avoid download from fallback storage in FileDownloadController
4 files changed · +180 −1
typo3/sysext/filelist/Classes/Controller/FileDownloadController.php+7 −1 modified@@ -101,7 +101,9 @@ public function handleRequest(ServerRequestInterface $request): ResponseInterfac } $zipFile->close(); $response = $this->createResponse($zipFileName, $filesAdded); - unlink($zipFileName); + if ($filesAdded > 0) { + unlink($zipFileName); + } return $response; } @@ -134,6 +136,10 @@ protected function collectFiles(array $items): array if ($fileOrFolderObject === null) { continue; } + // Files from fallback storage must be skipped in general + if ($fileOrFolderObject->getStorage()->isFallbackStorage()) { + continue; + } $baseIdentifier = dirname($fileOrFolderObject->getIdentifier()); if ($fileOrFolderObject instanceof Folder) { // handle file / folder structure
typo3/sysext/filelist/Tests/Functional/Controller/FileDownloadControllerTest.php+165 −0 added@@ -0,0 +1,165 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Filelist\Tests\Functional\Controller; + +use PHPUnit\Framework\Attributes\Test; +use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder; +use TYPO3\CMS\Core\Http\ServerRequest; +use TYPO3\CMS\Core\Localization\LanguageServiceFactory; +use TYPO3\CMS\Filelist\Controller\FileDownloadController; +use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; + +final class FileDownloadControllerTest extends FunctionalTestCase +{ + protected array $coreExtensionsToLoad = ['filelist']; + + /** + * @var array<string, non-empty-string> + */ + protected array $pathsToProvideInTestInstance = [ + 'typo3/sysext/filelist/Tests/Functional/Fixtures/textfile.txt' => 'fileadmin/textfile.txt', + ]; + + public function setUp(): void + { + parent::setUp(); + $this->importCSVDataSet(__DIR__ . '/../Fixtures/be_users.csv'); + $backendUser = $this->setUpBackendUser(1); + $GLOBALS['LANG'] = $this->get(LanguageServiceFactory::class)->createFromUserPreferences($backendUser); + } + + #[Test] + public function handleRequestExitsWithErrorResponseWhenNoItemsGiven(): void + { + $parsedBody = []; + $request = new ServerRequest(); + $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); + $request = $request->withParsedBody($parsedBody); + $response = $this->get(FileDownloadController::class)->handleRequest($request); + self::assertSame(400, $response->getStatusCode()); + } + + #[Test] + public function handleRequestReturnsNoFilesWhenFileNotFound(): void + { + $parsedBody = [ + 'items' => ['non-existing-file.txt'], + ]; + $request = new ServerRequest(); + $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); + $request = $request->withParsedBody($parsedBody); + $response = $this->get(FileDownloadController::class)->handleRequest($request); + self::assertSame(200, $response->getStatusCode()); + self::assertSame('application/json; charset=utf-8', $response->getHeaderLine('Content-Type')); + $body = (string)$response->getBody(); + $jsonArray = json_decode($body, true); + self::assertFalse($jsonArray['success']); + self::assertSame('noFiles', $jsonArray['status']); + } + + #[Test] + public function handleRequestReturnsFileAsZipWhenFileExists(): void + { + $parsedBody = [ + 'items' => ['1:/textfile.txt'], + ]; + $request = new ServerRequest(); + $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); + $request = $request->withParsedBody($parsedBody); + $response = $this->get(FileDownloadController::class)->handleRequest($request); + self::assertSame(200, $response->getStatusCode()); + self::assertSame('application/zip', $response->getHeaderLine('Content-Type')); + self::assertStringContainsString('attachment; filename=typo3_download_', $response->getHeaderLine('Content-Disposition')); + } + + #[Test] + public function handleRequestReturnsNoFilesWhenFilesInFallbackStorage(): void + { + $parsedBody = [ + 'items' => ['typo3temp/var/log/', '.htpasswd'], + ]; + $request = new ServerRequest(); + $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); + $request = $request->withParsedBody($parsedBody); + $response = $this->get(FileDownloadController::class)->handleRequest($request); + self::assertSame(200, $response->getStatusCode()); + self::assertSame('application/json; charset=utf-8', $response->getHeaderLine('Content-Type')); + $body = (string)$response->getBody(); + $jsonArray = json_decode($body, true); + self::assertFalse($jsonArray['success']); + self::assertSame('noFiles', $jsonArray['status']); + } + + #[Test] + public function handleRequestDeniesDownloadWhenDownloadIsDisabledForUser(): void + { + $backendUser = $this->setUpBackendUser(2); + $GLOBALS['LANG'] = $this->get(LanguageServiceFactory::class)->createFromUserPreferences($backendUser); + + $parsedBody = [ + 'items' => ['1:/textfile.txt'], + ]; + $request = new ServerRequest(); + $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); + $request = $request->withParsedBody($parsedBody); + $response = $this->get(FileDownloadController::class)->handleRequest($request); + self::assertSame(403, $response->getStatusCode()); + } + + #[Test] + public function handleRequestReturnsNoFilesWhenFileExistsButDownloadOfGivenFileExtensionIsNotInAllowList(): void + { + $backendUser = $this->setUpBackendUser(3); + $GLOBALS['LANG'] = $this->get(LanguageServiceFactory::class)->createFromUserPreferences($backendUser); + + $parsedBody = [ + 'items' => ['1:/textfile.txt'], + ]; + $request = new ServerRequest(); + $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); + $request = $request->withParsedBody($parsedBody); + $response = $this->get(FileDownloadController::class)->handleRequest($request); + self::assertSame(200, $response->getStatusCode()); + self::assertSame('application/json; charset=utf-8', $response->getHeaderLine('Content-Type')); + $body = (string)$response->getBody(); + $jsonArray = json_decode($body, true); + self::assertFalse($jsonArray['success']); + self::assertSame('noFiles', $jsonArray['status']); + } + + #[Test] + public function handleRequestReturnsNoFilesWhenFileExistsButDownloadOfGivenFileExtensionIsInDenyList(): void + { + $backendUser = $this->setUpBackendUser(4); + $GLOBALS['LANG'] = $this->get(LanguageServiceFactory::class)->createFromUserPreferences($backendUser); + + $parsedBody = [ + 'items' => ['1:/textfile.txt'], + ]; + $request = new ServerRequest(); + $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); + $request = $request->withParsedBody($parsedBody); + $response = $this->get(FileDownloadController::class)->handleRequest($request); + self::assertSame(200, $response->getStatusCode()); + self::assertSame('application/json; charset=utf-8', $response->getHeaderLine('Content-Type')); + $body = (string)$response->getBody(); + $jsonArray = json_decode($body, true); + self::assertFalse($jsonArray['success']); + self::assertSame('noFiles', $jsonArray['status']); + } +}
typo3/sysext/filelist/Tests/Functional/Fixtures/be_users.csv+7 −0 added@@ -0,0 +1,7 @@ +"be_users" +,"uid","pid","tstamp","username","password","admin","TSconfig","description" +# The password is "password" +,1,0,1366642540,"admin1","$1$tCrlLajZ$C0sikFQQ3SWaFAZ1Me0Z/1",1,"","User with all rights" +,2,0,1366642540,"admin2","$1$tCrlLajZ$C0sikFQQ3SWaFAZ1Me0Z/1",1,"options.file_list.fileDownload.enabled = 0","User with denied file download" +,3,0,1366642540,"admin3","$1$tCrlLajZ$C0sikFQQ3SWaFAZ1Me0Z/1",1,"options.file_list.fileDownload.allowedFileExtensions = pdf","User with .pdf as allowed file extension" +,4,0,1366642540,"admin4","$1$tCrlLajZ$C0sikFQQ3SWaFAZ1Me0Z/1",1,"options.file_list.fileDownload.disallowedFileExtensions = txt","User with .txt as disallowed file extension"
typo3/sysext/filelist/Tests/Functional/Fixtures/textfile.txt+1 −0 added@@ -0,0 +1 @@ +This is a simple text file.
Vulnerability mechanics
No source-code context for this CVE — mechanics is only generated when we can read the actual fix diff. Without that, the four sections (root cause, attack vector, affected code, fix) would be speculation rather than analysis.
References
3News mentions
1- TYPO3 CMS: Thirteen Backend Vulnerabilities Disclosed on June 9, 2026Vypr Intelligence · Jun 9, 2026