Skip to content

Commit 88757c3

Browse files
committed
[SECURITY] Enforce file extension and MIME-type consistency
The File Abstraction Layer (FAL) methods `addFile`, `renameFile`, `replaceFile`, and `addUploadedFile` in `ResourceStorage` have been enhanced to enforce consistency and security of file data. Two validations are now applied: * Only explicitly allowed file extensions are accepted. These must be configured in the TYPO3 global config under `$GLOBALS['TYPO3_CONF_VARS']['SYS']`: - `textfile_ext` - `mediafile_ext` - `miscfile_ext` * A file's MIME-type must match the expected type for its extension. For example, uploading a PNG image as `image.exe` is disallowed. The new configuration property `miscfile_ext` enables defining extensions that don't logically fit into text or media groups (e.g. `zip`, `xz`). Any extensions not configured are disallowed by default. New feature flags: * `security.system.enforceAllowedFileExtensions` - Enforces the file extension allowlist. - Disabled by default in existing installations. - Enabled by default in new installations. * `security.system.enforceFileExtensionMimeTypeConsistency` - Enforces consistency between file extension and MIME type. For internal use cases, such as file imports via trusted low-level system components, one-time exemptions can be declared using a dedicated trait method. Resolves: #106240 Releases: main, 13.4, 12.4 Change-Id: Ibfc5b97f65c817d1e2f281f619869a52bfbfef8d Security-Bulletin: TYPO3-CORE-SA-2025-014 Security-References: CVE-2025-47939 Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/89468 Tested-by: Oliver Hader <oliver.hader@typo3.org> Reviewed-by: Oliver Hader <oliver.hader@typo3.org>
1 parent 316f5ce commit 88757c3

File tree

20 files changed

+810
-6
lines changed

20 files changed

+810
-6
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the TYPO3 CMS project.
7+
*
8+
* It is free software; you can redistribute it and/or modify it under
9+
* the terms of the GNU General Public License, either version 2
10+
* of the License, or any later version.
11+
*
12+
* For the full copyright and license information, please read the
13+
* LICENSE.txt file that was distributed with this source code.
14+
*
15+
* The TYPO3 project - inspiring people to share!
16+
*/
17+
18+
namespace TYPO3\CMS\Core\Localization;
19+
20+
/**
21+
* @internal
22+
*/
23+
final class LabelBag
24+
{
25+
/**
26+
* @var list<string>
27+
*/
28+
public readonly array $arguments;
29+
30+
/**
31+
* @param string $key e.g. `LLL:EXT:core/Resources/Private/Language/Labels.xlf:HelloWorld`
32+
* @param string ...$arguments optional label arguments to be substituted
33+
*/
34+
public function __construct(
35+
public readonly string $key,
36+
string ...$arguments
37+
) {
38+
$this->arguments = $arguments;
39+
}
40+
41+
/**
42+
* Compiles the given label key and substituted label arguments if given.
43+
*/
44+
public function compile(LanguageService $languageService): string
45+
{
46+
$label = $languageService->sL($this->key);
47+
return sprintf(
48+
$label,
49+
...$this->arguments
50+
) ?: sprintf(
51+
'Error: could not translate key "%s" with value "%s" and %d argument(s)!',
52+
$this->key,
53+
$label,
54+
count($this->arguments)
55+
);
56+
}
57+
}

typo3/sysext/core/Classes/Resource/OnlineMedia/Helpers/AbstractOnlineMediaHelper.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,13 @@
2323
use TYPO3\CMS\Core\Resource\Folder;
2424
use TYPO3\CMS\Core\Resource\Index\FileIndexRepository;
2525
use TYPO3\CMS\Core\Resource\ResourceFactory;
26+
use TYPO3\CMS\Core\Resource\ResourceInstructionTrait;
2627
use TYPO3\CMS\Core\Utility\GeneralUtility;
2728

