diff --git a/typo3/sysext/core/Classes/Authentication/BackendUserAuthentication.php b/typo3/sysext/core/Classes/Authentication/BackendUserAuthentication.php index 5238df5e2698918080f69afb8258161ec77688de..570f3bbab02b31531f1a5efa27b61867533b3b5a 100644 --- a/typo3/sysext/core/Classes/Authentication/BackendUserAuthentication.php +++ b/typo3/sysext/core/Classes/Authentication/BackendUserAuthentication.php @@ -2368,4 +2368,26 @@ TCAdefaults.sys_note.email = ' . $this->user['email']; || ($globalConfig === 3 && $isAdmin) || ($globalConfig === 4 && $this->isSystemMaintainer()); } + + /** + * Returns if import functionality is available for current user + * + * @internal + */ + public function isImportEnabled(): bool + { + return $this->isAdmin() + || ($this->getTSConfig()['options.']['impexp.']['enableImportForNonAdminUser'] ?? false); + } + + /** + * Returns if export functionality is available for current user + * + * @internal + */ + public function isExportEnabled(): bool + { + return $this->isAdmin() + || ($this->getTSConfig()['options.']['impexp.']['enableExportForNonAdminUser'] ?? false); + } } diff --git a/typo3/sysext/core/Classes/Resource/Filter/ImportExportFilter.php b/typo3/sysext/core/Classes/Resource/Filter/ImportExportFilter.php new file mode 100644 index 0000000000000000000000000000000000000000..789f02ee7b46e15b2f776eb94cd382974a385ea9 --- /dev/null +++ b/typo3/sysext/core/Classes/Resource/Filter/ImportExportFilter.php @@ -0,0 +1,55 @@ +<?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\Core\Resource\Filter; + +use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; +use TYPO3\CMS\Core\Resource\Driver\DriverInterface; + +/** + * Utility methods for filtering filenames stored in `importexport` temporary folder. + * Albeit this filter is in the scope of `ext:impexp`, it is located in `ext:core` to + * apply filters on left-over fragments, even when `ext:impexp` is not installed. + * + * @internal + */ +class ImportExportFilter +{ + /** + * Filter method that checks if a directory or a file in such directory belongs to the temp directory of EXT:impexp + * and the user has "export" permissions. + */ + public static function filterImportExportFilesAndFolders(string $itemName, string $itemIdentifier, string $parentIdentifier, array $additionalInformation, DriverInterface $driverInstance) + { + // + `_temp_` is hard-coded in `BackendUserAuthentication::getDefaultUploadTemporaryFolder()` + // + `importexport` is hard-coded in `ImportExport::createDefaultImportExportFolder()` + $importExportFolderSubPath = '/_temp_/importexport/'; + if (str_ends_with($parentIdentifier, $importExportFolderSubPath) || str_contains($itemIdentifier, $importExportFolderSubPath)) { + $backendUser = self::getBackendUser(); + if ($backendUser === null || !$backendUser->isExportEnabled()) { + return -1; + } + } + + return true; + } + + protected static function getBackendUser(): ?BackendUserAuthentication + { + return $GLOBALS['BE_USER'] ?? null; + } +} diff --git a/typo3/sysext/core/Classes/Resource/ResourceStorage.php b/typo3/sysext/core/Classes/Resource/ResourceStorage.php index 94e1603b657f46ef4fb0fde396e55391bc6c6cc8..752eb0d5c6f4e33cba82ec04b63385947b921744 100644 --- a/typo3/sysext/core/Classes/Resource/ResourceStorage.php +++ b/typo3/sysext/core/Classes/Resource/ResourceStorage.php @@ -72,6 +72,7 @@ use TYPO3\CMS\Core\Resource\Exception\InvalidTargetFolderException; use TYPO3\CMS\Core\Resource\Exception\ResourcePermissionsUnavailableException; use TYPO3\CMS\Core\Resource\Exception\UploadException; use TYPO3\CMS\Core\Resource\Exception\UploadSizeException; +use TYPO3\CMS\Core\Resource\Filter\ImportExportFilter; use TYPO3\CMS\Core\Resource\Index\FileIndexRepository; use TYPO3\CMS\Core\Resource\Index\Indexer; use TYPO3\CMS\Core\Resource\OnlineMedia\Helpers\OnlineMediaHelperRegistry; @@ -1528,6 +1529,19 @@ class ResourceStorage implements ResourceStorageInterface $this->fileAndFolderNameFilters = $GLOBALS['TYPO3_CONF_VARS']['SYS']['fal']['defaultFilterCallbacks']; } + /** + * Returns a filter for files generated by EXT:impexp + * + * @return array<int, ImportExportFilter|string> + * @internal + */ + public function getImportExportFilter(): array + { + $filter = GeneralUtility::makeInstance(ImportExportFilter::class); + + return [$filter, 'filterImportExportFilesAndFolders']; + } + /** * Returns the file and folder name filters used by this storage. * @@ -1535,7 +1549,7 @@ class ResourceStorage implements ResourceStorageInterface */ public function getFileAndFolderNameFilters() { - return $this->fileAndFolderNameFilters; + return array_merge($this->fileAndFolderNameFilters, [$this->getImportExportFilter()]); } /** @@ -1600,7 +1614,7 @@ class ResourceStorage implements ResourceStorageInterface $rows = $this->getFileIndexRepository()->findByFolder($folder); - $filters = $useFilters == true ? $this->fileAndFolderNameFilters : []; + $filters = $useFilters == true ? $this->getFileAndFolderNameFilters() : []; $fileIdentifiers = array_values($this->driver->getFilesInFolder($folder->getIdentifier(), $start, $maxNumberOfItems, $recursive, $filters, $sort, $sortRev)); $items = []; @@ -1630,7 +1644,7 @@ class ResourceStorage implements ResourceStorageInterface */ public function getFileIdentifiersInFolder($folderIdentifier, $useFilters = true, $recursive = false) { - $filters = $useFilters == true ? $this->fileAndFolderNameFilters : []; + $filters = $useFilters == true ? $this->getFileAndFolderNameFilters() : []; return $this->driver->getFilesInFolder($folderIdentifier, 0, 0, $recursive, $filters); } @@ -1644,7 +1658,7 @@ class ResourceStorage implements ResourceStorageInterface public function countFilesInFolder(Folder $folder, $useFilters = true, $recursive = false) { $this->assureFolderReadPermission($folder); - $filters = $useFilters ? $this->fileAndFolderNameFilters : []; + $filters = $useFilters ? $this->getFileAndFolderNameFilters() : []; return $this->driver->countFilesInFolder($folder->getIdentifier(), $recursive, $filters); } @@ -1656,7 +1670,7 @@ class ResourceStorage implements ResourceStorageInterface */ public function getFolderIdentifiersInFolder($folderIdentifier, $useFilters = true, $recursive = false) { - $filters = $useFilters == true ? $this->fileAndFolderNameFilters : []; + $filters = $useFilters == true ? $this->getFileAndFolderNameFilters() : []; return $this->driver->getFoldersInFolder($folderIdentifier, 0, 0, $recursive, $filters); } @@ -2418,7 +2432,7 @@ class ResourceStorage implements ResourceStorageInterface */ public function getFoldersInFolder(Folder $folder, $start = 0, $maxNumberOfItems = 0, $useFilters = true, $recursive = false, $sort = '', $sortRev = false) { - $filters = $useFilters == true ? $this->fileAndFolderNameFilters : []; + $filters = $useFilters == true ? $this->getFileAndFolderNameFilters() : []; $folderIdentifiers = $this->driver->getFoldersInFolder($folder->getIdentifier(), $start, $maxNumberOfItems, $recursive, $filters, $sort, $sortRev); @@ -2429,6 +2443,7 @@ class ResourceStorage implements ResourceStorageInterface unset($folderIdentifiers[$processingIdentifier]); } } + $folders = []; foreach ($folderIdentifiers as $folderIdentifier) { $folders[$folderIdentifier] = $this->getFolder($folderIdentifier, true); @@ -2446,7 +2461,7 @@ class ResourceStorage implements ResourceStorageInterface public function countFoldersInFolder(Folder $folder, $useFilters = true, $recursive = false) { $this->assureFolderReadPermission($folder); - $filters = $useFilters ? $this->fileAndFolderNameFilters : []; + $filters = $useFilters ? $this->getFileAndFolderNameFilters() : []; return $this->driver->countFoldersInFolder($folder->getIdentifier(), $recursive, $filters); } diff --git a/typo3/sysext/core/Tests/Acceptance/Application/Impexp/UsersCest.php b/typo3/sysext/core/Tests/Acceptance/Application/Impexp/UsersCest.php index 2c96c718bc99725189dc448a2fc15909572d4944..43953ebb1167c4c84651ec692359bd6713d0bce0 100644 --- a/typo3/sysext/core/Tests/Acceptance/Application/Impexp/UsersCest.php +++ b/typo3/sysext/core/Tests/Acceptance/Application/Impexp/UsersCest.php @@ -52,7 +52,7 @@ class UsersCest extends AbstractCest /** * @throws \Exception */ - public function doNotShowImportInContextMenuForNonAdminUser(ApplicationTester $I, PageTree $pageTree): void + public function doNotShowImportAndExportInContextMenuForNonAdminUser(ApplicationTester $I, PageTree $pageTree): void { $selectedPageTitle = 'Root'; $selectedPageIcon = '//*[text()=\'' . $selectedPageTitle . '\']/../*[contains(@class, \'node-icon-container\')]'; @@ -65,7 +65,7 @@ class UsersCest extends AbstractCest $I->click($selectedPageIcon); $this->selectInContextMenu($I, [$this->contextMenuMore]); $I->waitForElementVisible('#contentMenu1', 5); - $I->seeElement($this->contextMenuExport); + $I->dontSeeElement($this->contextMenuExport); $I->dontSeeElement($this->contextMenuImport); $I->useExistingSession('admin'); @@ -74,19 +74,19 @@ class UsersCest extends AbstractCest /** * @throws \Exception */ - public function showImportInContextMenuForNonAdminUserIfFlagSet(ApplicationTester $I): void + public function showImportExportInContextMenuForNonAdminUserIfFlagSet(ApplicationTester $I): void { $selectedPageTitle = 'Root'; $selectedPageIcon = '//*[text()=\'' . $selectedPageTitle . '\']/../*[contains(@class, \'node-icon-container\')]'; - $this->setUserTsConfig($I, 2, 'options.impexp.enableImportForNonAdminUser = 1'); + $this->setUserTsConfig($I, 2, "options.impexp.enableImportForNonAdminUser = 1\noptions.impexp.enableExportForNonAdminUser = 1"); $I->useExistingSession('editor'); $I->click($selectedPageIcon); $this->selectInContextMenu($I, [$this->contextMenuMore]); $I->waitForElementVisible('#contentMenu1', 5); - $I->seeElement($this->contextMenuExport); $I->seeElement($this->contextMenuImport); + $I->seeElement($this->contextMenuExport); $I->useExistingSession('admin'); } diff --git a/typo3/sysext/core/Tests/Functional/Authentication/BackendUserAuthenticationTest.php b/typo3/sysext/core/Tests/Functional/Authentication/BackendUserAuthenticationTest.php index 7e77ab07d3f2134e7867c80544bcbae1ecd73f88..981c67eddfbeca4100a856f5407e2128e08972c2 100644 --- a/typo3/sysext/core/Tests/Functional/Authentication/BackendUserAuthenticationTest.php +++ b/typo3/sysext/core/Tests/Functional/Authentication/BackendUserAuthenticationTest.php @@ -147,4 +147,70 @@ class BackendUserAuthenticationTest extends FunctionalTestCase // which should fail since the user in the fixture has MFA activated but not yet passed. $this->setUpBackendUser(4); } + + public function isImportEnabledDataProvider(): array + { + return [ + 'admin user' => [ + 1, + '', + true, + ], + 'editor user' => [ + 2, + '', + false, + ], + 'editor user - enableImportForNonAdminUser = 1' => [ + 2, + 'options.impexp.enableImportForNonAdminUser = 1', + true, + ], + ]; + } + + /** + * @test + * @dataProvider isImportEnabledDataProvider + */ + public function isImportEnabledReturnsExpectedValues(int $userId, string $tsConfig, bool $expected): void + { + $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUserTSconfig'] = $tsConfig; + + $subject = $this->setUpBackendUser($userId); + self::assertEquals($expected, $subject->isImportEnabled()); + } + + public function isExportEnabledDataProvider(): array + { + return [ + 'admin user' => [ + 1, + '', + true, + ], + 'editor user' => [ + 2, + '', + false, + ], + 'editor user - enableExportForNonAdminUser = 1' => [ + 2, + 'options.impexp.enableExportForNonAdminUser = 1', + true, + ], + ]; + } + + /** + * @test + * @dataProvider isExportEnabledDataProvider + */ + public function isExportEnabledReturnsExpectedValues(int $userId, string $tsConfig, bool $expected): void + { + $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUserTSconfig'] = $tsConfig; + + $subject = $this->setUpBackendUser($userId); + self::assertEquals($expected, $subject->isExportEnabled()); + } } diff --git a/typo3/sysext/impexp/Classes/ContextMenu/ItemProvider.php b/typo3/sysext/impexp/Classes/ContextMenu/ItemProvider.php index a253f0f85595fdeec6ef81528de148835328047f..f1fa50a586ee3265ccb34447fd4942a3a75d5798 100644 --- a/typo3/sysext/impexp/Classes/ContextMenu/ItemProvider.php +++ b/typo3/sysext/impexp/Classes/ContextMenu/ItemProvider.php @@ -97,10 +97,10 @@ class ItemProvider extends AbstractProvider $canRender = false; switch ($itemName) { case 'exportT3d': - $canRender = true; + $canRender = $this->backendUser->isExportEnabled(); break; case 'importT3d': - $canRender = $this->table === 'pages' && $this->isImportEnabled(); + $canRender = $this->table === 'pages' && $this->backendUser->isImportEnabled(); break; } return $canRender; @@ -131,13 +131,4 @@ class ItemProvider extends AbstractProvider return $attributes; } - - /** - * Check if import functionality is available for current user - */ - protected function isImportEnabled(): bool - { - return $this->backendUser->isAdmin() - || (bool)($this->backendUser->getTSConfig()['options.']['impexp.']['enableImportForNonAdminUser'] ?? false); - } } diff --git a/typo3/sysext/impexp/Classes/Controller/ExportController.php b/typo3/sysext/impexp/Classes/Controller/ExportController.php index 2536a113a0b5408d672dd5a726de9366a164ce0b..adb9ae35ee29efa45d75bb1a02aa4beb6706829c 100644 --- a/typo3/sysext/impexp/Classes/Controller/ExportController.php +++ b/typo3/sysext/impexp/Classes/Controller/ExportController.php @@ -96,6 +96,14 @@ class ExportController extends ImportExportController */ public function mainAction(ServerRequestInterface $request): ResponseInterface { + if ($this->getBackendUser()->isExportEnabled() === false) { + throw new \RuntimeException( + 'Export module is disabled for non admin users and ' + . 'userTsConfig options.impexp.enableExportForNonAdminUser is not enabled.', + 1636901978 + ); + } + $this->main($request); // Input data diff --git a/typo3/sysext/impexp/Classes/Controller/ImportController.php b/typo3/sysext/impexp/Classes/Controller/ImportController.php index 0aa82d978bd12903333cfe04cda87b20b669818d..182446baf77ddf74e5140ac7042c7bd38e58bc8d 100644 --- a/typo3/sysext/impexp/Classes/Controller/ImportController.php +++ b/typo3/sysext/impexp/Classes/Controller/ImportController.php @@ -76,7 +76,7 @@ class ImportController extends ImportExportController */ public function mainAction(ServerRequestInterface $request): ResponseInterface { - if ($this->isImportEnabled() === false) { + if ($this->getBackendUser()->isImportEnabled() === false) { throw new \RuntimeException( 'Import module is disabled for non admin users and ' . 'userTsConfig options.impexp.enableImportForNonAdminUser is not enabled.', @@ -107,17 +107,6 @@ class ImportController extends ImportExportController return new HtmlResponse($this->moduleTemplate->renderContent()); } - /** - * Check if import functionality is available for current user - * - * @return bool - */ - protected function isImportEnabled(): bool - { - return $this->getBackendUser()->isAdmin() - || (bool)($this->getBackendUser()->getTSConfig()['options.']['impexp.']['enableImportForNonAdminUser'] ?? false); - } - protected function preprocessInputData(array $inData): array { if ($inData['new_import'] ?? false) { diff --git a/typo3/sysext/reports/Classes/Report/Status/SecurityStatus.php b/typo3/sysext/reports/Classes/Report/Status/SecurityStatus.php index 654ab07da32f1ef8a0d43ebf743736d69e671832..2e0ff200afa2e0a3ebb43e03863cb186004c1216 100644 --- a/typo3/sysext/reports/Classes/Report/Status/SecurityStatus.php +++ b/typo3/sysext/reports/Classes/Report/Status/SecurityStatus.php @@ -55,6 +55,7 @@ class SecurityStatus implements RequestAwareStatusProviderInterface 'fileDenyPattern' => $this->getFileDenyPatternStatus(), 'htaccessUpload' => $this->getHtaccessUploadStatus(), 'exceptionHandler' => $this->getExceptionHandlerStatus(), + 'exportedFiles' => $this->getExportedFilesStatus(), ]; if ($request !== null) { @@ -260,6 +261,51 @@ class SecurityStatus implements RequestAwareStatusProviderInterface return GeneralUtility::makeInstance(ReportStatus::class, $this->getLanguageService()->getLL('status_exceptionHandler'), $value, $message, $severity); } + protected function getExportedFilesStatus(): ReportStatus + { + $value = $this->getLanguageService()->getLL('status_ok'); + $message = ''; + $severity = ReportStatus::OK; + + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_file'); + $exportedFiles = $queryBuilder + ->select('storage', 'identifier') + ->from('sys_file') + ->where( + $queryBuilder->expr()->like( + 'identifier', + $queryBuilder->createNamedParameter('%/_temp_/importexport/%') + ), + $queryBuilder->expr()->or( + $queryBuilder->expr()->like( + 'identifier', + $queryBuilder->createNamedParameter('%.xml') + ), + $queryBuilder->expr()->like( + 'identifier', + $queryBuilder->createNamedParameter('%.t3d') + ) + ), + ) + ->executeQuery() + ->fetchAllAssociative(); + + if (count($exportedFiles) > 0) { + $files = []; + foreach ($exportedFiles as $exportedFile) { + $files[] = '<li>' . htmlspecialchars($exportedFile['storage'] . ':' . $exportedFile['identifier']) . '</li>'; + } + + $value = $this->getLanguageService()->getLL('status_insecure'); + $severity = ReportStatus::WARNING; + $message = $this->getLanguageService()->getLL('status_exportedFiles_warningMessage'); + $message .= '<ul>' . implode(PHP_EOL, $files) . '</ul>'; + $message .= $this->getLanguageService()->getLL('status_exportedFiles_warningRecommendation'); + } + + return GeneralUtility::makeInstance(ReportStatus::class, $this->getLanguageService()->getLL('status_exportedFiles'), $value, $message, $severity); + } + /** * @return LanguageService */ diff --git a/typo3/sysext/reports/Resources/Private/Language/locallang_reports.xlf b/typo3/sysext/reports/Resources/Private/Language/locallang_reports.xlf index e8eebf0adf6f8e9bce7fde91850236f75e4b5d15..6e5c86cad54aed61b3c419f2b6c4f5a32ed274a7 100644 --- a/typo3/sysext/reports/Resources/Private/Language/locallang_reports.xlf +++ b/typo3/sysext/reports/Resources/Private/Language/locallang_reports.xlf @@ -156,12 +156,21 @@ <trans-unit id="status_exceptionHandler" resname="status_exceptionHandler"> <source>Exception Handler / Error Reporting</source> </trans-unit> + <trans-unit id="status_exportedFiles" resname="status_exportedFiles"> + <source>XML/T3D export files</source> + </trans-unit> <trans-unit id="status_exceptionHandler_warningMessage" resname="status_exceptionHandler_warningMessage"> <source>Display Errors is set to 1 - errors will be displayed with the DebugExceptionHandler including stack traces.</source> </trans-unit> <trans-unit id="status_exceptionHandler_errorMessage" resname="status_exceptionHandler_errorMessage"> <source>Debug Exception Handler enabled in Production Context - will show full error messages including stack traces.</source> </trans-unit> + <trans-unit id="status_exportedFiles_warningMessage" resname="status_exportedFiles_warningMessage"> + <source>The following exported files where found:</source> + </trans-unit> + <trans-unit id="status_exportedFiles_warningRecommendation" resname="status_exportedFiles_warningRecommendation"> + <source>It is recommended to delete exported files to avoid possible disclosure of exported data to backend users with lower/different access rights than user(s) who originally created the export(s).</source> + </trans-unit> <trans-unit id="status_installToolPassword" resname="status_installToolPassword"> <source>Install Tool Password</source> </trans-unit>