VYPR
Moderate severityNVD Advisory· Published May 20, 2025· Updated May 20, 2025

TYPO3 CMS Vulnerable to Unrestricted File Upload in File Abstraction Layer

CVE-2025-47939

Description

TYPO3 is an open source, PHP based web content management system. By design, the file management module in TYPO3’s backend user interface has historically allowed the upload of any file type, with the exception of those that are directly executable in a web server context. This lack of restriction means it is possible to upload files that may be considered potentially harmful, such as executable binaries (e.g., .exe files), or files with inconsistent file extensions and MIME types (for example, a file incorrectly named with a .png extension but actually carrying the MIME type application/zip) starting in version 9.0.0 and prior to versions 9.5.51 ELTS, 10.4.50 ELTS, 11.5.44 ELTS, 12.4.31 LTS, and 13.4.12 LTS. Although such files are not directly executable through the web server, their presence can introduce indirect risks. For example, third-party services such as antivirus scanners or malware detection systems might flag or block access to the website for end users if suspicious files are found. This could negatively affect the availability or reputation of the site. Users should update to TYPO3 version 9.5.51 ELTS, 10.4.50 ELTS, 11.5.44 ELTS, 12.4.31 LTS, or 13.4.12 LTS to fix the problem.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

TYPO3 CMS versions 9.0.0 through 13.4.11 allow unrestricted file upload in the backend, enabling potentially harmful files that can cause indirect availability and reputation risks.

Vulnerability

Description

CVE-2025-47939 is a security misconfiguration in the File Abstraction Layer of TYPO3 CMS, affecting versions from 9.0.0 up to 9.5.50, 10.4.49, 11.5.43, 12.4.30, and 13.4.11 [1][2]. The backend file management module has historically allowed the upload of any file type except those directly executable in a web server context. This means files such as .exe binaries or files with mismatched extensions and MIME types (e.g., a file named .png but having application/zip MIME) can be uploaded [2][4].

Exploitation

Prerequisites

An authenticated backend user with file upload permissions can exploit this by uploading files that are not directly executable but are still potentially harmful. The attack vector is network-based, requires low privileges, and no user interaction (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:L) [2]. The vulnerability is present in both the backend interface and any extension or custom integration that uses TYPO3's File Abstraction Layer (FAL) [4].

Impact

While the uploaded files cannot be executed directly on the web server, they introduce indirect risks. For example, third-party services like antivirus scanners or malware detection systems may flag or block access to the website if they detect suspicious files [1][2]. This can negatively affect the site's availability and reputation [2][4].

Mitigation

The issue is fixed in TYPO3 versions 9.5.51 ELTS, 10.4.50 ELTS, 11.5.44 ELTS, 12.4.31 LTS, and 13.4.12 LTS [1][2]. The fix enforces file extension and MIME-type consistency during upload and rename operations, introducing a new ResourceConsistencyService to validate resources [3]. Administrators are advised to review new configuration options such as $GLOBALS['TYPO3_CONF_VARS']['SYS']['miscfile_ext'] to explicitly permit specific file types [4].

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
typo3/cms-corePackagist
>= 9.0.0, < 9.5.519.5.51
typo3/cms-corePackagist
>= 10.0.0, < 10.4.5010.4.50
typo3/cms-corePackagist
>= 11.0.0, < 11.5.4411.5.44
typo3/cms-corePackagist
>= 12.0.0, < 12.4.3112.4.31
typo3/cms-corePackagist
>= 13.0.0, < 13.4.1213.4.12

Affected products

3
  • TYPO3/Typo3llm-fuzzy2 versions
    >=9.0.0, <9.5.51 ELTS, <10.4.50 ELTS, <11.5.44 ELTS, <12.4.31 LTS, <13.4.12 LTS+ 1 more
    • (no CPE)range: >=9.0.0, <9.5.51 ELTS, <10.4.50 ELTS, <11.5.44 ELTS, <12.4.31 LTS, <13.4.12 LTS
    • (no CPE)range: >= 9.0.0, < 9.5.51
  • ghsa-coords
    Range: >= 9.0.0, < 9.5.51

Patches

1
c265beed6e2c

[SECURITY] Enforce file extension and MIME-type consistency