2829
abstract class AbstractOnlineMediaHelper implements OnlineMediaHelperInterface
2930
{
31+
use ResourceInstructionTrait;
32+
3033
/**
3134
* Cached OnlineMediaIds [fileUid => id]
3235
*
@@ -113,6 +116,7 @@ protected function createNewFile(Folder $targetFolder, $fileName, $onlineMediaId
113116
{
114117
$temporaryFile = GeneralUtility::tempnam('online_media');
115118
GeneralUtility::writeFileToTypo3tempDir($temporaryFile, $onlineMediaId);
119+
$this->skipResourceConsistencyCheckForCommands($targetFolder->getStorage(), $temporaryFile, $fileName);
116120
$file = $targetFolder->addFile($temporaryFile, $fileName, DuplicationBehavior::RENAME);
117121
return $file;
118122
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the TYPO3 CMS project.
7+
*
8+
* It is free software; you can redistribute it and/or modify it under
9+
* the terms of the GNU General Public License, either version 2
10+
* of the License, or any later version.
11+
*
12+
* For the full copyright and license information, please read the
13+
* LICENSE.txt file that was distributed with this source code.
14+
*
15+
* The TYPO3 project - inspiring people to share!
16+
*/
17+
18+
namespace TYPO3\CMS\Core\Resource;
19+
20+
use Psr\Http\Message\UploadedFileInterface;
21+
use TYPO3\CMS\Core\Resource\Service\ResourceConsistencyService;
22+
use TYPO3\CMS\Core\Utility\GeneralUtility;
23+
use TYPO3\CMS\Core\Utility\PathUtility;
24+
25+
/**
26+
* Trait for creating skip-instructions for `ResourceConsistencyService::validate()`.
27+
*/
28+
trait ResourceInstructionTrait
29+
{
30+
/**
31+
* Registers an instruction to skip validation in `ResourceConsistencyService` for a specific uploaded file.
32+
*/
33+
private function skipResourceConsistencyCheckForUploads(
34+
ResourceStorage $storage,
35+
array|UploadedFileInterface $uploadedFile,
36+
?string $targetFileName = null,
37+
): void {
38+
GeneralUtility::makeInstance(ResourceConsistencyService::class)->addExceptionItem(
39+
$storage,
40+
$storage->getUploadedLocalFilePath($uploadedFile),
41+
$storage->getUploadedTargetFileName($uploadedFile, $targetFileName),
42+
);
43+
}
44+
45+
/**
46+
* Registers an instruction to skip validation in `ResourceConsistencyService`
47+
* for commands (such as rename or replace) for existing files.
48+
*/
49+
private function skipResourceConsistencyCheckForCommands(
50+
ResourceStorage $storage,
51+
string|FileInterface $resource,
52+
?string $targetFileName = null,
53+
): void {
54+
$targetFileName ??= PathUtility::basename(
55+
$resource instanceof FileInterface ? $resource->getName() : $resource
56+
);
57+
GeneralUtility::makeInstance(ResourceConsistencyService::class)->addExceptionItem(
58+
$storage,
59+
$resource,
60+
$targetFileName
61+
);
62+
}
63+
}

typo3/sysext/core/Classes/Resource/ResourceStorage.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,13 @@
9191
use TYPO3\CMS\Core\Resource\Search\Result\FileSearchResultInterface;
9292
use TYPO3\CMS\Core\Resource\Security\FileNameValidator;
9393
use TYPO3\CMS\Core\Resource\Service\FileProcessingService;
94+
use TYPO3\CMS\Core\Resource\Service\ResourceConsistencyService;
9495
use TYPO3\CMS\Core\Service\FlexFormService;
9596
use TYPO3\CMS\Core\Utility\Exception\NotImplementedMethodException;
9697
use TYPO3\CMS\Core\Utility\GeneralUtility;
9798
use TYPO3\CMS\Core\Utility\PathUtility;
9899
use TYPO3\CMS\Core\Utility\StringUtility;
100+
use TYPO3\CMS\Core\Validation\ResultException;
99101

100102
/**
101103
* A "mount point" inside the TYPO3 file handling.
@@ -1070,6 +1072,14 @@ protected function assureFileCopyPermissions(FileInterface $file, FolderInterfac
10701072
}
10711073
}
10721074

1075+
/**
1076+
* @throws ResultException
1077+
*/
1078+
protected function assureResourceConsistency(string|FileInterface $resource, string $fileName = ''): void
1079+
{
1080+
GeneralUtility::makeInstance(ResourceConsistencyService::class)->validate($this, $resource, $fileName);
1081+
}
1082+
10731083
/**
10741084
* Check if a file has the permission to be copied on a File/Folder/Storage,
10751085
* if not throw an exception.
@@ -1182,6 +1192,7 @@ public function addFile(string $localFilePath, Folder $targetFolder, string $tar
11821192
)->getFileName();
11831193

11841194
$this->assureFileAddPermissions($targetFolder, $targetFileName);
1195+
$this->assureResourceConsistency($localFilePath, $targetFileName);
11851196

11861197
$replaceExisting = false;
11871198
if ($conflictMode === DuplicationBehavior::CANCEL && $this->driver->fileExistsInFolder($targetFileName, $targetFolder->getIdentifier())) {
@@ -1912,6 +1923,8 @@ public function renameFile(FileInterface $file, string $targetFileName, Duplicat
19121923
}
19131924

19141925
$this->assureFileRenamePermissions($file, $sanitizedTargetFileName);
1926+
$this->assureResourceConsistency($file, $sanitizedTargetFileName);
1927+
19151928
$this->eventDispatcher->dispatch(
19161929
new BeforeFileRenamedEvent($file, $sanitizedTargetFileName)
19171930
);
@@ -1954,6 +1967,8 @@ public function renameFile(FileInterface $file, string $targetFileName, Duplicat
19541967
public function replaceFile(FileInterface $file, string $localFilePath): FileInterface
19551968
{
19561969
$this->assureFileReplacePermissions($file);
1970+
$this->assureResourceConsistency($localFilePath, $file->getName());
1971+
19571972
if (!file_exists($localFilePath)) {
19581973
throw new \InvalidArgumentException('File "' . $localFilePath . '" does not exist.', 1325842622);
19591974
}
@@ -1989,6 +2004,8 @@ public function addUploadedFile(array|UploadedFileInterface $uploadedFileData, ?
19892004
$targetFolder ??= $this->getDefaultFolder();
19902005

19912006
$this->assureFileUploadPermissions($localFilePath, $targetFolder, $targetFileName, $size);
2007+
$this->assureResourceConsistency($localFilePath, $targetFileName);
2008+
19922009
if ($this->hasFileInFolder($targetFileName, $targetFolder) && $conflictMode === DuplicationBehavior::REPLACE) {
19932010
$file = $this->getFileInFolder($targetFileName, $targetFolder);
19942011
$resultObject = $this->replaceFile($file, $localFilePath);

0 commit comments

Comments
 (0)