CVE-2022-31046: Cleartext Transmission of Sensitive Information (CWE-319)
value: TYPO3 is an open source web content management system. Prior to versions 7.6.57 ELTS, 8.7.47 ELTS, 9.5.34 ELTS, 10.4.29, and 11.5.11, the export functionality fails to limit the result set to allowed columns of a particular database table. This way, authenticated users can export internal details of database tables they already have access to. TYPO3 versions 7.6.57 ELTS, 8.7.47 ELTS, 9.5.34 ELTS, 10.4.29, 11.5.11 fix the problem described above. In order to address this issue, access to mentioned export functionality is completely denied for regular backend users.
CWE description: The product transmits sensitive or security-critical data in cleartext in a communication channel that can be sniffed by unauthorized actors.
CVSS v3 base score: 4.3
Published: 2022-06-14T21:15Z
Repository: https://github.com/TYPO3/typo3
Language: PHP
Files changed: typo3/sysext/core/Classes/Authentication/BackendUserAuthentication.php, typo3/sysext/core/Classes/Resource/Filter/ImportExportFilter.php, typo3/sysext/core/Classes/Resource/ResourceStorage.php, typo3/sysext/core/Tests/Acceptance/Application/Impexp/UsersCest.php, typo3/sysext/core/Tests/Functional/Authentication/BackendUserAuthenticationTest.php, typo3/sysext/impexp/Classes/ContextMenu/ItemProvider.php, typo3/sysext/impexp/Classes/Controller/ExportController.php, typo3/sysext/impexp/Classes/Controller/ImportController.php, typo3/sysext/reports/Classes/Report/Status/SecurityStatus.php, typo3/sysext/reports/Resources/Private/Language/locallang_reports.xlf
Commit date: 2022-06-14 09:13:00 +0200
Commit message: [SECURITY] Restrict export functionality to allowed users

The import functionality of the import/export module is already
restricted to admin users or users, who explicitly have access through
the user TSConfig setting "options.impexp.enableImportForNonAdminUser".

The export functionality has the following security drawbacks:

* Export for editors is not limited on field level
* The "Save to filename" functionality saves to a shared folder, which
  other editors with different access rights may have access to.

Both issues are not easy to resolve and also the target audience for
the Import/Export functionality are mainly TYPO3 admins.

Therefore, now also the export functionality is restricted to TYPO3
admin users and to users, who explicitly have access through the new
user TSConfig setting "options.impexp.enableExportForNonAdminUser".

Additionally, the contents of the temporary "importexport" folder in
file storages is now only visible to users who have access to the
export functionality.

In general, it is recommended to only install the Import/Export
extension when the functionality is required.

Resolves: #94951
Releases: main, 11.5, 10.4
Change-Id: Iae020baf051aeec0613366687aa8ebcbf9b3d8b2
Security-Bulletin: TYPO3-CORE-SA-2022-001
Security-References: CVE-2022-31046
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/74902
Tested-by: Oliver Hader <oliver.hader@typo3.org>
Reviewed-by: Oliver Hader <oliver.hader@typo3.org>

