TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:09 +02:00
commit af8cc155b5
6818 changed files with 642608 additions and 0 deletions
@@ -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)) : [];
}
}