Skip to content

Commit

Permalink
[SECURITY] Deny directly modifying file abstraction layer entities
Browse files Browse the repository at this point in the history
Write access to table `sys_file` is denied per default, unless data
is being imported. In addition, write access to related FAL entities
`sys_file_reference` and `sys_file_metadata` is denied in case a file
on legacy storage (uid=0) is used or corresponding user does not have
permissions to access a particular file.

Resolves: #93969
Releases: main, 13.0, 12.4, 11.5
Change-Id: Ic8ac7132d732bd117aa63f6a33545ceb1d1f421d
Security-Bulletin: TYPO3-CORE-SA-2024-006
Security-References: CVE-2024-25121
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/82956
Tested-by: Oliver Hader <oliver.hader@typo3.org>
Reviewed-by: Oliver Hader <oliver.hader@typo3.org>
  • Loading branch information
ohader committed Feb 13, 2024
1 parent ae0dfc4 commit 38f0bf9
Show file tree
Hide file tree
Showing 20 changed files with 361 additions and 0 deletions.
4 changes: 4 additions & 0 deletions typo3/sysext/core/Classes/DataHandling/DataHandler.php
Expand Up @@ -865,6 +865,10 @@ public function process_datamap()
foreach ($hookObjectsArr as $hookObj) {
if (method_exists($hookObj, 'processDatamap_preProcessFieldArray')) {
$hookObj->processDatamap_preProcessFieldArray($incomingFieldArray, $table, $id, $this);
// in case hook invalidated `$incomingFieldArray`, skip the record completely
if (!is_array($incomingFieldArray)) {
continue 2;
}
}
}
// ******************************
Expand Down
202 changes: 202 additions & 0 deletions typo3/sysext/core/Classes/Resource/Security/FilePermissionAspect.php
@@ -0,0 +1,202 @@
<?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\Security;

use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\DataHandling\DataHandlerCheckModifyAccessListHookInterface;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\SysLog\Action\Database as SystemLogDatabaseAction;
use TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;