https://github.com/TYPO3-CMS/coreOliver HaderMay 20, 2025via ghsa
15 files changed · +674 3
  • Classes/Localization/LabelBag.php+57 0 added
    @@ -0,0 +1,57 @@
    +<?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\Localization;
    +
    +/**
    + * @internal
    + */
    +final class LabelBag
    +{
    +    /**
    +     * @var list<string>
    +     */
    +    public readonly array $arguments;
    +
    +    /**
    +     * @param string $key e.g. `LLL:EXT:core/Resources/Private/Language/Labels.xlf:HelloWorld`
    +     * @param string ...$arguments optional label arguments to be substituted
    +     */
    +    public function __construct(
    +        public readonly string $key,
    +        string ...$arguments
    +    ) {
    +        $this->arguments = $arguments;
    +    }
    +
    +    /**
    +     * Compiles the given label key and substituted label arguments if given.
    +     */
    +    public function compile(LanguageService $languageService): string
    +    {
    +        $label = $languageService->sL($this->key);
    +        return sprintf(
    +            $label,
    +            ...$this->arguments
    +        ) ?: sprintf(
    +            'Error: could not translate key "%s" with value "%s" and %d argument(s)!',
    +            $this->key,
    +            $label,
    +            count($this->arguments)
    +        );
    +    }
    +}
    
  • Classes/Resource/OnlineMedia/Helpers/AbstractOnlineMediaHelper.php+4 0 modified
    @@ -23,10 +23,13 @@
     use TYPO3\CMS\Core\Resource\Folder;
     use TYPO3\CMS\Core\Resource\Index\FileIndexRepository;
     use TYPO3\CMS\Core\Resource\ResourceFactory;
    +use TYPO3\CMS\Core\Resource\ResourceInstructionTrait;
     use TYPO3\CMS\Core\Utility\GeneralUtility;
     
     abstract class AbstractOnlineMediaHelper implements OnlineMediaHelperInterface
     {
    +    use ResourceInstructionTrait;
    +
         /**
          * Cached OnlineMediaIds [fileUid => id]
          *
    @@ -113,6 +116,7 @@ protected function createNewFile(Folder $targetFolder, $fileName, $onlineMediaId
         {
             $temporaryFile = GeneralUtility::tempnam('online_media');
             GeneralUtility::writeFileToTypo3tempDir($temporaryFile, $onlineMediaId);
    +        $this->skipResourceConsistencyCheckForCommands($targetFolder->getStorage(), $temporaryFile, $fileName);
             $file = $targetFolder->addFile($temporaryFile, $fileName, DuplicationBehavior::RENAME);
             return $file;
         }
    
  • Classes/Resource/ResourceInstructionTrait.php+63 0 added
    @@ -0,0 +1,63 @@
    +<?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;
    +
    +use Psr\Http\Message\UploadedFileInterface;
    +use TYPO3\CMS\Core\Resource\Service\ResourceConsistencyService;
    +use TYPO3\CMS\Core\Utility\GeneralUtility;
    +use TYPO3\CMS\Core\Utility\PathUtility;
    +
    +/**
    + * Trait for creating skip-instructions for `ResourceConsistencyService::validate()`.
    + */
    +trait ResourceInstructionTrait
    +{
    +    /**
    +     * Registers an instruction to skip validation in `ResourceConsistencyService` for a specific uploaded file.
    +     */
    +    private function skipResourceConsistencyCheckForUploads(
    +        ResourceStorage $storage,
    +        array|UploadedFileInterface $uploadedFile,
    +        ?string $targetFileName = null,
    +    ): void {
    +        GeneralUtility::makeInstance(ResourceConsistencyService::class)->addExceptionItem(
    +            $storage,
    +            $storage->getUploadedLocalFilePath($uploadedFile),
    +            $storage->getUploadedTargetFileName($uploadedFile, $targetFileName),
    +        );
    +    }
    +
    +    /**
    +     * Registers an instruction to skip validation in `ResourceConsistencyService`
    +     * for commands (such as rename or replace) for existing files.
    +     */
    +    private function skipResourceConsistencyCheckForCommands(
    +        ResourceStorage $storage,
    +        string|FileInterface $resource,
    +        ?string $targetFileName = null,
    +    ): void {
    +        $targetFileName ??= PathUtility::basename(
    +            $resource instanceof FileInterface ? $resource->getName() : $resource
    +        );
    +        GeneralUtility::makeInstance(ResourceConsistencyService::class)->addExceptionItem(
    +            $storage,
    +            $resource,
    +            $targetFileName
    +        );
    +    }
    +}
    
  • Classes/Resource/ResourceStorage.php+17 0 modified
    @@ -91,11 +91,13 @@
     use TYPO3\CMS\Core\Resource\Search\Result\FileSearchResultInterface;
     use TYPO3\CMS\Core\Resource\Security\FileNameValidator;
     use TYPO3\CMS\Core\Resource\Service\FileProcessingService;
    +use TYPO3\CMS\Core\Resource\Service\ResourceConsistencyService;
     use TYPO3\CMS\Core\Service\FlexFormService;
     use TYPO3\CMS\Core\Utility\Exception\NotImplementedMethodException;
     use TYPO3\CMS\Core\Utility\GeneralUtility;
     use TYPO3\CMS\Core\Utility\PathUtility;
     use TYPO3\CMS\Core\Utility\StringUtility;
    +use TYPO3\CMS\Core\Validation\ResultException;
     
     /**
      * A "mount point" inside the TYPO3 file handling.
    @@ -1070,6 +1072,14 @@ protected function assureFileCopyPermissions(FileInterface $file, FolderInterfac
             }
         }
     
    +    /**
    +     * @throws ResultException
    +     */
    +    protected function assureResourceConsistency(string|FileInterface $resource, string $fileName = ''): void
    +    {
    +        GeneralUtility::makeInstance(ResourceConsistencyService::class)->validate($this, $resource, $fileName);
    +    }
    +
         /**
          * Check if a file has the permission to be copied on a File/Folder/Storage,
          * if not throw an exception.
    @@ -1182,6 +1192,7 @@ public function addFile(string $localFilePath, Folder $targetFolder, string $tar
             )->getFileName();
     
             $this->assureFileAddPermissions($targetFolder, $targetFileName);
    +        $this->assureResourceConsistency($localFilePath, $targetFileName);
     
             $replaceExisting = false;
             if ($conflictMode === DuplicationBehavior::CANCEL && $this->driver->fileExistsInFolder($targetFileName, $targetFolder->getIdentifier())) {
    @@ -1912,6 +1923,8 @@ public function renameFile(FileInterface $file, string $targetFileName, Duplicat
             }
     
             $this->assureFileRenamePermissions($file, $sanitizedTargetFileName);
    +        $this->assureResourceConsistency($file, $sanitizedTargetFileName);
    +
             $this->eventDispatcher->dispatch(
                 new BeforeFileRenamedEvent($file, $sanitizedTargetFileName)
             );
    @@ -1954,6 +1967,8 @@ public function renameFile(FileInterface $file, string $targetFileName, Duplicat
         public function replaceFile(FileInterface $file, string $localFilePath): FileInterface
         {
             $this->assureFileReplacePermissions($file);
    +        $this->assureResourceConsistency($localFilePath, $file->getName());
    +
             if (!file_exists($localFilePath)) {
                 throw new \InvalidArgumentException('File "' . $localFilePath . '" does not exist.', 1325842622);
             }
    @@ -1989,6 +2004,8 @@ public function addUploadedFile(array|UploadedFileInterface $uploadedFileData, ?
             $targetFolder ??= $this->getDefaultFolder();
     
             $this->assureFileUploadPermissions($localFilePath, $targetFolder, $targetFileName, $size);
    +        $this->assureResourceConsistency($localFilePath, $targetFileName);
    +
             if ($this->hasFileInFolder($targetFileName, $targetFolder) && $conflictMode === DuplicationBehavior::REPLACE) {
                 $file = $this->getFileInFolder($targetFileName, $targetFolder);
                 $resultObject = $this->replaceFile($file, $localFilePath);
    
  • Classes/Resource/Service/ResourceConsistencyService.php+205 0 added
    @@ -0,0 +1,205 @@
    +<?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();
    +            $fileSize = $fileInfo->isReadable() ? $fileInfo->getSize() : 0;
    +            $fileExtension ??= $fileInfo->getExtension();
    +        }
    +        $mimeType = $this->considerFileExtensionToMimeTypeMap($fileExtension) ?? $mimeType;
    +        $isEmptyFile = $fileSize === 0;
    +        $messages = [];
    +        // skip mime-type checks for empty files
    +        if (!$isEmptyFile && !$this->areFileExtensionAndMimeTypeConsistent($fileExtension, $mimeType)) {
    +            $arguments = [$mimeType, $fileExtension];
    +            $messages[] = new ResultMessage(
    +                sprintf('Mime-type "%s" not allowed for file extension "%s"', ...$arguments),
    +                new LabelBag(
    +                    'LLL:EXT:core/Resources/Private/Language/fileMessages.xlf:FileUtility.MimeTypeNotAllowedForFileExtension',
    +                    ...$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;
    +        }
    +        $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;
    +        }
    +        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(strtolower(...), $allowedFileExtensions);
    +    }
    +
    +    private function considerFileExtensionToMimeTypeMap(string $fileExtension): ?string
    +    {
    +        $fileExtensionMimeTypeMap = $GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType'] ?? null;
    +        if (!is_array($fileExtensionMimeTypeMap)) {
    +            return null;
    +        }
    +        $fileExtensionMimeTypeMap = array_filter(
    +            $fileExtensionMimeTypeMap,
    +            static fn(string $mimeType): bool => $mimeType !== ''
    +        );
    +        if (!is_string($fileExtensionMimeTypeMap[$fileExtension] ?? null)) {
    +            return null;
    +        }
    +        return $fileExtensionMimeTypeMap[$fileExtension];
    +    }
    +
    +    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;
    +    }
    +}
    
  • Classes/Utility/File/ExtendedFileUtility.php+40 3 modified
    @@ -57,6 +57,7 @@
     use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
     use TYPO3\CMS\Core\Utility\Exception\NotImplementedMethodException;
     use TYPO3\CMS\Core\Utility\GeneralUtility;
    +use TYPO3\CMS\Core\Validation\ResultException;
     
     /**
      * Contains functions for performing file operations like copying, pasting, uploading, moving,
    @@ -291,9 +292,7 @@ protected function writeLog(int $action, int $severity, string $message, array $
          */
         protected function addMessageToFlashMessageQueue($localizationKey, array $replaceMarkers = [], ContextualFeedbackSeverity $severity = ContextualFeedbackSeverity::ERROR)
         {
    -        if (($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface
    -            && ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isBackend()
    -        ) {
    +        if ($this->isBackendScope()) {
                 $label = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/fileMessages.xlf:' . $localizationKey);
                 $message = vsprintf($label, $replaceMarkers);
                 $flashMessage = GeneralUtility::makeInstance(
    @@ -307,6 +306,26 @@ protected function addMessageToFlashMessageQueue($localizationKey, array $replac
             }
         }
     
    +    protected function addEvaluationResultHintsToFlashMessageQueue(
    +        ResultException $exception,
    +        ContextualFeedbackSeverity $severity = ContextualFeedbackSeverity::ERROR,
    +    ): void {
    +        if (!$this->isBackendScope()) {
    +            return;
    +        }
    +        foreach ($exception->messages as $messageItem) {
    +            $message = $messageItem->labelBag?->compile($this->getLanguageService()) ?? $messageItem->message;
    +            $flashMessage = GeneralUtility::makeInstance(
    +                FlashMessage::class,
    +                $message,
    +                '',
    +                $severity,
    +                true
    +            );
    +            $this->addFlashMessage($flashMessage);
    +        }
    +    }
    +
         /*************************************
          *
          * File operation functions
    @@ -803,6 +822,9 @@ public function func_rename($cmds)
                 } catch (NotInMountPointException $e) {
                     $this->writeLog(SystemLogFileAction::RENAME, SystemLogErrorClassification::USER_ERROR, 'Destination path "{destination}" was not within your mountpoints', ['destination' => $targetFile]);
                     $this->addMessageToFlashMessageQueue('FileUtility.DestinationPathWasNotWithinYourMountpoints', [$targetFile]);
    +            } catch (ResultException $e) {
    +                $this->writeLog(SystemLogFileAction::RENAME, SystemLogErrorClassification::USER_ERROR, 'File {identifier} was not renamed to {destination}', ['identifier' => $sourceFileObject->getName(), 'destination' => $targetFile]);
    +                $this->addEvaluationResultHintsToFlashMessageQueue($e);
                 } catch (\RuntimeException $e) {
                     $this->writeLog(SystemLogFileAction::RENAME, SystemLogErrorClassification::USER_ERROR, 'File "{identifier}" was not renamed. Write-permission problem in "{destination}"?', ['identifier' => $sourceFileObject->getName(), 'destination' => $targetFile]);
                     $this->addMessageToFlashMessageQueue('FileUtility.FileWasNotRenamed', [$sourceFileObject->getName(), $targetFile]);
    @@ -832,6 +854,9 @@ public function func_rename($cmds)
                 } catch (NotInMountPointException $e) {
                     $this->writeLog(SystemLogFileAction::RENAME, SystemLogErrorClassification::USER_ERROR, 'Destination path "{destination}" was not within your mountpoints', ['destination' => $targetFile]);
                     $this->addMessageToFlashMessageQueue('FileUtility.DestinationPathWasNotWithinYourMountpoints', [$targetFile]);
    +            } catch (ResultException $e) {
    +                $this->writeLog(SystemLogFileAction::RENAME, SystemLogErrorClassification::USER_ERROR, 'File {identifier} was not renamed to {destination}', ['identifier' => $sourceFileObject->getName(), 'destination' => $targetFile]);
    +                $this->addEvaluationResultHintsToFlashMessageQueue($e);
                 } catch (\RuntimeException $e) {
                     $this->writeLog(SystemLogFileAction::RENAME, SystemLogErrorClassification::USER_ERROR, 'Directory "{identifier}" was not renamed. Write-permission problem in "{destination}"?', ['identifier' => $sourceFileObject->getName(), 'destination' => $targetFile]);
                     $this->addMessageToFlashMessageQueue('FileUtility.DirectoryWasNotRenamed', [$sourceFileObject->getName(), $targetFile]);
    @@ -1070,6 +1095,9 @@ public function func_upload($cmds)
                 } catch (ExistingTargetFileNameException $e) {
                     $this->writeLog(SystemLogFileAction::UPLOAD, SystemLogErrorClassification::USER_ERROR, 'No unique filename available in "{destination}"', ['destination' => $targetFolderObject->getIdentifier()]);
                     $this->addMessageToFlashMessageQueue('FileUtility.NoUniqueFilenameAvailableIn', [$targetFolderObject->getIdentifier()]);
    +            } catch (ResultException $e) {
    +                $this->writeLog(SystemLogFileAction::UPLOAD, SystemLogErrorClassification::USER_ERROR, 'Uploading file "{identifier}" to "{destination}" failed', ['identifier' => $fileInfo['name'], 'destination' => $targetFolderObject->getIdentifier()]);
    +                $this->addEvaluationResultHintsToFlashMessageQueue($e);
                 } catch (\RuntimeException $e) {
                     $this->writeLog(SystemLogFileAction::UPLOAD, SystemLogErrorClassification::USER_ERROR, 'Uploaded file could not be moved. Write-permission problem in "{destination}"? Error: {error}', ['destination' => $targetFolderObject->getIdentifier(), 'error' => $e->getMessage()]);
                     $this->addMessageToFlashMessageQueue('FileUtility.UploadedFileCouldNotBeMoved', [$targetFolderObject->getIdentifier()]);
    @@ -1144,6 +1172,9 @@ protected function replaceFile(array $cmdArr)
             } catch (ExistingTargetFileNameException $e) {
                 $this->writeLog(SystemLogFileAction::UPLOAD, SystemLogErrorClassification::USER_ERROR, 'No unique filename available in "{destination}"', ['destination' => $fileObjectToReplace->getIdentifier()]);
                 $this->addMessageToFlashMessageQueue('FileUtility.NoUniqueFilenameAvailableIn', [$fileObjectToReplace->getIdentifier()]);
    +        } catch (ResultException $e) {
    +            $this->writeLog(SystemLogFileAction::UPLOAD, SystemLogErrorClassification::USER_ERROR, 'Replacing file "{identifier}" to "{destination}" failed', ['identifier' => $fileInfo['name'], 'destination' => $fileObjectToReplace->getIdentifier()]);
    +            $this->addEvaluationResultHintsToFlashMessageQueue($e);
             } catch (\RuntimeException $e) {
                 throw $e;
             }
    @@ -1161,6 +1192,12 @@ protected function addFlashMessage(FlashMessage $flashMessage)
             $defaultFlashMessageQueue->enqueue($flashMessage);
         }
     
    +    protected function isBackendScope(): bool
    +    {
    +        return ($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface
    +            && ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isBackend();
    +    }
    +
         /**
          * Gets Indexer
          *
    
  • Classes/Validation/ResultException.php+37 0 added
    @@ -0,0 +1,37 @@
    +<?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\Validation;
    +
    +use TYPO3\CMS\Core\Exception;
    +
    +/**
    + * @internal
    + */
    +final class ResultException extends Exception
    +{
    +    /**
    +     * @var list<ResultMessage>
    +     */
    +    public readonly array $messages;
    +
    +    public function __construct(string $message = '', int $code = 0, ResultMessage ...$messages)
    +    {
    +        parent::__construct($message, $code);
    +        $this->messages = $messages;
    +    }
    +}
    
  • Classes/Validation/ResultMessage.php+31 0 added
    @@ -0,0 +1,31 @@
    +<?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\Validation;
    +
    +use TYPO3\CMS\Core\Localization\LabelBag;
    +
    +/**
    + * @internal
    + */
    +final class ResultMessage
    +{
    +    public function __construct(
    +        public readonly string $message,
    +        public readonly ?LabelBag $labelBag = null,
    +    ) {}
    +}
    
  • Configuration/DefaultConfigurationDescription.yaml+9 0 modified
    @@ -101,6 +101,9 @@ SYS:
             mediafile_ext:
                 type: text
                 description: 'Commalist of file extensions perceived as media files by TYPO3. Lowercase and no spaces between!'
    +        miscfile_ext:
    +          type: list
    +          description: "Commalist of file extensions that don't logically fit into `textfile_ext` or `mediafile_ext` (like `zip` or `xz`). Lowercase and no spaces between!"
             binPath:
                 type: text
                 description: 'List of absolute paths where external programs should be searched for. Eg. <code>/usr/local/webbin/,/home/xyz/bin/</code>. (ImageMagick path have to be configured separately)'
    @@ -212,6 +215,12 @@ SYS:
                   security.frontend.allowInsecureSiteResolutionByQueryParameters:
                     type: bool
                     description: 'If on, site resolution can be overwritten by `&id=...&L=...` parameters, URI path & host are just used as default.'
    +              security.system.enforceAllowedFileExtensions:
    +                type: bool
    +                description: 'If on, only file extensions configured in `textfile_ext`, `mediafile_ext` and `miscfile_ext` are allowed to be processed in the File Abstraction Layer.'
    +              security.system.enforceFileExtensionMimeTypeConsistency:
    +                type: bool
    +                description: 'If on, only file extensions that are consistent with their expected mime-type are allowed to be processed in the File Abstraction Layer.'
             availablePasswordHashAlgorithms:
                 type: array
                 description: 'A list of available password hash mechanisms. Extensions may register additional mechanisms here. This is usually not extended in system/settings.php.'
    
  • Configuration/DefaultConfiguration.php+6 0 modified
    @@ -91,6 +91,11 @@
                 'security.frontend.reportContentSecurityPolicy' => false,
                 'security.frontend.allowInsecureSiteResolutionByQueryParameters' => false,
                 'security.frontend.allowInsecureFrameOptionInShowImageController' => false,
    +            // only file extensions configured in 'textfile_ext', 'mediafile_ext', 'miscfile_ext' are accepted
    +            'security.system.enforceAllowedFileExtensions' => false,
    +            // only files having file-extension to mime-type matches are allowed
    +            // (adjustable by `$GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType']`)
    +            'security.system.enforceFileExtensionMimeTypeConsistency' => true,
             ],
             'createGroup' => '',
             'sitename' => 'TYPO3',
    @@ -103,6 +108,7 @@
             'loginCopyrightWarrantyURL' => '',
             'textfile_ext' => 'txt,ts,typoscript,html,htm,css,tmpl,js,sql,xml,csv,xlf,yaml,yml',
             'mediafile_ext' => 'gif,jpg,jpeg,bmp,png,webp,pdf,svg,ai,mp3,wav,mp4,ogg,flac,opus,webm,youtube,vimeo',
    +        'miscfile_ext' => 'zip',
             'binPath' => '',
             'binSetup' => '',
             'setMemoryLimit' => 0,
    
  • Configuration/FactoryConfiguration.php+2 0 modified
    @@ -26,6 +26,8 @@
             'UTF8filesystem' => true,
             'features' => [
                 'frontend.cache.autoTagging' => true,
    +            // only file extensions configured in 'textfile_ext', 'mediafile_ext', 'miscfile_ext' are accepted
    +            'security.system.enforceAllowedFileExtensions' => true,
             ],
         ],
     ];
    
  • Configuration/Services.yaml+5 0 modified
    @@ -111,6 +111,11 @@ services:
           - name: softreference.parser
             parserKey: url
     
    +  # @todo use Autoconfigure attribute in v13/v14
    +  TYPO3\CMS\Core\Resource\Service\ResourceConsistencyService:
    +    public: true
    +    shared: true
    +
       # Core caches, cache.core, cache.assets and cache.runtime are injected as
       # early entries in TYPO3\CMS\Core\Core\Bootstrap and therefore omitted here
       cache.hash:
    
  • Documentation/Changelog/12.4.x/Important-106240-EnforceFile-extensionsAndMime-typeConsistencyInFileAbstractionLayer.rst+79 0 added
    @@ -0,0 +1,79 @@
    +..  include:: /Includes.rst.txt
    +
    +..  _important-106240-1747316969:
    +
    +===============================================================================================
    +Important: #106240 - Enforce File Extension and MIME-Type Consistency in File Abstraction Layer
    +===============================================================================================
    +
    +See :issue:`106240`
    +
    +Description
    +===========
    +
    +The following methods of :php:`ResourceStorage` have been improved to enhance
    +consistency and security for both existing and uploaded files:
    +
    +* :php:`addFile`
    +* :php:`renameFile`
    +* :php:`replaceFile`
    +* :php:`addUploadedFile`
    +
    +Key enhancements
    +----------------
    +
    +* Only explicitly allowed file extensions are accepted. These must be configured
    +  under the following sub-properties in :php:`$GLOBALS['TYPO3_CONF_VARS']['SYS']`:
    +  :php:`textfile_ext`, :php:`mediafile_ext`, or :php:`miscfile_ext`.
    +* Files are only accepted if their MIME type matches the expected file extension.
    +  The MIME type is determined based on the actual file content. For example,
    +  uploading a real PNG image with the filename `image.exe` will be rejected,
    +  because `image/png` is not a valid MIME type for the `exe` extension.
    +
    +New Configuration Property in `$GLOBALS['TYPO3_CONF_VARS']['SYS']`
    +------------------------------------------------------------------
    +
    +A new configuration property, :php:`miscfile_ext`, has been introduced. It
    +allows specifying file extensions that don't belong to either `textfile_ext`
    +or `mediafile_ext`, such as `zip` or `xz`.
    +
    +New Feature Flags
    +-----------------
    +
    +* :php:`security.system.enforceAllowedFileExtensions`:
    +  Controls whether only the configured file extensions are permitted.
    +  - **Disabled by default** in existing installations.
    +  - **Enabled by default** in new installations.
    +* :php:`security.system.enforceFileExtensionMimeTypeConsistency`:
    +  Controls whether the MIME type and file extension consistency check
    +  is enforced.
    +
    +Exemptions
    +----------
    +
    +Some use cases—such as importing files through internal low-level system
    +components—may require temporary exemptions from the above restrictions.
    +
    +The following example shows how to define a one-time exemption for a known
    +and controlled operation:
    +
    +..  code-block:: php
    +
    +    <?php
    +    class ImportCommand
    +    {
    +        use \TYPO3\CMS\Core\Resource\ResourceInstructionTrait;
    +
    +        protected function execute(): void
    +        {
    +            // ...
    +
    +            // Skip the consistency check once for the specified storage, source, and target
    +            $this->skipResourceConsistencyCheckForCommands($storage, $temporaryFileName, $targetFileName);
    +
    +            /** @var \TYPO3\CMS\Core\Resource\File $file */
    +            $file = $storage->addFile($temporaryFileName, $targetFolder, $targetFileName);
    +        }
    +    }
    +
    +..  index:: FAL, LocalConfiguration, ext:core
    
  • Resources/Private/Language/fileMessages.xlf+6 0 modified
    @@ -228,6 +228,12 @@
     			<trans-unit id="FileUtility.YouDontHaveFullAccessToTheDestinationDirectory">
     				<source>You don't have full access to the destination directory "%s"!</source>
     			</trans-unit>
    +			<trans-unit id="FileUtility.MimeTypeNotAllowedForFileExtension">
    +				<source>Mime-type "%s" not allowed for file extension "%s".</source>
    +			</trans-unit>
    +			<trans-unit id="FileUtility.FileExtensionIsNotAllowed">
    +				<source>File extension "%s" is not in the list of allowed values.</source>
    +			</trans-unit>
     		</body>
     	</file>
     </xliff>
    
  • Tests/Functional/Resource/Service/ResourceConsistencyServiceTest.php+113 0 added
    @@ -0,0 +1,113 @@
    +<?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\Tests\Functional\Resource\Service;
    +
    +use PHPUnit\Framework\Attributes\Test;
    +use PHPUnit\Framework\MockObject\MockObject;
    +use TYPO3\CMS\Core\Resource\ResourceStorage;
    +use TYPO3\CMS\Core\Resource\Service\ResourceConsistencyService;
    +use TYPO3\CMS\Core\Validation\ResultException;
    +use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
    +
    +final class ResourceConsistencyServiceTest extends FunctionalTestCase
    +{
    +    protected bool $initializeDatabase = false;
    +
    +    protected array $pathsToProvideInTestInstance = [
    +        'typo3/sysext/core/Tests/Functional/Resource/Fixtures/ProcessedFileTest.jpg' => 'fileadmin/ProcessedFileTest.jpg',
    +    ];
    +
    +    private ResourceConsistencyService $subject;
    +    private \ReflectionMethod $subjectShallValidate;
    +    private array $items;
    +    private array $storages;
    +
    +    protected function setUp(): void
    +    {
    +        parent::setUp();
    +
    +        $this->subject = $this->get(ResourceConsistencyService::class);
    +        $this->subjectShallValidate = (new \ReflectionObject($this->subject))->getMethod('shallValidate');
    +        $this->subjectShallValidate->setAccessible(true);
    +
    +        $this->storages = [
    +            1 => $this->createMockedStorage(1),
    +            2 => $this->createMockedStorage(2),
    +        ];
    +        $this->items = [
    +            [
    +                'storage' => $this->storages[1],
    +                'resource' => '/image.png',
    +                'targetFileName' => 'target.png',
    +            ],
    +            [
    +                'storage' => $this->storages[2],
    +                'resource' => '/image.png',
    +                'targetFileName' => 'target.png',
    +            ],
    +        ];
    +    }
    +
    +    #[Test]
    +    public function validationSucceeds(): void
    +    {
    +        $this->expectNotToPerformAssertions();
    +
    +        $this->subject->validate(
    +            $this->storages[1],
    +            $this->instancePath . '/fileadmin/ProcessedFileTest.jpg',
    +            'ProcessedFileTest.jpg'
    +        );
    +    }
    +
    +    #[Test]
    +    public function validationFails(): void
    +    {
    +        $this->expectException(ResultException::class);
    +        $this->expectExceptionCode(1747230949);
    +        $this->expectExceptionMessage('Resource consistency check failed');
    +
    +        $this->subject->validate(
    +            $this->storages[1],
    +            $this->instancePath . '/fileadmin/ProcessedFileTest.jpg',
    +            'ProcessedFileTest.exe'
    +        );
    +    }
    +
    +    #[Test]
    +    public function shallValidateConsidersExceptionItems(): void
    +    {
    +        $this->subject->addExceptionItem(...$this->items[0]);
    +        $this->subject->addExceptionItem(...$this->items[1]);
    +        self::assertFalse($this->subjectShallValidate->invokeArgs($this->subject, array_values($this->items[0])));
    +        self::assertFalse($this->subjectShallValidate->invokeArgs($this->subject, array_values($this->items[1])));
    +        // this must be true now, since those items have been "consumed" in the previous invocations
    +        self::assertTrue($this->subjectShallValidate->invokeArgs($this->subject, array_values($this->items[0])));
    +        self::assertTrue($this->subjectShallValidate->invokeArgs($this->subject, array_values($this->items[1])));
    +    }
    +
    +    private function createMockedStorage(int $uid): ResourceStorage&MockObject
    +    {
    +        $mock = $this->getMockBuilder(ResourceStorage::class)
    +            ->onlyMethods(['getUid'])
    +            ->disableOriginalConstructor()
    +            ->getMock();
    +        $mock->method('getUid')->willReturn($uid);
    +        return $mock;
    +    }
    +}
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.