Diff: diff --git a/typo3/sysext/core/Classes/Authentication/BackendUserAuthentication.php b/typo3/sysext/core/Classes/Authentication/BackendUserAuthentication.php
index fa93dd5fa26..8183f6e2ff8 100644
--- a/typo3/sysext/core/Classes/Authentication/BackendUserAuthentication.php
+++ b/typo3/sysext/core/Classes/Authentication/BackendUserAuthentication.php
@@ -2287,6 +2287,28 @@ TCAdefaults.sys_note.email = ' . $this->user['email'];
         $isAdmin = $this->isAdmin();
         return ($globalConfig === 2 && !$isAdmin)
             || ($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 00000000000..789f02ee7b4
--- /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 4933a98a40d..246b2f08a8f 100644
--- a/typo3/sysext/core/Classes/Resource/ResourceStorage.php
+++ b/typo3/sysext/core/Classes/Resource/ResourceStorage.php
@@ -72,10 +72,11 @@ use TYPO3\CMS\Core\Resource\Exception\InvalidConfigurationException;
 use TYPO3\CMS\Core\Resource\Exception\InvalidHashException;
 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;
 use TYPO3\CMS\Core\Resource\Search\FileSearchDemand;
 use TYPO3\CMS\Core\Resource\Search\Result\DriverFilteredSearchResult;
@@ -1515,18 +1516,31 @@ class ResourceStorage implements ResourceStorageInterface
     public function resetFileAndFolderNameFiltersToDefault()
     {
         $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.
      *
      * @return array
      */
     public function getFileAndFolderNameFilters()
     {
-        return $this->fileAndFolderNameFilters;
+        return array_merge($this->fileAndFolderNameFilters, [$this->getImportExportFilter()]);
     }
 
     /**
      * @param array $filters
      * @return $this
@@ -1587,11 +1601,11 @@ class ResourceStorage implements ResourceStorageInterface
     {
         $this->assureFolderReadPermission($folder);
 
         $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 = [];
         foreach ($fileIdentifiers as $identifier) {
             if (isset($rows[$identifier])) {
@@ -1617,11 +1631,11 @@ class ResourceStorage implements ResourceStorageInterface
      * @param bool $recursive
      * @return array
      */
     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);
     }
 
     /**
      * @param Folder $folder
@@ -1631,11 +1645,11 @@ class ResourceStorage implements ResourceStorageInterface
      * @throws Exception\InsufficientFolderAccessPermissionsException
      */
     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);
     }
 
     /**
      * @param string $folderIdentifier
@@ -1643,11 +1657,11 @@ class ResourceStorage implements ResourceStorageInterface
      * @param bool $recursive
      * @return array
      */
     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);
     }
 
     /**
      * Returns TRUE if the specified file exists
@@ -2415,21 +2429,22 @@ class ResourceStorage implements ResourceStorageInterface
      * @param bool $sortRev TRUE to indicate reverse sorting (last to first)
      * @return Folder[]
      */
     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);
 
         // Exclude processing folders
         foreach ($this->getProcessingFolders() as $processingFolder) {
             $processingIdentifier = $processingFolder->getIdentifier();
             if (isset($folderIdentifiers[$processingIdentifier])) {
                 unset($folderIdentifiers[$processingIdentifier]);
             }
         }
+
         $folders = [];
         foreach ($folderIdentifiers as $folderIdentifier) {
             $folders[$folderIdentifier] = $this->getFolder($folderIdentifier, true);
         }
         return $folders;
