Skip to content

Commit 7447a3d

Browse files
derhansenohader
authored andcommitted
[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>
1 parent 7879a3d commit 7447a3d

10 files changed

Lines changed: 236 additions & 33 deletions

File tree

typo3/sysext/core/Classes/Authentication/BackendUserAuthentication.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2289,4 +2289,26 @@ public function isMfaSetupRequired(): bool
22892289
|| ($globalConfig === 3 && $isAdmin)
22902290
|| ($globalConfig === 4 && $this->isSystemMaintainer());
22912291
}
2292+
2293+
/**
2294+
* Returns if import functionality is available for current user
2295+
*
2296+
* @internal
2297+
*/
2298+
public function isImportEnabled(): bool
2299+
{
2300+
return $this->isAdmin()
2301+
|| ($this->getTSConfig()['options.']['impexp.']['enableImportForNonAdminUser'] ?? false);
2302+
}
2303+
2304+
/**
2305+
* Returns if export functionality is available for current user
2306+
*
2307+
* @internal
2308+
*/
2309+
public function isExportEnabled(): bool
2310+
{
2311+
return $this->isAdmin()
2312+
|| ($this->getTSConfig()['options.']['impexp.']['enableExportForNonAdminUser'] ?? false);
2313+
}
22922314
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the TYPO3 CMS project.
7+
*
8+
* It is free software; you can redistribute it and/or modify it under
9+
* the terms of the GNU General Public License, either version 2
10+
* of the License, or any later version.
11+
*
12+
* For the full copyright and license information, please read the
13+
* LICENSE.txt file that was distributed with this source code.
14+
*
15+
* The TYPO3 project - inspiring people to share!
16+
*/
17+
18+
namespace TYPO3\CMS\Core\Resource\Filter;
19+
20+
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
21+
use TYPO3\CMS\Core\Resource\Driver\DriverInterface;
22+
23+
/**
24+
* Utility methods for filtering filenames stored in `importexport` temporary folder.
25+
* Albeit this filter is in the scope of `ext:impexp`, it is located in `ext:core` to
26+
* apply filters on left-over fragments, even when `ext:impexp` is not installed.
27+
*
28+
* @internal
29+
*/
30+
class ImportExportFilter
31+
{
32+
/**
33+
* Filter method that checks if a directory or a file in such directory belongs to the temp directory of EXT:impexp
34+
* and the user has "export" permissions.
35+
*/
36+
public static function filterImportExportFilesAndFolders(string $itemName, string $itemIdentifier, string $parentIdentifier, array $additionalInformation, DriverInterface $driverInstance)
37+
{
38+
// + `_temp_` is hard-coded in `BackendUserAuthentication::getDefaultUploadTemporaryFolder()`
39+
// + `importexport` is hard-coded in `ImportExport::createDefaultImportExportFolder()`
40+
$importExportFolderSubPath = '/_temp_/importexport/';
41+
if (str_ends_with($parentIdentifier, $importExportFolderSubPath) || str_contains($itemIdentifier, $importExportFolderSubPath)) {
42+
$backendUser = self::getBackendUser();
43+
if ($backendUser === null || !$backendUser->isExportEnabled()) {
44+
return -1;
45+
}
46+
}
47+
48+
return true;
49+
}
50+
51+
protected static function getBackendUser(): ?BackendUserAuthentication
52+
{
53+
return $GLOBALS['BE_USER'] ?? null;
54+
}
55+
}

typo3/sysext/core/Classes/Resource/ResourceStorage.php

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
use TYPO3\CMS\Core\Resource\Exception\ResourcePermissionsUnavailableException;
7575
use TYPO3\CMS\Core\Resource\Exception\UploadException;
7676
use TYPO3\CMS\Core\Resource\Exception\UploadSizeException;
77+
use TYPO3\CMS\Core\Resource\Filter\ImportExportFilter;
7778
use TYPO3\CMS\Core\Resource\Index\FileIndexRepository;
7879
use TYPO3\CMS\Core\Resource\Index\Indexer;
7980
use TYPO3\CMS\Core\Resource\OnlineMedia\Helpers\OnlineMediaHelperRegistry;
@@ -1517,14 +1518,27 @@ public function resetFileAndFolderNameFiltersToDefault()
15171518
$this->fileAndFolderNameFilters = $GLOBALS['TYPO3_CONF_VARS']['SYS']['fal']['defaultFilterCallbacks'];
15181519
}
15191520

