TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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 Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Backend\Form\Event\ModifyEditFormUserAccessEvent;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Attribute\AsEventListener;
|
||||
use TYPO3\CMS\Core\DataHandling\DataHandler;
|
||||
use TYPO3\CMS\Core\DataHandling\DataHandlerCheckModifyAccessListHookInterface;
|
||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||
|
||||
/**
|
||||
* Dealing with file metadata data security is an assembly of hooks to
|
||||
* check permissions on files belonging to file metadata records
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
readonly class FileMetadataPermissionsAspect implements DataHandlerCheckModifyAccessListHookInterface
|
||||
{
|
||||
public function __construct(
|
||||
private ResourceFactory $resourceFactory,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* This hook is called before any write operation by DataHandler
|
||||
*
|
||||
* @param string $table
|
||||
* @param int $id
|
||||
* @param array $fileMetadataRecord
|
||||
* @param int|null $otherHookGrantedAccess
|
||||
* @return int|null
|
||||
*/
|
||||
public function checkRecordUpdateAccess($table, $id, $fileMetadataRecord, $otherHookGrantedAccess, DataHandler $dataHandler)
|
||||
{
|
||||
$accessAllowed = $otherHookGrantedAccess;
|
||||
if ($table === 'sys_file_metadata' && $accessAllowed !== 0) {
|
||||
$existingFileMetadataRecord = BackendUtility::getRecord('sys_file_metadata', $id);
|
||||
if ($existingFileMetadataRecord === null || (empty($existingFileMetadataRecord['file']) && !empty($fileMetadataRecord['file']))) {
|
||||
$existingFileMetadataRecord = $fileMetadataRecord;
|
||||
}
|
||||
$accessAllowed = $this->checkFileWriteAccessForFileMetaData($existingFileMetadataRecord) ? 1 : 0;
|
||||
}
|
||||
|
||||
return $accessAllowed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook that determines whether a user has access to modify a table.
|
||||
* We "abuse" it here to actually check if access is allowed to sys_file_metadata.
|
||||
*
|
||||
* @param bool $accessAllowed Whether the user has access to modify a table
|
||||
* @param string $table The name of the table to be modified
|
||||
*/
|
||||
public function checkModifyAccessList(&$accessAllowed, $table, DataHandler $parent): void
|
||||
{
|
||||
if ($table !== 'sys_file_metadata') {
|
||||
return;
|
||||
}
|
||||
foreach (($parent->cmdmap['sys_file_metadata'] ?? []) as $id => $command) {
|
||||
$fileMetadataRecord = (array)BackendUtility::getRecord('sys_file_metadata', (int)$id);
|
||||
$accessAllowed = $this->checkFileWriteAccessForFileMetaData($fileMetadataRecord);
|
||||
if (!$accessAllowed) {
|
||||
// If for any item in the array, access is not allowed, we deny the whole operation
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isset($parent->datamap[$table])) {
|
||||
foreach ($parent->datamap[$table] as $id => $data) {
|
||||
$recordAccessAllowed = false;
|
||||
if (!str_contains((string)$id, 'NEW')) {
|
||||
$fileMetadataRecord = BackendUtility::getRecord('sys_file_metadata', (int)$id);
|
||||
if ($fileMetadataRecord !== null) {
|
||||
if ($parent->isImporting && empty($fileMetadataRecord['file'])) {
|
||||
// When importing the record was added with an empty file relation as first step
|
||||
$recordAccessAllowed = true;
|
||||
} else {
|
||||
$recordAccessAllowed = $this->checkFileWriteAccessForFileMetaData($fileMetadataRecord);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For new records record access is allowed
|
||||
$recordAccessAllowed = true;
|
||||
}
|
||||
if (isset($data['file'])) {
|
||||
if ($parent->isImporting && empty($data['file'])) {
|
||||
// When importing the record will be created with an empty file relation as first step
|
||||
$dataAccessAllowed = true;
|
||||
} elseif (empty($data['file'])) {
|
||||
$dataAccessAllowed = false;
|
||||
} else {
|
||||
$dataAccessAllowed = $this->checkFileWriteAccessForFileMetaData($data);
|
||||
}
|
||||
} else {
|
||||
$dataAccessAllowed = true;
|
||||
}
|
||||
if (!$recordAccessAllowed || !$dataAccessAllowed) {
|
||||
// If for any item in the array, access is not allowed, we deny the whole operation
|
||||
$accessAllowed = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deny access to the edit form. This is not mandatory, but better to show this right away that access is denied.
|
||||
*/
|
||||
#[AsEventListener('evaluate-file-meta-data-edit-form-access')]
|
||||
public function isAllowedToShowEditForm(ModifyEditFormUserAccessEvent $event): void
|
||||
{
|
||||
if (!$event->doesUserHaveAccess() || $event->getTableName() !== 'sys_file_metadata' || $event->getCommand() !== 'edit') {
|
||||
return;
|
||||
}
|
||||
$this->checkFileWriteAccessForFileMetaData(
|
||||
(array)BackendUtility::getRecord('sys_file_metadata', (int)($event->getDatabaseRow()['uid'] ?? 0))
|
||||
) ? $event->allowUserAccess() : $event->denyUserAccess();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks write access to the file belonging to a metadata entry
|
||||
*/
|
||||
protected function checkFileWriteAccessForFileMetaData(array $fileMetadataRecord): bool
|
||||
{
|
||||
if (empty($fileMetadataRecord['file'])) {
|
||||
return false;
|
||||
}
|
||||
$file = $fileMetadataRecord['file'];
|
||||
if (str_contains($file, 'sys_file_')) {
|
||||
// The file relation could be written as sys_file_[uid], strip this off before checking access rights
|
||||
$file = substr($file, strlen('sys_file_'));
|
||||
}
|
||||
$fileObject = $this->resourceFactory->getFileObject((int)$file);
|
||||
return $fileObject->checkActionPermission('editMeta');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Ensures that any filename that an editor chooses for naming (or uses for uploading a file) is valid, meaning
|
||||
* that no invalid characters (null-bytes) are added, or that the file does not contain an invalid file extension.
|
||||
*/
|
||||
readonly class FileNameValidator
|
||||
{
|
||||
public const DEFAULT_FILE_DENY_PATTERN = '\\.(php[3-8]?|phpsh|phtml|pht|phar|shtml|cgi)(\\..*)?$|\\.pl$|^\\.htaccess$';
|
||||
|
||||
/**
|
||||
* Verifies the input filename against the 'fileDenyPattern'
|
||||
*
|
||||
* Filenames are not allowed to contain control characters. Therefore we
|
||||
* always filter on [[:cntrl:]].
|
||||
*
|
||||
* @param string $fileName File path to evaluate
|
||||
* @return bool Returns TRUE if the file name is OK.
|
||||
*/
|
||||
public function isValid(string $fileName): bool
|
||||
{
|
||||
$pattern = '/[[:cntrl:]]/';
|
||||
if ($fileName !== '' && $this->getCurrentFileDenyPattern() !== '') {
|
||||
$pattern = '/(?:[[:cntrl:]]|' . $this->getCurrentFileDenyPattern() . ')/iu';
|
||||
}
|
||||
return preg_match($pattern, $fileName) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find out if there is a custom file deny pattern configured.
|
||||
*/
|
||||
public function customFileDenyPatternConfigured(): bool
|
||||
{
|
||||
return $this->getCurrentFileDenyPattern() !== self::DEFAULT_FILE_DENY_PATTERN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given file deny pattern does not have parts that the default pattern should
|
||||
* recommend. Used in status overview.
|
||||
*/
|
||||
public function missingImportantPatterns(): bool
|
||||
{
|
||||
$defaultParts = explode('|', self::DEFAULT_FILE_DENY_PATTERN);
|
||||
$givenParts = explode('|', $this->getCurrentFileDenyPattern());
|
||||
$missingParts = array_diff($defaultParts, $givenParts);
|
||||
return !empty($missingParts);
|
||||
}
|
||||
|
||||
protected function getCurrentFileDenyPattern(): string
|
||||
{
|
||||
if (isset($GLOBALS['TYPO3_CONF_VARS']['BE']['fileDenyPattern'])) {
|
||||
return (string)$GLOBALS['TYPO3_CONF_VARS']['BE']['fileDenyPattern'];
|
||||
}
|
||||
return static::DEFAULT_FILE_DENY_PATTERN;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
<?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 Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
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
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
readonly class FilePermissionAspect implements DataHandlerCheckModifyAccessListHookInterface
|
||||
{
|
||||
public function __construct(
|
||||
private ResourceFactory $resourceFactory,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Denies write access to `sys_file` in general, unless it is an internal process.
|
||||
*
|
||||
* @param bool &$accessAllowed
|
||||
* @param string $table
|
||||
*/
|
||||
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 DataHandler $dataHandler
|
||||
*/
|
||||
public function processDatamap_preProcessFieldArray(&$incomingFieldArray, string $table, int|string $id, DataHandler $dataHandler): void
|
||||
{
|
||||
if (!is_array($incomingFieldArray)) {
|
||||
$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,
|
||||
null,
|
||||
SystemLogErrorClassification::USER_ERROR,
|
||||
$message,
|
||||
null,
|
||||
[$table]
|
||||
);
|
||||
}
|
||||
|
||||
protected function usesLegacyStorage(File $file): bool
|
||||
{
|
||||
return $file->getStorage()->getUid() === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $fileAction
|
||||
* @param BackendUserAuthentication|mixed $backendUser
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?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 Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\Attribute\AsEventListener;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Http\ApplicationType;
|
||||
use TYPO3\CMS\Core\Resource\Event\AfterResourceStorageInitializationEvent;
|
||||
use TYPO3\CMS\Core\Resource\Exception\FolderDoesNotExistException;
|
||||
use TYPO3\CMS\Core\Resource\ResourceStorage;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* The aspect injects user permissions and mount points into the storage
|
||||
* based on user or group configuration.
|
||||
*
|
||||
* We do not have AOP in TYPO3, thus the aspect which
|
||||
* deals with resource security is an EventListener which reacts on storage object creation.
|
||||
*
|
||||
* @internal this is an Event Listener, and not part of TYPO3 Core API.
|
||||
*/
|
||||
final class StoragePermissionsAspect
|
||||
{
|
||||
/**
|
||||
* The event listener for the event where storage objects are created
|
||||
*/
|
||||
#[AsEventListener('backend-user-permissions')]
|
||||
public function addUserPermissionsToStorage(AfterResourceStorageInitializationEvent $event): void
|
||||
{
|
||||
$storage = $event->getStorage();
|
||||
if (($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface
|
||||
&& ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isBackend()
|
||||
&& !$this->getBackendUser()->isAdmin()
|
||||
&& !$storage->isFallbackStorage()
|
||||
) {
|
||||
$storage->setEvaluatePermissions(true);
|
||||
$storage->setUserPermissions($this->getFilePermissionsForStorage($storage));
|
||||
$this->addFileMountsToStorage($storage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds file mounts from the user's file mount records
|
||||
*/
|
||||
private function addFileMountsToStorage(ResourceStorage $storage): void
|
||||
{
|
||||
foreach ($this->getBackendUser()->getFileMountRecords() as $fileMountRow) {
|
||||
if (!str_contains($fileMountRow['identifier'] ?? '', ':')) {
|
||||
// Skip record since the file mount identifier is invalid
|
||||
continue;
|
||||
}
|
||||
[$base, $path] = GeneralUtility::trimExplode(':', $fileMountRow['identifier'], false, 2);
|
||||
if ((int)$base === $storage->getUid()) {
|
||||
try {
|
||||
$storage->addFileMount($path, $fileMountRow);
|
||||
} catch (FolderDoesNotExistException $e) {
|
||||
// That file mount does not seem to be valid, fail silently
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the file permissions for a storage
|
||||
* by merging any storage-specific permissions for a
|
||||
* storage with the default settings.
|
||||
* Admin users will always get the default settings.
|
||||
*/
|
||||
private function getFilePermissionsForStorage(ResourceStorage $storageObject): array
|
||||
{
|
||||
$backendUser = $this->getBackendUser();
|
||||
$finalUserPermissions = $backendUser->getFilePermissions();
|
||||
if ($backendUser->isAdmin()) {
|
||||
return $finalUserPermissions;
|
||||
}
|
||||
$storageFilePermissions = $backendUser->getTSConfig()['permissions.']['file.']['storage.'][$storageObject->getUid() . '.'] ?? [];
|
||||
if (!empty($storageFilePermissions)) {
|
||||
array_walk(
|
||||
$storageFilePermissions,
|
||||
static function (string $value, string $permission) use (&$finalUserPermissions): void {
|
||||
$finalUserPermissions[$permission] = (bool)$value;
|
||||
}
|
||||
);
|
||||
}
|
||||
return $finalUserPermissions;
|
||||
}
|
||||
|
||||
private function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?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\Attribute\AsEventListener;
|
||||
use TYPO3\CMS\Core\Resource\Event\AfterFileContentsSetEvent;
|
||||
use TYPO3\CMS\Core\Resource\Event\BeforeFileAddedEvent;
|
||||
use TYPO3\CMS\Core\Resource\Event\BeforeFileReplacedEvent;
|
||||
|
||||
class SvgEventListener
|
||||
{
|
||||
/**
|
||||
* @var SvgSanitizer
|
||||
*/
|
||||
protected $sanitizer;
|
||||
|
||||
/**
|
||||
* @var SvgTypeCheck
|
||||
*/
|
||||
protected $typeCheck;
|
||||
|
||||
public function __construct(SvgSanitizer $sanitizer, SvgTypeCheck $typeCheck)
|
||||
{
|
||||
$this->sanitizer = $sanitizer;
|
||||
$this->typeCheck = $typeCheck;
|
||||
}
|
||||
|
||||
#[AsEventListener('svg-resource-storage-listener-before-file-added')]
|
||||
public function beforeFileAdded(BeforeFileAddedEvent $event): void
|
||||
{
|
||||
$filePath = $event->getSourceFilePath();
|
||||
if ($this->typeCheck->forFilePath($filePath)) {
|
||||
$this->sanitizer->sanitizeFile($filePath);
|
||||
}
|
||||
}
|
||||
|
||||
#[AsEventListener('svg-resource-storage-listener-before-file-replaced')]
|
||||
public function beforeFileReplaced(BeforeFileReplacedEvent $event): void
|
||||
{
|
||||
$filePath = $event->getLocalFilePath();
|
||||
if ($this->typeCheck->forFilePath($filePath)) {
|
||||
$this->sanitizer->sanitizeFile($filePath);
|
||||
}
|
||||
}
|
||||
|
||||
#[AsEventListener('svg-resource-storage-listener-after-file-content-set')]
|
||||
public function afterFileContentsSet(AfterFileContentsSetEvent $event): void
|
||||
{
|
||||
$file = $event->getFile();
|
||||
if (!$this->typeCheck->forResource($file)) {
|
||||
return;
|
||||
}
|
||||
$content = $event->getContent();
|
||||
$sanitizedContent = $this->sanitizer->sanitizeContent($content);
|
||||
// cave: setting content will trigger calling this handler again
|
||||
// (having custom-flags on `FileInterface` would allow to mark it as "processed")
|
||||
if ($sanitizedContent !== $content) {
|
||||
$file->setContents($sanitizedContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?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 Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
|
||||
#[Autoconfigure(public: true)]
|
||||
class SvgHookHandler
|
||||
{
|
||||
/**
|
||||
* @var SvgSanitizer
|
||||
*/
|
||||
protected $sanitizer;
|
||||
|
||||
/**
|
||||
* @var SvgTypeCheck
|
||||
*/
|
||||
protected $typeCheck;
|
||||
|
||||
public function __construct(SvgSanitizer $sanitizer, SvgTypeCheck $typeCheck)
|
||||
{
|
||||
$this->sanitizer = $sanitizer;
|
||||
$this->typeCheck = $typeCheck;
|
||||
}
|
||||
|
||||
public function processMoveUploadedFile(array $parameters)
|
||||
{
|
||||
$filePath = $parameters['source'] ?? null;
|
||||
if ($filePath !== null && $this->typeCheck->forFilePath($filePath)) {
|
||||
$this->sanitizer->sanitizeFile($filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?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 enshrined\svgSanitize\data\AllowedTags;
|
||||
use enshrined\svgSanitize\data\TagInterface;
|
||||
use enshrined\svgSanitize\data\XPath;
|
||||
use enshrined\svgSanitize\ElementReference\Resolver;
|
||||
use enshrined\svgSanitize\Sanitizer;
|
||||
|
||||
readonly class SvgSanitizer
|
||||
{
|
||||
public function sanitizeFile(string $sourcePath, ?string $targetPath = null): void
|
||||
{
|
||||
if ($targetPath === null) {
|
||||
$targetPath = $sourcePath;
|
||||
}
|
||||
$svg = file_get_contents($sourcePath);
|
||||
if (!is_string($svg)) {
|
||||
return;
|
||||
}
|
||||
$sanitizedSvg = $this->sanitizeContent($svg);
|
||||
if ($sanitizedSvg !== $svg) {
|
||||
file_put_contents($targetPath, $sanitizedSvg);
|
||||
}
|
||||
}
|
||||
|
||||
public function sanitizeContent(string $svg, bool $minify = false, bool $removeLinks = false): string
|
||||
{
|
||||
$sanitizer = new Sanitizer();
|
||||
$sanitizer->removeRemoteReferences(true);
|
||||
$sanitizer->minify($minify);
|
||||
if ($removeLinks) {
|
||||
$sanitizer->setAllowedTags(new class implements TagInterface {
|
||||
public static function getTags(): array
|
||||
{
|
||||
return array_values(array_diff(AllowedTags::getTags(), ['a']));
|
||||
}
|
||||
});
|
||||
}
|
||||
return $sanitizer->sanitize($svg) ?: '';
|
||||
}
|
||||
|
||||
public function sanitizeNode(
|
||||
\DOMNode $node,
|
||||
): \DOMNode {
|
||||
$svgSanitizer = new class extends Sanitizer {
|
||||
public function sanitizeDocument(\DOMDocument $document): void
|
||||
{
|
||||
$this->xmlDocument = $document;
|
||||
$this->setUpBefore();
|
||||
// Pre-process all identified elements
|
||||
$xPath = new XPath($this->xmlDocument);
|
||||
$this->elementReferenceResolver = new Resolver($xPath, $this->useNestingLimit);
|
||||
$this->elementReferenceResolver->collect();
|
||||
$elementsToRemove = $this->elementReferenceResolver->getElementsToRemove();
|
||||
// Start the cleaning process
|
||||
$this->startClean($this->xmlDocument->childNodes, $elementsToRemove);
|
||||
$this->resetAfter();
|
||||
}
|
||||
};
|
||||
|
||||
$svg = new \DOMDocument();
|
||||
$svg->appendChild($svg->importNode($node, true));
|
||||
|
||||
$svgSanitizer->removeRemoteReferences(true);
|
||||
$svgSanitizer->sanitizeDocument($svg);
|
||||
return $node->ownerDocument->importNode($svg->documentElement, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?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 Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Core\Resource\FileInterface;
|
||||
use TYPO3\CMS\Core\Resource\MimeTypeDetector;
|
||||
use TYPO3\CMS\Core\Type\File\FileInfo;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
#[Autoconfigure(public: true)]
|
||||
class SvgTypeCheck
|
||||
{
|
||||
protected const MIME_TYPES = ['image/svg', 'image/svg+xml', 'application/svg', 'application/svg+xml'];
|
||||
|
||||
/**
|
||||
* @var MimeTypeDetector
|
||||
*/
|
||||
protected $mimeTypeDetector;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
protected $fileExtensions;
|
||||
|
||||
public function __construct(MimeTypeDetector $mimeTypeDetector)
|
||||
{
|
||||
$this->mimeTypeDetector = $mimeTypeDetector;
|
||||
$this->fileExtensions = $this->resolveFileExtensions();
|
||||
}
|
||||
|
||||
public function forFilePath(string $filePath): bool
|
||||
{
|
||||
$fileInfo = GeneralUtility::makeInstance(FileInfo::class, $filePath);
|
||||
$fileExtension = $fileInfo->getExtension();
|
||||
$mimeType = $fileInfo->getMimeType();
|
||||
return in_array($fileExtension, $this->fileExtensions, true)
|
||||
|| in_array($mimeType, self::MIME_TYPES, true);
|
||||
}
|
||||
|
||||
public function forResource(FileInterface $file): bool
|
||||
{
|
||||
$fileExtension = $file->getExtension();
|
||||
$mimeType = $file->getMimeType();
|
||||
return in_array($fileExtension, $this->fileExtensions, true)
|
||||
|| in_array($mimeType, self::MIME_TYPES, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
protected function resolveFileExtensions(): array
|
||||
{
|
||||
$fileExtensions = array_map(
|
||||
function (string $mimeType): array {
|
||||
return $this->mimeTypeDetector->getFileExtensionsForMimeType($mimeType);
|
||||
},
|
||||
self::MIME_TYPES
|
||||
);
|
||||
$fileExtensions = array_filter($fileExtensions);
|
||||
return count($fileExtensions) > 0 ? array_unique(array_merge(...$fileExtensions)) : [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user