@@ -2443,11 +2458,11 @@ class ResourceStorage implements ResourceStorageInterface
      * @throws Exception\InsufficientFolderAccessPermissionsException
      */
     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);
     }
 
     /**
      * Returns TRUE if the specified folder exists.
diff --git a/typo3/sysext/core/Tests/Acceptance/Application/Impexp/UsersCest.php b/typo3/sysext/core/Tests/Acceptance/Application/Impexp/UsersCest.php
index 85ea5adfecd..af0c7871bdf 100644
--- a/typo3/sysext/core/Tests/Acceptance/Application/Impexp/UsersCest.php
+++ b/typo3/sysext/core/Tests/Acceptance/Application/Impexp/UsersCest.php
@@ -50,11 +50,11 @@ 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\')]';
 
         $this->setPageAccess($I, $pageTree, [$selectedPageTitle], 1);
@@ -63,32 +63,32 @@ class UsersCest extends AbstractCest
         $I->useExistingSession('editor');
 
         $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');
     }
 
     /**
      * @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 693bfdc8235..2ed0db2f2a7 100644
--- a/typo3/sysext/core/Tests/Functional/Authentication/BackendUserAuthenticationTest.php
+++ b/typo3/sysext/core/Tests/Functional/Authentication/BackendUserAuthenticationTest.php
@@ -145,6 +145,72 @@ class BackendUserAuthenticationTest extends FunctionalTestCase
         $this->expectException(MfaRequiredException::class);
         // This will setup a user and therefore implicit call the ->checkAuthentication() method
         // 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 a3e12055312..a3ad969622e 100644
--- a/typo3/sysext/impexp/Classes/ContextMenu/ItemProvider.php
+++ b/typo3/sysext/impexp/Classes/ContextMenu/ItemProvider.php
@@ -95,14 +95,14 @@ class ItemProvider extends AbstractProvider
             return false;
         }
         $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;
     }
 
@@ -129,15 +129,6 @@ class ItemProvider extends AbstractProvider
                 break;
         }
 
         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 68078195388..7d1a4d1dddf 100644
--- a/typo3/sysext/impexp/Classes/Controller/ExportController.php
+++ b/typo3/sysext/impexp/Classes/Controller/ExportController.php
@@ -79,10 +79,18 @@ class ExportController
     ) {
     }
 
     public function handleRequest(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
+            );
+        }
+
         $backendUser = $this->getBackendUser();
         $queryParams = $request->getQueryParams();
         $parsedBody = $request->getParsedBody();
 
         $id = (int)($parsedBody['id'] ?? $queryParams['id'] ?? 0);
diff --git a/typo3/sysext/impexp/Classes/Controller/ImportController.php b/typo3/sysext/impexp/Classes/Controller/ImportController.php
index 3c84d37db03..d155952de6d 100644
--- a/typo3/sysext/impexp/Classes/Controller/ImportController.php
+++ b/typo3/sysext/impexp/Classes/Controller/ImportController.php
@@ -57,11 +57,11 @@ class ImportController
     ) {
     }
 
     public function handleRequest(ServerRequestInterface $request): ResponseInterface
     {
-        if (!$this->isImportEnabled()) {
+        if (!$this->getBackendUser()->isImportEnabled()) {
             throw new \RuntimeException(
                 'Import module is disabled for non admin users and userTsConfig options.impexp.enableImportForNonAdminUser is not enabled.',
                 1464435459
             );
         }
@@ -140,19 +140,10 @@ class ImportController
             ->setIcon($this->iconFactory->getIcon('actions-view-page', Icon::SIZE_SMALL))
             ->setDataAttributes($previewDataAttributes ?? []);
         $buttonBar->addButton($viewButton);
     }
 
-    /**
-     * Check if import functionality is available for current user.
-     */
-    protected function isImportEnabled(): bool
-    {
-        $backendUser = $this->getBackendUser();
-        return $backendUser->isAdmin() || ($backendUser->getTSConfig()['options.']['impexp.']['enableImportForNonAdminUser'] ?? false);
-    }
-
     protected function handleFileUpload(ServerRequestInterface $request): ?File
     {
         $parsedBody = $request->getParsedBody() ?? [];
         $file = $parsedBody['file'] ?? [];
         $conflictMode = empty($parsedBody['overwriteExistingFiles']) ? DuplicationBehavior::CANCEL : DuplicationBehavior::REPLACE;
diff --git a/typo3/sysext/reports/Classes/Report/Status/SecurityStatus.php b/typo3/sysext/reports/Classes/Report/Status/SecurityStatus.php
index b49d2dd6d56..8a99b3446ec 100644
--- a/typo3/sysext/reports/Classes/Report/Status/SecurityStatus.php
+++ b/typo3/sysext/reports/Classes/Report/Status/SecurityStatus.php
@@ -53,10 +53,11 @@ class SecurityStatus implements RequestAwareStatusProviderInterface
             'trustedHostsPattern' => $this->getTrustedHostsPatternStatus(),
             'adminUserAccount' => $this->getAdminAccountStatus(),
             'fileDenyPattern' => $this->getFileDenyPatternStatus(),
             'htaccessUpload' => $this->getHtaccessUploadStatus(),
             'exceptionHandler' => $this->getExceptionHandlerStatus(),
+            'exportedFiles' => $this->getExportedFilesStatus(),
         ];
 
         if ($request !== null) {
             $statuses['encryptedConnectionStatus'] = $this->getEncryptedConnectionStatus($request);
             $lockSslStatus = $this->getLockSslStatus($request);
@@ -263,10 +264,55 @@ class SecurityStatus implements RequestAwareStatusProviderInterface
             $message = $this->getLanguageService()->getLL('status_exceptionHandler_warningMessage');
         }
         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);