/**
* `DataHandler` hook handling to avoid direct access to `sys_file` related entities:
*
* + denies any write access to `sys_file` (in datamap and cmdmap, unless it is an internal process)
* + denies any write access to `sys_file` that is on legacy storage
* + denies any write access to `sys_file_reference`, referencing a file on legacy storage,
* or not part of the file-mounts of the corresponding user
* + denies any write access to `sys_file_metadata`, referencing a file on legacy storage,
* or not part of the file-mounts of the corresponding user
*/
class FilePermissionAspect implements DataHandlerCheckModifyAccessListHookInterface
{
protected ResourceFactory $resourceFactory;

public function __construct(ResourceFactory $resourceFactory = null)
{
$this->resourceFactory = $resourceFactory ?? GeneralUtility::makeInstance(ResourceFactory::class);
}

/**
* Denies write access to `sys_file` in general, unless it is an internal process.
*
* @param bool &$accessAllowed
* @param string $table
* @param DataHandler $parent
*/
public function checkModifyAccessList(&$accessAllowed, $table, DataHandler $parent): void
{
$isInternalProcess = $parent->isImporting || $parent->bypassAccessCheckForRecords;
if ($table === 'sys_file' && !$isInternalProcess) {
$accessAllowed = false;
}
}

/**
* Checks file related data being processed in `DataHandler`:
* + `sys_file` (only if `checkModifyAccessList` passed -> during internal process)
* + `sys_file_reference`
* + `sys_file_metadata`
*
* @param mixed $incomingFieldArray
* @param string $table
* @param int|string $id
* @param DataHandler $dataHandler
*/
public function processDatamap_preProcessFieldArray(&$incomingFieldArray, string $table, $id, DataHandler $dataHandler): void
{
if (!is_array($incomingFieldArray) || !is_scalar($id)) {
$incomingFieldArray = null;
return;
}
$isInternalProcess = $dataHandler->isImporting || $dataHandler->bypassAccessCheckForRecords;
$isNew = !MathUtility::canBeInterpretedAsInteger($id);
$logId = $isNew ? 0 : (int)$id;
if ($table === 'sys_file') {
$file = $this->resolveFile((int)$id);
if (!$this->isValidStorageData($incomingFieldArray)
|| (!$isNew && $file !== null && $this->usesLegacyStorage($file))
) {
$incomingFieldArray = null;
$this->logError($table, $logId, 'Attempt to set legacy storage directly is disallowed', $dataHandler);
}
} elseif ($table === 'sys_file_reference') {
$files = $this->resolveReferencedFiles($incomingFieldArray, 'uid_local');
foreach ($files as $file) {
if ($file === null) {
$incomingFieldArray = null;
$this->logError($table, $logId, 'Attempt to reference invalid file is disallowed', $dataHandler);
} elseif ($this->usesLegacyStorage($file)) {
$incomingFieldArray = null;
$this->logError($table, $logId, sprintf('Attempt to reference file "%d" in legacy storage is disallowed', $file->getUid()), $dataHandler);
} elseif (!$isInternalProcess && $this->usesDisallowedFileMount($file, 'read', $dataHandler->BE_USER)) {
$incomingFieldArray = null;
$this->logError($table, $logId, sprintf('Attempt to reference file "%d" without permission is disallowed', $file->getUid()), $dataHandler);
}
}
} elseif ($table === 'sys_file_metadata') {
$file = $this->resolveReferencedFile($incomingFieldArray, 'file');
if ($file !== null && $this->usesLegacyStorage($file)) {
$incomingFieldArray = null;
$this->logError($table, $logId, sprintf('Attempt to alter metadata of file "%d" in legacy storage is disallowed', $file->getUid()), $dataHandler);
} elseif (!$isInternalProcess && $file !== null && $this->usesDisallowedFileMount($file, 'editMeta', $dataHandler->BE_USER)) {
$incomingFieldArray = null;
$this->logError($table, $logId, sprintf('Attempt to alter metadata of file "%d" without permission is disallowed', $file->getUid()), $dataHandler);
}
}
}

protected function logError(string $table, int $id, string $message, DataHandler $dataHandler): void
{
$dataHandler->log(
$table,
$id,
SystemLogDatabaseAction::UPDATE,
0,
SystemLogErrorClassification::USER_ERROR,
$message,
1,
[$table]
);
}

protected function usesLegacyStorage(File $file): bool
{
return $file->getStorage()->getUid() === 0;
}

/**
* @param non-empty-string $fileAction
* @param BackendUserAuthentication|mixed $backendUser
* @return bool
*/
protected function usesDisallowedFileMount(File $file, string $fileAction, mixed $backendUser): bool
{
// strict: disallow, in case it cannot be determined from BE_USER
if (!$backendUser instanceof BackendUserAuthentication) {
return true;
}
foreach ($backendUser->getFileStorages() as $storage) {
if ($storage->getUid() === $file->getStorage()->getUid()) {
return !$storage->checkFileActionPermission($fileAction, $file);
}
}
return false;
}

/**
* @return list<?File>
*/
protected function resolveReferencedFiles(array $data, string $propertyName): array
{
$propertyItems = GeneralUtility::trimExplode(',', (string)($data[$propertyName] ?? ''), true);
return array_map(
function (string $item): ?File {
if (MathUtility::canBeInterpretedAsInteger($item)) {
return $this->resolveFile((int)$item);
}
if (preg_match('/^sys_file_(?P<fileId>\d+)$/', $item, $matches) && (int)$matches['fileId'] > 0) {
return $this->resolveFile((int)$matches['fileId']);
}
return null;
},
$propertyItems
);
}

protected function resolveReferencedFile(array $data, string $propertyName): ?File
{
$propertyValue = $data[$propertyName] ?? null;
if ($propertyValue === null || !MathUtility::canBeInterpretedAsInteger($propertyValue)) {
return null;
}
return $this->resolveFile((int)$propertyValue);
}

protected function resolveFile(int $fileId): ?File
{
try {
return $this->resourceFactory->getFileObject($fileId);
} catch (\Throwable $t) {
return null;
}
}

protected function isValidStorageData(array $data): bool
{
$storage = $data['storage'] ?? '';
if (!MathUtility::canBeInterpretedAsInteger($storage)) {
return false;
}
return (int)$storage > 0;
}
}
Expand Up @@ -7,10 +7,12 @@
"sys_file",,,,,,,,,,,,,,,,,,,,
,"uid","pid","type","storage","identifier","extension","mime_type","name","sha1","size","creation_date","modification_date","missing","metadata","identifier_hash","folder_hash","last_indexed",,,
,1,0,2,1,"/_migrated/pics/kasper-skarhoj1_01.jpeg","jpeg","image/jpeg","kasper-skarhoj1_01.jpeg","b841902021bbe23bd71e4a5b5b97626da7734b90",39056,1375080761,1374139442,0,0,"2a4941658e4bd943048a234a5e1f305a1f736b10","f6e391567e01bdb14eac504413794a3bc1300abd",0,,,
,9,0,2,0,"/legacy-storage-file.txt","txt","text/plain","legacy-storage-file.txt","3333333333333333333333333333333333333333",1234,1375080761,1374139442,0,0,"2707d25218a2fa6a0faef9fa85a10694aa07a4c7","f6e391567e01bdb14eac504413794a3bc1300abd",0,,,
,21,0,2,1,"/_migrated/pics/typo3_image5_01.jpg","jpg","image/jpeg","typo3_image5_01.jpg","ce136877a22606a6e44ce9b1f8ed3be70c74e6ee",126872,1375080761,1374139442,0,0,"9df04e41b37d2c29777ee64ced3f612b2422a02e","f6e391567e01bdb14eac504413794a3bc1300abd",0,,,
"sys_file_metadata",,,,,,,,,,,,,,,,,,,,
,"uid","pid","sys_language_uid","l10n_parent","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","file","title","width","height","description","alternative","categories","l10n_diffsource",,,
,1,0,0,0,0,0,0,0,0,1,"Image Kasper",401,600,,,0,,,,
,9,0,0,0,0,0,0,0,0,9,"Legacy Storage File",0,0,,,0,,,,
,21,0,0,0,0,0,0,0,0,21,"Image T3BOARD",1024,683,,,0,,,,
"sys_file_reference",,,,,,,,,,,,,,,,,,,,
,"uid","pid","deleted","sys_language_uid","l10n_parent","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","uid_local","uid_foreign","tablenames","fieldname","sorting_foreign","title","description","alternative","link","l10n_diffsource"
Expand All @@ -26,9 +28,11 @@
,"hash","tablename","recuid","field","flexpointer","softref_key","softref_id","sorting","workspace","ref_table","ref_uid","ref_string",,,,,,,,
,"39433ea4a82060704109046e4828d3c8","sys_file",1,"storage",,,,0,0,"sys_file_storage",1,,,,,,,,,
,"fe80a6589cac9798aa13ab5e0192cb56","sys_file",1,"metadata",,,,0,0,"sys_file_metadata",1,,,,,,,,,
,"a87344067faefa9fb880a404a4790d94","sys_file",9,"metadata",,,,0,0,"sys_file_metadata",9,,,,,,,,,
,"45491331fdb3cba18904110be4b946ea","sys_file",21,"storage",,,,0,0,"sys_file_storage",1,,,,,,,,,
,"f68c0805e7b937b9dd993024a7e3e74f","sys_file",21,"metadata",,,,0,0,"sys_file_metadata",21,,,,,,,,,
,"bb9038a252bcfeadc2e1e8a6b5266986","sys_file_metadata",1,"file",,,,0,0,"sys_file",1,,,,,,,,,
,"7522fc0217031429d17920232b22c08b","sys_file_metadata",9,"file",,,,0,0,"sys_file",9,,,,,,,,,
,"c78c9588e7aadd6bcfc994551fe0540c","sys_file_metadata",21,"file",,,,0,0,"sys_file",21,,,,,,,,,
,"3c5c7becb1384c7157ffe4cf218cb70e","sys_file_reference",126,"uid_local",,,,0,0,"sys_file",1,,,,,,,,,
,"6d8283ea74e4379720297750955d2352","sys_file_reference",127,"uid_local",,,,0,0,"sys_file",21,,,,,,,,,
Expand Down
Expand Up @@ -307,4 +307,62 @@ public function createContentWithFileReferenceAndDeleteFileReference(): void
$this->assertCSVDataSet(__DIR__ . '/DataSet/createContentWFileReferenceNDeleteFileReference.csv');
// No FE test: Create and delete scenarios have FE coverage, this test is only about DB state.
}