1521+
/**
1522+
* Returns a filter for files generated by EXT:impexp
1523+
*
1524+
* @return array<int, ImportExportFilter|string>
1525+
* @internal
1526+
*/
1527+
public function getImportExportFilter(): array
1528+
{
1529+
$filter = GeneralUtility::makeInstance(ImportExportFilter::class);
1530+
1531+
return [$filter, 'filterImportExportFilesAndFolders'];
1532+
}
1533+
15201534
/**
15211535
* Returns the file and folder name filters used by this storage.
15221536
*
15231537
* @return array
15241538
*/
15251539
public function getFileAndFolderNameFilters()
15261540
{
1527-
return $this->fileAndFolderNameFilters;
1541+
return array_merge($this->fileAndFolderNameFilters, [$this->getImportExportFilter()]);
15281542
}
15291543

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

15901604
$rows = $this->getFileIndexRepository()->findByFolder($folder);
15911605

1592-
$filters = $useFilters == true ? $this->fileAndFolderNameFilters : [];
1606+
$filters = $useFilters == true ? $this->getFileAndFolderNameFilters() : [];
15931607
$fileIdentifiers = array_values($this->driver->getFilesInFolder($folder->getIdentifier(), $start, $maxNumberOfItems, $recursive, $filters, $sort, $sortRev));
15941608

