Skip to content
Permalink
Browse files Browse the repository at this point in the history
[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>
  • Loading branch information
derhansen authored and ohader committed Jun 14, 2022
1 parent 7879a3d commit 7447a3d
Show file tree
Hide file tree
Showing 10 changed files with 236 additions and 33 deletions.
Expand Up @@ -2289,4 +2289,26 @@ public function isMfaSetupRequired(): bool
|| ($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);
}
}
55 changes: 55 additions & 0 deletions 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;
}
}
29 changes: 22 additions & 7 deletions typo3/sysext/core/Classes/Resource/ResourceStorage.php
Expand Up @@ -74,6 +74,7 @@
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;
Expand Down Expand Up @@ -1517,14 +1518,27 @@ 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()]);
}

/**
Expand Down Expand Up @@ -1589,7 +1603,7 @@ public function getFilesInFolder(Folder $folder, $start = 0, $maxNumberOfItems =

$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 = [];
Expand Down Expand Up @@ -1619,7 +1633,7 @@ public function getFilesInFolder(Folder $folder, $start = 0, $maxNumberOfItems =
*/
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);
}

Expand All @@ -1633,7 +1647,7 @@ public function getFileIdentifiersInFolder($folderIdentifier, $useFilters = true
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);
}

Expand All @@ -1645,7 +1659,7 @@ public function countFilesInFolder(Folder $folder, $useFilters = true, $recursiv
*/
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);
}

Expand Down Expand Up @@ -2417,7 +2431,7 @@ public function getFolderInFolder($folderName, Folder $parentFolder, $returnInac
*/
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);

Expand All @@ -2428,6 +2442,7 @@ public function getFoldersInFolder(Folder $folder, $start = 0, $maxNumberOfItems
unset($folderIdentifiers[$processingIdentifier]);
}
}

$folders = [];
foreach ($folderIdentifiers as $folderIdentifier) {
$folders[$folderIdentifier] = $this->getFolder($folderIdentifier, true);
Expand All @@ -2445,7 +2460,7 @@ public function getFoldersInFolder(Folder $folder, $start = 0, $maxNumberOfItems
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);
}

Expand Down
Expand Up @@ -52,7 +52,7 @@ public function _before(ApplicationTester $I): void
/**
* @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\')]';
Expand All @@ -65,7 +65,7 @@ public function doNotShowImportInContextMenuForNonAdminUser(ApplicationTester $I
$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');
Expand All @@ -74,19 +74,19 @@ public function doNotShowImportInContextMenuForNonAdminUser(ApplicationTester $I
/**
* @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');
}
Expand Down
Expand Up @@ -147,4 +147,70 @@ public function mfaRequiredExceptionIsThrown(): void
// 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());
}
}
13 changes: 2 additions & 11 deletions typo3/sysext/impexp/Classes/ContextMenu/ItemProvider.php
Expand Up @@ -97,10 +97,10 @@ protected function canRender(string $itemName, string $type): bool
$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;
Expand Down Expand Up @@ -131,13 +131,4 @@ protected function getAdditionalAttributes(string $itemName): array

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);
}
}
8 changes: 8 additions & 0 deletions typo3/sysext/impexp/Classes/Controller/ExportController.php
Expand Up @@ -81,6 +81,14 @@ public function __construct(

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();
Expand Down
11 changes: 1 addition & 10 deletions typo3/sysext/impexp/Classes/Controller/ImportController.php
Expand Up @@ -59,7 +59,7 @@ public function __construct(

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
Expand Down Expand Up @@ -142,15 +142,6 @@ protected function addDocHeaderPreviewButton(ModuleTemplate $view, int $pageUid)
$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() ?? [];
Expand Down

0 comments on commit 7447a3d

Please sign in to comment.