/**
* @test
*/
public function creatingFileIsDenied(): void
{
$this->expectedErrorLogEntries = 1;
$this->actionService->createNewRecord('sys_file', 0, [
'storage' => 1,
'name' => 'any.file',
'extension' => 'file',
'identifer' => '/any.file',
'mime_type' => 'text/plain',
'sha1' => 'this-is-not-a-hash-value',
]);
$this->assertCSVDataSet(__DIR__ . '/DataSet/sysFileUnchanged.csv');
}

/**
* @test
*/
public function modifyingFileIsDenied(): void
{
$this->expectedErrorLogEntries = 1;
$this->actionService->modifyRecord('sys_file', 21, [
'storage' => 1,
'name' => 'any.file',
'extension' => 'file',
'identifer' => '/any.file',
'mime_type' => 'text/plain',
'sha1' => 'this-is-not-a-hash-value',
]);
$this->assertCSVDataSet(__DIR__ . '/DataSet/sysFileUnchanged.csv');
}

/**
* @test
*/
public function usingLegacyStorageFileInFileReferenceIsDenied(): void
{
$this->expectedErrorLogEntries = 1;
$this->actionService->modifyRecord('sys_file_reference', 127, [
'uid_local' => 9,
]);
$this->assertCSVDataSet(__DIR__ . '/DataSet/sysFileUnchanged.csv');
}