+    }
+
     protected function getLanguageService(): LanguageService
     {
         return $GLOBALS['LANG'];
     }
 }
diff --git a/typo3/sysext/reports/Resources/Private/Language/locallang_reports.xlf b/typo3/sysext/reports/Resources/Private/Language/locallang_reports.xlf
index 27c703e872a..e9084dfad25 100644
--- a/typo3/sysext/reports/Resources/Private/Language/locallang_reports.xlf
+++ b/typo3/sysext/reports/Resources/Private/Language/locallang_reports.xlf
@@ -154,16 +154,25 @@
 				<source>.htaccess Upload Protection</source>
 			</trans-unit>
 			<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_installTool" resname="status_installTool">
 				<source>Install Tool</source>
 			</trans-unit>
 			<trans-unit id="status_phpVersion" resname="status_phpVersion">
 				<source>PHP Version</source>

Vulnerable code:         return $this->fileAndFolderNameFilters;
        $filters = $useFilters == true ? $this->fileAndFolderNameFilters : [];
        $filters = $useFilters == true ? $this->fileAndFolderNameFilters : [];
        $filters = $useFilters ? $this->fileAndFolderNameFilters : [];
        $filters = $useFilters == true ? $this->fileAndFolderNameFilters : [];
        $filters = $useFilters == true ? $this->fileAndFolderNameFilters : [];
        $filters = $useFilters ? $this->fileAndFolderNameFilters : [];
    public function doNotShowImportInContextMenuForNonAdminUser(ApplicationTester $I, PageTree $pageTree): void
        $I->seeElement($this->contextMenuExport);
    public function showImportInContextMenuForNonAdminUserIfFlagSet(ApplicationTester $I): void
        $this->setUserTsConfig($I, 2, 'options.impexp.enableImportForNonAdminUser = 1');
        $I->seeElement($this->contextMenuExport);
                $canRender = true;
                $canRender = $this->table === 'pages' && $this->isImportEnabled();

    /**
     * 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);
    }
        if (!$this->isImportEnabled()) {
    /**
     * Check if import functionality is available for current user.
     */
    protected function isImportEnabled(): bool
    {
        $backendUser = $this->getBackendUser();
        return $backendUser->isAdmin() || ($backendUser->getTSConfig()['options.']['impexp.']['enableImportForNonAdminUser'] ?? false);
    }

Fixed code: 
    /**
     * 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);
    }
<?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;
    }
}
use TYPO3\CMS\Core\Resource\Filter\ImportExportFilter;
    /**
     * 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'];
    }

        return array_merge($this->fileAndFolderNameFilters, [$this->getImportExportFilter()]);
        $filters = $useFilters == true ? $this->getFileAndFolderNameFilters() : [];
        $filters = $useFilters == true ? $this->getFileAndFolderNameFilters() : [];
        $filters = $useFilters ? $this->getFileAndFolderNameFilters() : [];
        $filters = $useFilters == true ? $this->getFileAndFolderNameFilters() : [];
        $filters = $useFilters == true ? $this->getFileAndFolderNameFilters() : [];

        $filters = $useFilters ? $this->getFileAndFolderNameFilters() : [];
    public function doNotShowImportAndExportInContextMenuForNonAdminUser(ApplicationTester $I, PageTree $pageTree): void
        $I->dontSeeElement($this->contextMenuExport);
    public function showImportExportInContextMenuForNonAdminUserIfFlagSet(ApplicationTester $I): void
        $this->setUserTsConfig($I, 2, "options.impexp.enableImportForNonAdminUser = 1\noptions.impexp.enableExportForNonAdminUser = 1");
        $I->seeElement($this->contextMenuExport);

    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());
    }
                $canRender = $this->backendUser->isExportEnabled();
                $canRender = $this->table === 'pages' && $this->backendUser->isImportEnabled();
        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
            );
        }

        if (!$this->getBackendUser()->isImportEnabled()) {
            'exportedFiles' => $this->getExportedFilesStatus(),
    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);
    }

			<trans-unit id="status_exportedFiles" resname="status_exportedFiles">
				<source>XML/T3D export files</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>