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,196 @@
<?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\Service;
use TYPO3\CMS\Core\Configuration\Features;
use TYPO3\CMS\Core\Crypto\Random;
use TYPO3\CMS\Core\Localization\LabelBag;
use TYPO3\CMS\Core\Resource\FileInterface;
use TYPO3\CMS\Core\Resource\MimeTypeDetector;
use TYPO3\CMS\Core\Resource\ResourceStorage;
use TYPO3\CMS\Core\Type\File\FileInfo;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Validation\ResultException;
use TYPO3\CMS\Core\Validation\ResultMessage;
/**
* This service is invoked by ResourceStorage when modifying files, validating the following:
* + only explicitly allowed file-extensions are allowed:
* see `TYPO3_CONF_VARS` settings for `textfile_ext`, `mediafile_ext` and `miscfile_ext`
* + only files having valid file-extension to mime-type items are allowed:
* e.g. denies using `image.exe` with `image/png`
*
* @phpstan-type ExceptionItem array{storage: ResourceStorage, resource: string|FileInterface, targetFileName: string}
* @phpstan-type ExceptionItemCollection array<string, ExceptionItem>
* @internal
*/
final class ResourceConsistencyService
{
/**
* Exception items, which shall not be validated.
* These are usually set by internal components (e.g. `ext:impexp`).
*
* @var ExceptionItemCollection
*/
private array $exceptionItems = [];
public function __construct(
private readonly Random $random,
private readonly Features $features,
private readonly MimeTypeDetector $mimeTypeDetector,
) {}
public function addExceptionItem(ResourceStorage $storage, string|FileInterface $resource, string $targetFileName): void
{
$identifier = $this->random->generateRandomHexString(40);
$this->exceptionItems[$identifier] = $this->createExceptionItem($storage, $resource, $targetFileName);
}
public function removeException(string $identifier): void
{
unset($this->exceptionItems[$identifier]);
}
/**
* @param FileInterface|string $resource holding the contents
* @param string $targetFileName (optional) target file name to be used as the identifier
* @throws ResultException
*/
public function validate(ResourceStorage $storage, string|FileInterface $resource, string $targetFileName = ''): void
{
if (!$this->shallValidate($storage, $resource, $targetFileName)) {
return;
}
if ($targetFileName !== '') {
$fileExtension = pathinfo($targetFileName, PATHINFO_EXTENSION);
}
if ($resource instanceof FileInterface) {
$mimeType = $resource->getMimeType();
$fileSize = $resource->getSize();
$fileExtension ??= $resource->getExtension();
} else {
$fileInfo = new FileInfo($resource);
$mimeType = (string)$fileInfo->getMimeType($targetFileName);
$fileSize = $fileInfo->isReadable() ? $fileInfo->getSize() : 0;
$fileExtension ??= $fileInfo->getExtension();
}
$isEmptyFile = $fileSize === 0;
$messages = [];
// skip mime-type checks for empty files
if (!$isEmptyFile && !$this->areFileExtensionAndMimeTypeConsistent($fileExtension, $mimeType)) {
$expectedTypes = $this->mimeTypeDetector->getMimeTypesForFileExtension($fileExtension);
if ($expectedTypes === []) {
$listOfExpectedTypes = 'N/A';
} else {
$listOfExpectedTypes = implode(', ', $expectedTypes);
}
$arguments = [$mimeType, $fileExtension, $listOfExpectedTypes];
$messages[] = new ResultMessage(
sprintf('Mime-type "%s" not allowed for file extension "%s" (expected: %s).', ...$arguments),
new LabelBag(
'LLL:EXT:core/Resources/Private/Language/fileMessages.xlf:FileUtility.MimeTypeNotAllowedForFileExtensionWithExpectation',
...$arguments
)
);
}
if (!$this->isFileExtensionAllowed($fileExtension)) {
$arguments = [$fileExtension];
$messages[] = new ResultMessage(
sprintf('File extension "%s" is not in the list of allowed values.', ...$arguments),
new LabelBag(
'LLL:EXT:core/Resources/Private/Language/fileMessages.xlf:FileUtility.FileExtensionIsNotAllowed',
...$arguments
)
);
}
if ($messages !== []) {
throw new ResultException('Resource consistency check failed', 1747230949, ...$messages);
}
}
private function areFileExtensionAndMimeTypeConsistent(string $fileExtension, string $mimeType): bool
{
if (!$this->features->isFeatureEnabled('security.system.enforceFileExtensionMimeTypeConsistency')) {
return true;
}
$fileExtension = mb_strtolower($fileExtension);
$assumedMimesTypeOfFileExtension = $this->mimeTypeDetector->getMimeTypesForFileExtension($fileExtension);
// pass, in case no assumed mime-type was found (e.g., for individual file extension)
return $assumedMimesTypeOfFileExtension === []
|| ($mimeType !== '' && in_array($mimeType, $assumedMimesTypeOfFileExtension, true));
}
private function isFileExtensionAllowed(string $fileExtension): bool
{
if (!$this->features->isFeatureEnabled('security.system.enforceAllowedFileExtensions')) {
return true;
}
$fileExtension = mb_strtolower($fileExtension);
return in_array($fileExtension, $this->getAllowedFileExtensions(), true);
}
private function getAllowedFileExtensions(): array
{
$allowedFileExtensions = GeneralUtility::trimExplode(
',',
$GLOBALS['TYPO3_CONF_VARS']['SYS']['textfile_ext'] . ','
. $GLOBALS['TYPO3_CONF_VARS']['SYS']['mediafile_ext'] . ','
. $GLOBALS['TYPO3_CONF_VARS']['SYS']['miscfile_ext'],
true
);
return array_map(mb_strtolower(...), $allowedFileExtensions);
}
private function shallValidate(ResourceStorage $storage, string|FileInterface $resource, string $targetFileName): bool
{
$needle = $this->createExceptionItem($storage, $resource, $targetFileName);
$exceptionItems = array_filter(
$this->exceptionItems,
fn(array $exception): bool => $this->exceptionItemsMatch($exception, $needle),
);
if ($exceptionItems === []) {
return true;
}
foreach (array_keys($exceptionItems) as $identifier) {
$this->removeException($identifier);
}
return false;
}
/**
* @return ExceptionItem
*/
private function createExceptionItem(ResourceStorage $storage, string|FileInterface $resource, string $targetFileName): array
{
return [
'storage' => $storage,
'resource' => $resource,
'targetFileName' => $targetFileName,
];
}
private function exceptionItemsMatch(array $left, array $right): bool
{
foreach ($right as $key => $value) {
if ($value !== ($left[$key] ?? null)) {
return false;
}
}
return true;
}
}