15951609
$items = [];
@@ -1619,7 +1633,7 @@ public function getFilesInFolder(Folder $folder, $start = 0, $maxNumberOfItems =
16191633
*/
16201634
public function getFileIdentifiersInFolder($folderIdentifier, $useFilters = true, $recursive = false)
16211635
{
1622-
$filters = $useFilters == true ? $this->fileAndFolderNameFilters : [];
1636+
$filters = $useFilters == true ? $this->getFileAndFolderNameFilters() : [];
16231637
return $this->driver->getFilesInFolder($folderIdentifier, 0, 0, $recursive, $filters);
16241638
}
16251639

@@ -1633,7 +1647,7 @@ public function getFileIdentifiersInFolder($folderIdentifier, $useFilters = true
16331647
public function countFilesInFolder(Folder $folder, $useFilters = true, $recursive = false)
16341648
{
16351649
$this->assureFolderReadPermission($folder);
1636-
$filters = $useFilters ? $this->fileAndFolderNameFilters : [];
1650+
$filters = $useFilters ? $this->getFileAndFolderNameFilters() : [];
16371651
return $this->driver->countFilesInFolder($folder->getIdentifier(), $recursive, $filters);
16381652
}
16391653

@@ -1645,7 +1659,7 @@ public function countFilesInFolder(Folder $folder, $useFilters = true, $recursiv
16451659
*/
16461660
public function getFolderIdentifiersInFolder($folderIdentifier, $useFilters = true, $recursive = false)
16471661
{
1648-
$filters = $useFilters == true ? $this->fileAndFolderNameFilters : [];
1662+
$filters = $useFilters == true ? $this->getFileAndFolderNameFilters() : [];
16491663
return $this->driver->getFoldersInFolder($folderIdentifier, 0, 0, $recursive, $filters);
16501664
}
16511665

@@ -2417,7 +2431,7 @@ public function getFolderInFolder($folderName, Folder $parentFolder, $returnInac
24172431
*/
24182432
public function getFoldersInFolder(Folder $folder, $start = 0, $maxNumberOfItems = 0, $useFilters = true, $recursive = false, $sort = '', $sortRev = false)
24192433
{
2420-
$filters = $useFilters == true ? $this->fileAndFolderNameFilters : [];
2434+
$filters = $useFilters == true ? $this->getFileAndFolderNameFilters() : [];
24212435

24222436
$folderIdentifiers = $this->driver->getFoldersInFolder($folder->getIdentifier(), $start, $maxNumberOfItems, $recursive, $filters, $sort, $sortRev);
24232437

@@ -2428,6 +2442,7 @@ public function getFoldersInFolder(Folder $folder, $start = 0, $maxNumberOfItems
24282442
unset($folderIdentifiers[$processingIdentifier]);
24292443
}
24302444
}
2445+
24312446
$folders = [];
24322447
foreach ($folderIdentifiers as $folderIdentifier) {
24332448
$folders[$folderIdentifier] = $this->getFolder($folderIdentifier, true);
@@ -2445,7 +2460,7 @@ public function getFoldersInFolder(Folder $folder, $start = 0, $maxNumberOfItems
24452460
public function countFoldersInFolder(Folder $folder, $useFilters = true, $recursive = false)
24462461
{
24472462
$this->assureFolderReadPermission($folder);
2448-
$filters = $useFilters ? $this->fileAndFolderNameFilters : [];
2463+
$filters = $useFilters ? $this->getFileAndFolderNameFilters() : [];
24492464
return $this->driver->countFoldersInFolder($folder->getIdentifier(), $recursive, $filters);
24502465
}
24512466

typo3/sysext/core/Tests/Acceptance/Application/Impexp/UsersCest.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public function _before(ApplicationTester $I): void
5252
/**
5353
* @throws \Exception
5454
*/
55-
public function doNotShowImportInContextMenuForNonAdminUser(ApplicationTester $I, PageTree $pageTree): void
55+
public function doNotShowImportAndExportInContextMenuForNonAdminUser(ApplicationTester $I, PageTree $pageTree): void
5656
{
5757
$selectedPageTitle = 'Root';
5858
$selectedPageIcon = '//*[text()=\'' . $selectedPageTitle . '\']/../*[contains(@class, \'node-icon-container\')]';
@@ -65,7 +65,7 @@ public function doNotShowImportInContextMenuForNonAdminUser(ApplicationTester $I
6565
$I->click($selectedPageIcon);
6666
$this->selectInContextMenu($I, [$this->contextMenuMore]);
6767
$I->waitForElementVisible('#contentMenu1', 5);
68-
$I->seeElement($this->contextMenuExport);
68+
$I->dontSeeElement($this->contextMenuExport);
6969
$I->dontSeeElement($this->contextMenuImport);
7070

7171
$I->useExistingSession('admin');
@@ -74,19 +74,19 @@ public function doNotShowImportInContextMenuForNonAdminUser(ApplicationTester $I
7474
/**
7575
* @throws \Exception
7676
*/
77-
public function showImportInContextMenuForNonAdminUserIfFlagSet(ApplicationTester $I): void
77+
public function showImportExportInContextMenuForNonAdminUserIfFlagSet(ApplicationTester $I): void
7878
{
7979
$selectedPageTitle = 'Root';
8080
$selectedPageIcon = '//*[text()=\'' . $selectedPageTitle . '\']/../*[contains(@class, \'node-icon-container\')]';
8181

82-
$this->setUserTsConfig($I, 2, 'options.impexp.enableImportForNonAdminUser = 1');
82+
$this->setUserTsConfig($I, 2, "options.impexp.enableImportForNonAdminUser = 1\noptions.impexp.enableExportForNonAdminUser = 1");
8383
$I->useExistingSession('editor');
8484

8585
$I->click($selectedPageIcon);
8686
$this->selectInContextMenu($I, [$this->contextMenuMore]);
8787
$I->waitForElementVisible('#contentMenu1', 5);
88-
$I->seeElement($this->contextMenuExport);
8988
$I->seeElement($this->contextMenuImport);
89+
$I->seeElement($this->contextMenuExport);
9090

9191
$I->useExistingSession('admin');
9292
}

typo3/sysext/core/Tests/Functional/Authentication/BackendUserAuthenticationTest.php

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,4 +147,70 @@ public function mfaRequiredExceptionIsThrown(): void
147147
// which should fail since the user in the fixture has MFA activated but not yet passed.
148148
$this->setUpBackendUser(4);
149149
}
150+
151+
public function isImportEnabledDataProvider(): array
152+
{
153+
return [
154+
'admin user' => [
155+
1,
156+
'',
157+
true,
158+
],
159+
'editor user' => [
160+
2,
161+
'',
162+
false,
163+
],
164+
'editor user - enableImportForNonAdminUser = 1' => [
165+
2,
166+
'options.impexp.enableImportForNonAdminUser = 1',
167+
true,
168+
],
169+
];
170+
}
171+
172+
/**
173+
* @test
174+
* @dataProvider isImportEnabledDataProvider
175+
*/
176+
public function isImportEnabledReturnsExpectedValues(int $userId, string $tsConfig, bool $expected): void
177+
{
178+
$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUserTSconfig'] = $tsConfig;
179+
180+
$subject = $this->setUpBackendUser($userId);
181+
self::assertEquals($expected, $subject->isImportEnabled());
182+
}
183+
184+
public function isExportEnabledDataProvider(): array
185+
{
186+
return [
187+
'admin user' => [
188+
1,
189+
'',
190+
true,
191+
],
192+
'editor user' => [
193+
2,
194+
'',
195+
false,
196+
],
197+
'editor user - enableExportForNonAdminUser = 1' => [
198+
2,
199+
'options.impexp.enableExportForNonAdminUser = 1',
200+
true,
201+
],
202+
];
203+
}
204+
205+
/**
206+
* @test
207+
* @dataProvider isExportEnabledDataProvider
208+
*/
209+
public function isExportEnabledReturnsExpectedValues(int $userId, string $tsConfig, bool $expected): void
210+
{
211+
$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUserTSconfig'] = $tsConfig;
212+
213+
$subject = $this->setUpBackendUser($userId);
214+
self::assertEquals($expected, $subject->isExportEnabled());
215+
}
150216
}

typo3/sysext/impexp/Classes/ContextMenu/ItemProvider.php

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,10 @@ protected function canRender(string $itemName, string $type): bool
9797
$canRender = false;
9898
switch ($itemName) {
9999
case 'exportT3d':
100-
$canRender = true;
100+
$canRender = $this->backendUser->isExportEnabled();
101101
break;
102102
case 'importT3d':
103-
$canRender = $this->table === 'pages' && $this->isImportEnabled();
103+
$canRender = $this->table === 'pages' && $this->backendUser->isImportEnabled();
104104
break;
105105
}
106106
return $canRender;
@@ -131,13 +131,4 @@ protected function getAdditionalAttributes(string $itemName): array
131131

132132
return $attributes;
133133
}
134-
135-
/**
136-
* Check if import functionality is available for current user
137-
*/
138-
protected function isImportEnabled(): bool
139-
{
140-
return $this->backendUser->isAdmin()
141-
|| (bool)($this->backendUser->getTSConfig()['options.']['impexp.']['enableImportForNonAdminUser'] ?? false);
142-
}
143134
}

typo3/sysext/impexp/Classes/Controller/ExportController.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,14 @@ public function __construct(
8181

8282
public function handleRequest(ServerRequestInterface $request): ResponseInterface
8383
{
84+
if ($this->getBackendUser()->isExportEnabled() === false) {
85+
throw new \RuntimeException(
86+
'Export module is disabled for non admin users and '
87+
. 'userTsConfig options.impexp.enableExportForNonAdminUser is not enabled.',
88+
1636901978
89+
);
90+
}
91+
8492
$backendUser = $this->getBackendUser();
8593
$queryParams = $request->getQueryParams();
8694
$parsedBody = $request->getParsedBody();

typo3/sysext/impexp/Classes/Controller/ImportController.php

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public function __construct(
5959

6060
public function handleRequest(ServerRequestInterface $request): ResponseInterface
6161
{
62-
if (!$this->isImportEnabled()) {
62+
if (!$this->getBackendUser()->isImportEnabled()) {
6363
throw new \RuntimeException(
6464
'Import module is disabled for non admin users and userTsConfig options.impexp.enableImportForNonAdminUser is not enabled.',
6565
1464435459
@@ -142,15 +142,6 @@ protected function addDocHeaderPreviewButton(ModuleTemplate $view, int $pageUid)
142142
$buttonBar->addButton($viewButton);
143143
}
144144

145-
/**
146-
* Check if import functionality is available for current user.
147-
*/
148-
protected function isImportEnabled(): bool
149-
{
150-
$backendUser = $this->getBackendUser();
151-
return $backendUser->isAdmin() || ($backendUser->getTSConfig()['options.']['impexp.']['enableImportForNonAdminUser'] ?? false);
152-
}
153-
154145
protected function handleFileUpload(ServerRequestInterface $request): ?File
155146
{
156147
$parsedBody = $request->getParsedBody() ?? [];

0 commit comments

Comments
 (0)