/**
* @test
*/
public function usingLegacyStorageFileInFileMetadataIsDenied(): void
{
$this->expectedErrorLogEntries = 1;
$this->actionService->modifyRecord('sys_file_metadata', 21, [
'file' => 9,
]);
$this->assertCSVDataSet(__DIR__ . '/DataSet/sysFileUnchanged.csv');
}
}
Expand Up @@ -7,10 +7,12 @@
"sys_file",,,,,,,,,,,,,,,,,,,
,"uid","pid","type","storage","identifier","extension","mime_type","name","sha1","size","creation_date","modification_date","missing","metadata","identifier_hash","folder_hash","last_indexed",,
,1,0,2,1,"/_migrated/pics/kasper-skarhoj1_01.jpeg","jpeg","image/jpeg","kasper-skarhoj1_01.jpeg","b841902021bbe23bd71e4a5b5b97626da7734b90",39056,1375080761,1374139442,0,0,"2a4941658e4bd943048a234a5e1f305a1f736b10","f6e391567e01bdb14eac504413794a3bc1300abd",0,,
,9,0,2,0,"/legacy-storage-file.txt","txt","text/plain","legacy-storage-file.txt","3333333333333333333333333333333333333333",1234,1375080761,1374139442,0,0,"2707d25218a2fa6a0faef9fa85a10694aa07a4c7","f6e391567e01bdb14eac504413794a3bc1300abd",0,,
,21,0,2,1,"/_migrated/pics/typo3_image5_01.jpg","jpg","image/jpeg","typo3_image5_01.jpg","ce136877a22606a6e44ce9b1f8ed3be70c74e6ee",126872,1375080761,1374139442,0,0,"9df04e41b37d2c29777ee64ced3f612b2422a02e","f6e391567e01bdb14eac504413794a3bc1300abd",0,,
"sys_file_metadata",,,,,,,,,,,,,,,,,,,
,"uid","pid","sys_language_uid","l10n_parent","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","file","title","width","height","description","alternative","categories","l10n_diffsource",,
,1,0,0,0,0,0,0,0,0,1,"Image Kasper",401,600,,,0,,,
,9,0,0,0,0,0,0,0,0,9,"Legacy Storage File",0,0,,,0,,,
,21,0,0,0,0,0,0,0,0,21,"Image T3BOARD",1024,683,,,0,,,
"sys_file_reference",,,,,,,,,,,,,,,,,,,
,"uid","pid","deleted","sys_language_uid","l10n_parent","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","uid_local","uid_foreign","tablenames","fieldname","sorting_foreign","title","description","alternative","link"
Expand All @@ -26,9 +28,11 @@
,"hash","tablename","recuid","field","flexpointer","softref_key","softref_id","sorting","workspace","ref_table","ref_uid","ref_string",,,,,,,
,"39433ea4a82060704109046e4828d3c8","sys_file",1,"storage",,,,0,0,"sys_file_storage",1,,,,,,,,
,"fe80a6589cac9798aa13ab5e0192cb56","sys_file",1,"metadata",,,,0,0,"sys_file_metadata",1,,,,,,,,
,"a87344067faefa9fb880a404a4790d94","sys_file",9,"metadata",,,,0,0,"sys_file_metadata",9,,,,,,,,,
,"45491331fdb3cba18904110be4b946ea","sys_file",21,"storage",,,,0,0,"sys_file_storage",1,,,,,,,,
,"f68c0805e7b937b9dd993024a7e3e74f","sys_file",21,"metadata",,,,0,0,"sys_file_metadata",21,,,,,,,,
,"bb9038a252bcfeadc2e1e8a6b5266986","sys_file_metadata",1,"file",,,,0,0,"sys_file",1,,,,,,,,
,"7522fc0217031429d17920232b22c08b","sys_file_metadata",9,"file",,,,0,0,"sys_file",9,,,,,,,,,
,"c78c9588e7aadd6bcfc994551fe0540c","sys_file_metadata",21,"file",,,,0,0,"sys_file",21,,,,,,,,
,"3c5c7becb1384c7157ffe4cf218cb70e","sys_file_reference",126,"uid_local",,,,0,0,"sys_file",1,,,,,,,,
,"6d8283ea74e4379720297750955d2352","sys_file_reference",127,"uid_local",,,,0,0,"sys_file",21,,,,,,,,
Expand Down
Expand Up @@ -7,10 +7,12 @@
"sys_file",,,,,,,,,,,,,,,,,,,
,"uid","pid","type","storage","identifier","extension","mime_type","name","sha1","size","creation_date","modification_date","missing","metadata","identifier_hash","folder_hash","last_indexed",,
,1,0,2,1,"/_migrated/pics/kasper-skarhoj1_01.jpeg","jpeg","image/jpeg","kasper-skarhoj1_01.jpeg","b841902021bbe23bd71e4a5b5b97626da7734b90",39056,1375080761,1374139442,0,0,"2a4941658e4bd943048a234a5e1f305a1f736b10","f6e391567e01bdb14eac504413794a3bc1300abd",0,,
,9,0,2,0,"/legacy-storage-file.txt","txt","text/plain","legacy-storage-file.txt","3333333333333333333333333333333333333333",1234,1375080761,1374139442,0,0,"2707d25218a2fa6a0faef9fa85a10694aa07a4c7","f6e391567e01bdb14eac504413794a3bc1300abd",0,,
,21,0,2,1,"/_migrated/pics/typo3_image5_01.jpg","jpg","image/jpeg","typo3_image5_01.jpg","ce136877a22606a6e44ce9b1f8ed3be70c74e6ee",126872,1375080761,1374139442,0,0,"9df04e41b37d2c29777ee64ced3f612b2422a02e","f6e391567e01bdb14eac504413794a3bc1300abd",0,,
"sys_file_metadata",,,,,,,,,,,,,,,,,,,
,"uid","pid","sys_language_uid","l10n_parent","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","file","title","width","height","description","alternative","categories","l10n_diffsource",,
,1,0,0,0,0,0,0,0,0,1,"Image Kasper",401,600,,,0,,,
,9,0,0,0,0,0,0,0,0,9,"Legacy Storage File",0,0,,,0,,,
,21,0,0,0,0,0,0,0,0,21,"Image T3BOARD",1024,683,,,0,,,
"sys_file_reference",,,,,,,,,,,,,,,,,,,
,"uid","pid","deleted","sys_language_uid","l10n_parent","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","uid_local","uid_foreign","tablenames","fieldname","sorting_foreign","title","description","alternative","link"
Expand All @@ -29,9 +31,11 @@
,"hash","tablename","recuid","field","flexpointer","softref_key","softref_id","sorting","workspace","ref_table","ref_uid","ref_string",,,,,,,
,"39433ea4a82060704109046e4828d3c8","sys_file",1,"storage",,,,0,0,"sys_file_storage",1,,,,,,,,
,"fe80a6589cac9798aa13ab5e0192cb56","sys_file",1,"metadata",,,,0,0,"sys_file_metadata",1,,,,,,,,
,"a87344067faefa9fb880a404a4790d94","sys_file",9,"metadata",,,,0,0,"sys_file_metadata",9,,,,,,,,,
,"45491331fdb3cba18904110be4b946ea","sys_file",21,"storage",,,,0,0,"sys_file_storage",1,,,,,,,,
,"f68c0805e7b937b9dd993024a7e3e74f","sys_file",21,"metadata",,,,0,0,"sys_file_metadata",21,,,,,,,,
,"bb9038a252bcfeadc2e1e8a6b5266986","sys_file_metadata",1,"file",,,,0,0,"sys_file",1,,,,,,,,
,"7522fc0217031429d17920232b22c08b","sys_file_metadata",9,"file",,,,0,0,"sys_file",9,,,,,,,,,
,"c78c9588e7aadd6bcfc994551fe0540c","sys_file_metadata",21,"file",,,,0,0,"sys_file",21,,,,,,,,
,"3c5c7becb1384c7157ffe4cf218cb70e","sys_file_reference",126,"uid_local",,,,0,0,"sys_file",1,,,,,,,,
,"6d8283ea74e4379720297750955d2352","sys_file_reference",127,"uid_local",,,,0,0,"sys_file",21,,,,,,,,
Expand Down

0 comments on commit 38f0bf9

Please sign in to comment.