CVE-2019-11832
Description
TYPO3 8.x before 8.7.25 and 9.x before 9.5.6 allow remote code execution via improperly configured image processing tools like ImageMagick or GraphicsMagick.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
TYPO3 8.x before 8.7.25 and 9.x before 9.5.6 allow remote code execution via improperly configured image processing tools like ImageMagick or GraphicsMagick.
CVE-2019-11832 is a critical vulnerability in TYPO3 CMS versions 8.x before 8.7.25 and 9.x before 9.5.6. The root cause is improper configuration of the applications used for image processing, specifically ImageMagick or GraphicsMagick. TYPO3 fails to properly sanitize the file type scope when invoking these external image processing tools, allowing an attacker to inject arbitrary commands [1][2][3][4].
Exploitation
An attacker can exploit this vulnerability by uploading a specially crafted file that, when processed by the image processing tool, executes arbitrary commands. The TYPO3 system does not properly enclose or validate the file type prefix (e.g., 'png:' or 'gif:') when constructing the command line for ImageMagick/GraphicsMagick, enabling command injection. No authentication is required if the attacker can upload files through public-facing functionality (e.g., file upload fields). The attack surface is the image processing functionality that TYPO3 applies to user-uploaded content [1][2][3][4].
Impact
Successful exploitation results in remote code execution (RCE) on the server, allowing the attacker to fully compromise the TYPO3 installation. This can lead to data theft, site defacement, or further lateral movement within the host environment. The vulnerability is rated critical due to the high potential for complete compromise [1].
Mitigation
The vulnerability is fixed in TYPO3 versions 8.7.25 and 9.5.6. The fix introduces a new ImageMagickFile value object that properly resolves and encloses the file type scope (e.g., 'png:file.png') to prevent command injection [2][3][4]. Users must upgrade to patched versions immediately. No workarounds are provided.
AI Insight generated on May 22, 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.
| Package | Affected versions | Patched versions |
|---|---|---|
typo3/cms-corePackagist | >= 8.0.0, < 8.7.25 | 8.7.25 |
typo3/cms-corePackagist | >= 9.0.0, < 9.5.6 | 9.5.6 |
typo3/cmsPackagist | >= 8.0.0, < 8.7.25 | 8.7.25 |
typo3/cmsPackagist | >= 9.0.0, < 9.5.6 | 9.5.6 |
Affected products
3- TYPO3/TYPO3description
- ghsa-coords2 versions
>= 8.0.0, < 8.7.25+ 1 more
- (no CPE)range: >= 8.0.0, < 8.7.25
- (no CPE)range: >= 8.0.0, < 8.7.25
Patches
3e845d90b82b2[SECURITY] Enclose file type scope when invoking ImageMagick
18 files changed · +9548 −18
typo3/sysext/core/Classes/Imaging/GraphicalFunctions.php+18 −9 modified@@ -2427,8 +2427,11 @@ public function imageMagickIdentify($imagefile) */ protected function executeIdentifyCommandForImageFile(string $imageFile): ?string { - $frame = $this->addFrameSelection ? '[0]' : ''; - $cmd = CommandUtility::imageMagickCommand('identify', '-format "%w %h %e %m" ' . CommandUtility::escapeShellArgument($imageFile . $frame)); + $frame = $this->addFrameSelection ? 0 : null; + $cmd = CommandUtility::imageMagickCommand( + 'identify', + '-format "%w %h %e %m" ' . ImageMagickFile::fromFilePath($imageFile, $frame) + ); $returnVal = []; CommandUtility::exec($cmd, $returnVal); $result = array_pop($returnVal); @@ -2453,8 +2456,13 @@ public function imageMagickExec($input, $output, $params, $frame = 0) } // If addFrameSelection is set in the Install Tool, a frame number is added to // select a specific page of the image (by default this will be the first page) - $frame = $this->addFrameSelection ? '[' . (int)$frame . ']' : ''; - $cmd = CommandUtility::imageMagickCommand('convert', $params . ' ' . CommandUtility::escapeShellArgument($input . $frame) . ' ' . CommandUtility::escapeShellArgument($output)); + $frame = $this->addFrameSelection ? (int)$frame : null; + $cmd = CommandUtility::imageMagickCommand( + 'convert', + $params + . ' ' . ImageMagickFile::fromFilePath($input, $frame) + . ' ' . CommandUtility::escapeShellArgument($output) + ); $this->IM_commands[] = [$output, $cmd]; $ret = CommandUtility::exec($cmd); // Change the permissions of the file @@ -2484,9 +2492,9 @@ public function combineExec($input, $overlay, $mask, $output) $parameters = '-compose over' . ' -quality ' . $this->jpegQuality . ' +matte ' - . CommandUtility::escapeShellArgument($input) . ' ' - . CommandUtility::escapeShellArgument($overlay) . ' ' - . CommandUtility::escapeShellArgument($theMask) . ' ' + . ImageMagickFile::fromFilePath($input) . ' ' + . ImageMagickFile::fromFilePath($overlay) . ' ' + . ImageMagickFile::fromFilePath($theMask) . ' ' . CommandUtility::escapeShellArgument($output); $cmd = CommandUtility::imageMagickCommand('combine', $parameters); $this->IM_commands[] = [$output, $cmd]; @@ -2531,7 +2539,7 @@ public static function gifCompress($theFile, $type) if (@rename($theFile, $temporaryName)) { $cmd = CommandUtility::imageMagickCommand( 'convert', - implode(' ', CommandUtility::escapeShellArguments([$temporaryName, $theFile])), + ImageMagickFile::fromFilePath($temporaryName) . ' ' . CommandUtility::escapeShellArgument($theFile), $gfxConf['processor_path_lzw'] ); CommandUtility::exec($cmd); @@ -2581,7 +2589,8 @@ public static function readPngGif($theFile, $output_png = false) $newFile = Environment::getPublicPath() . '/typo3temp/assets/images/' . md5($theFile . '|' . filemtime($theFile)) . ($output_png ? '.png' : '.gif'); $cmd = CommandUtility::imageMagickCommand( 'convert', - implode(' ', CommandUtility::escapeShellArguments([$theFile, $newFile])), + ImageMagickFile::fromFilePath($theFile) + . ' ' . CommandUtility::escapeShellArgument($newFile), $GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_path'] ); CommandUtility::exec($cmd);
typo3/sysext/core/Classes/Imaging/ImageMagickFile.php+223 −0 added@@ -0,0 +1,223 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Core\Imaging; + +/* + * 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! + */ + +use TYPO3\CMS\Core\Exception; +use TYPO3\CMS\Core\Type\File\FileInfo; +use TYPO3\CMS\Core\Utility\CommandUtility; +use TYPO3\CMS\Core\Utility\GeneralUtility; + +/** + * Value object for file to be used for ImageMagick/GraphicsMagick invocation when + * being used as input file (implies and requires that file exists for some evaluations). + */ +class ImageMagickFile +{ + /** + * Path to input file to be processed + * + * @var string + */ + protected $filePath; + + /** + * Frame to be used (of multi-page document, e.g. PDF) + * + * @var int|null + */ + protected $frame; + + /** + * Whether file actually exists + * + * @var bool + */ + protected $fileExists; + + /** + * File extension as given in $filePath (e.g. 'file.png' -> 'png') + * + * @var string + */ + protected $fileExtension; + + /** + * Resolved mime-type of file + * + * @var string + */ + protected $mimeType; + + /** + * Resolved extension for mime-type (e.g. 'image/png' -> 'png') + * (might be empty if not defined in magic.mime database) + * + * @var string[] + * @see FileInfo::getMimeExtensions() + */ + protected $mimeExtensions = []; + + /** + * Result to be used for ImageMagick/GraphicsMagick invocation containing + * combination of resolved format prefix, $filePath and frame escaped to be + * used as CLI argument (e.g. "'png:file.png'") + * + * @var string + */ + protected $asArgument; + + /** + * File extensions that directly can be used (and are considered to be safe). + * + * @var string[] + */ + protected $allowedExtensions = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'tif', 'tiff', 'bmp', 'pcx', 'tga', 'ico']; + + /** + * File extensions that never shall be used. + * + * @var string[] + */ + protected $deniedExtensions = ['epi', 'eps', 'eps2', 'eps3', 'epsf', 'epsi', 'ept', 'ept2', 'ept3', 'msl', 'ps', 'ps2', 'ps3']; + + /** + * File mime-types that have to be matching. Adding custom mime-types is possible using + * $GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType'] + * + * @var string[] + * @see FileInfo::getMimeExtensions() + */ + protected $mimeTypeExtensionMap = [ + 'image/png' => 'png', + 'image/jpeg' => 'jpg', + 'image/gif' => 'gif', + 'image/heic' => 'heic', + 'image/heif' => 'heif', + 'image/webp' => 'webp', + 'image/svg' => 'svg', + 'image/svg+xml' => 'svg', + 'image/tiff' => 'tif', + 'application/pdf' => 'pdf', + ]; + + /** + * @param string $filePath + * @param int|null $frame + * @return ImageMagickFile + */ + public static function fromFilePath(string $filePath, int $frame = null): self + { + return GeneralUtility::makeInstance( + static::class, + $filePath, + $frame + ); + } + + /** + * @param string $filePath + * @param int|null $frame + * @throws Exception + */ + public function __construct(string $filePath, int $frame = null) + { + $this->frame = $frame; + $this->fileExists = file_exists($filePath); + $this->filePath = $filePath; + $this->fileExtension = pathinfo($filePath, PATHINFO_EXTENSION); + + if ($this->fileExists) { + $fileInfo = $this->getFileInfo($filePath); + $this->mimeType = $fileInfo->getMimeType(); + $this->mimeExtensions = $fileInfo->getMimeExtensions(); + } + + $this->asArgument = $this->escape( + $this->resolvePrefix() . $this->filePath + . ($this->frame !== null ? '[' . $this->frame . ']' : '') + ); + } + + /** + * @return string + */ + public function __toString(): string + { + return $this->asArgument; + } + + /** + * Resolves according ImageMagic/GraphicsMagic format (e.g. 'png:', 'jpg:', ...). + * + in case mime-type could be resolved and is configured, it takes precedence + * + otherwise resolved mime-type extension of mime.magick database is used if available + * (includes custom settings with $GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType']) + * + otherwise "safe" and allowed file extension is used (jpg, png, gif, webp, tif, ...) + * + potentially malicious script formats (eps, ps, ...) are not allowed + * + * @return string + * @throws Exception + */ + protected function resolvePrefix(): string + { + $prefixExtension = null; + $fileExtension = strtolower($this->fileExtension); + if ($this->mimeType !== null && !empty($this->mimeTypeExtensionMap[$this->mimeType])) { + $prefixExtension = $this->mimeTypeExtensionMap[$this->mimeType]; + } elseif (!empty($this->mimeExtensions) && strpos((string)$this->mimeType, 'image/') === 0) { + $prefixExtension = $this->mimeExtensions[0]; + } elseif ($this->isInAllowedExtensions($fileExtension)) { + $prefixExtension = $fileExtension; + } + if ($prefixExtension !== null && !in_array(strtolower($prefixExtension), $this->deniedExtensions, true)) { + return $prefixExtension . ':'; + } + throw new Exception( + sprintf( + 'Unsupported file %s (%s)', + basename($this->filePath), + $this->mimeType ?? 'unknown' + ), + 1550060977 + ); + } + + /** + * @param string $value + * @return string + */ + protected function escape(string $value): string + { + return CommandUtility::escapeShellArgument($value); + } + + /** + * @param string $extension + * @return bool + */ + protected function isInAllowedExtensions(string $extension): bool + { + return in_array($extension, $this->allowedExtensions, true); + } + + /** + * @param string $filePath + * @return FileInfo + */ + protected function getFileInfo(string $filePath): FileInfo + { + return GeneralUtility::makeInstance(FileInfo::class, $filePath); + } +}
typo3/sysext/core/Classes/Resource/OnlineMedia/Processing/PreviewProcessing.php+4 −4 modified@@ -16,6 +16,7 @@ use TYPO3\CMS\Core\Core\Environment; use TYPO3\CMS\Core\Imaging\GraphicalFunctions; +use TYPO3\CMS\Core\Imaging\ImageMagickFile; use TYPO3\CMS\Core\Resource\Driver\DriverInterface; use TYPO3\CMS\Core\Resource\File; use TYPO3\CMS\Core\Resource\OnlineMedia\Helpers\OnlineMediaHelperRegistry; @@ -138,11 +139,10 @@ protected function resizeImage($originalFileName, $temporaryFileName, $configura $arguments = CommandUtility::escapeShellArguments([ 'width' => $configuration['width'], 'height' => $configuration['height'], - 'originalFileName' => $originalFileName, - 'temporaryFileName' => $temporaryFileName, ]); - $parameters = '-sample ' . $arguments['width'] . 'x' . $arguments['height'] . ' ' - . $arguments['originalFileName'] . '[0] ' . $arguments['temporaryFileName']; + $parameters = '-sample ' . $arguments['width'] . 'x' . $arguments['height'] + . ' ' . ImageMagickFile::fromFilePath($originalFileName, 0) + . ' ' . CommandUtility::escapeShellArgument($temporaryFileName); $cmd = CommandUtility::imageMagickCommand('convert', $parameters) . ' 2>&1'; CommandUtility::exec($cmd);
typo3/sysext/core/Classes/Resource/Processing/LocalPreviewHelper.php+4 −4 modified@@ -15,6 +15,7 @@ */ use TYPO3\CMS\Core\Imaging\GraphicalFunctions; +use TYPO3\CMS\Core\Imaging\ImageMagickFile; use TYPO3\CMS\Core\Resource\File; use TYPO3\CMS\Core\Utility\CommandUtility; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -151,11 +152,10 @@ protected function generatePreviewFromFile(File $file, array $configuration, $ta $arguments = CommandUtility::escapeShellArguments([ 'width' => $configuration['width'], 'height' => $configuration['height'], - 'originalFileName' => $originalFileName, - 'targetFilePath' => $targetFilePath, ]); - $parameters = '-sample ' . $arguments['width'] . 'x' . $arguments['height'] . ' ' - . $arguments['originalFileName'] . '[0] ' . $arguments['targetFilePath']; + $parameters = '-sample ' . $arguments['width'] . 'x' . $arguments['height'] + . ' ' . ImageMagickFile::fromFilePath($originalFileName, 0) + . ' ' . CommandUtility::escapeShellArgument($targetFilePath); $cmd = CommandUtility::imageMagickCommand('convert', $parameters) . ' 2>&1'; CommandUtility::exec($cmd);
typo3/sysext/core/Classes/Type/File/FileInfo.php+42 −1 modified@@ -13,7 +13,9 @@ * * The TYPO3 project - inspiring people to share! */ + use TYPO3\CMS\Core\Type\TypeInterface; +use TYPO3\CMS\Core\Utility\GeneralUtility; /** * A SPL FileInfo class providing general information related to a file. @@ -23,6 +25,9 @@ class FileInfo extends \SplFileInfo implements TypeInterface /** * Return the mime type of a file. * + * TYPO3 specific settings in $GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType'] take + * precedence over native resolving. + * * @return string|bool Returns the mime type or FALSE if the mime type could not be discovered */ public function getMimeType() @@ -48,7 +53,7 @@ public function getMimeType() 'mimeType' => &$mimeType ]; - \TYPO3\CMS\Core\Utility\GeneralUtility::callUserFunction( + GeneralUtility::callUserFunction( $mimeTypeGuesser, $hookParameters, $this @@ -57,4 +62,40 @@ public function getMimeType() return $mimeType; } + + /** + * Returns the file extensions appropiate for a the MIME type detected in the file. For types that commonly have + * multiple file extensions, such as JPEG images, then the return value is multiple extensions, for instance that + * could be ['jpeg', 'jpg', 'jpe', 'jfif']. For unknown types not available in the magic.mime database + * (/etc/magic.mime, /etc/mime.types, ...), then return value is an empty array. + * + * TYPO3 specific settings in $GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType'] take + * precedence over native resolving. + * + * @return string[] + */ + public function getMimeExtensions(): array + { + $mimeExtensions = []; + if ($this->isFile()) { + $fileExtensionToMimeTypeMapping = $GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType']; + $mimeType = $this->getMimeType(); + if (in_array($mimeType, $fileExtensionToMimeTypeMapping, true)) { + $mimeExtensions = array_keys($fileExtensionToMimeTypeMapping, $mimeType, true); + } elseif (function_exists('finfo_file')) { + $fileInfo = new \finfo(); + $mimeExtensions = array_filter( + GeneralUtility::trimExplode( + '/', + (string)$fileInfo->file($this->getPathname(), FILEINFO_EXTENSION) + ), + function ($item) { + // filter invalid items ('???' is used if not found in magic.mime database) + return $item !== '' && $item !== '???'; + } + ); + } + } + return $mimeExtensions; + } }
typo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.ai+1018 −0 addedtypo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.bmp+0 −0 addedtypo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.eps+7842 −0 addedtypo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.fax+0 −0 addedtypo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.gif+0 −0 addedtypo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.jpg+0 −0 addedtypo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.pdf+0 −0 addedtypo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.png+0 −0 addedtypo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.ps+0 −0 addedtypo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.svg+44 −0 added@@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + version="1.1" + width="481.71875" + height="203.5625" + id="svg7592"> + <defs + id="defs7594" /> + <metadata + id="metadata7597"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + transform="translate(258,-219.15625)" + id="layer1"> + <path + d="m 140.60001,371.50349 c -5.205,0 -12.96125,-1.59375 -13.9175,-1.81 l 0,-7.75125 c 2.55125,0.53 9.135,1.62875 13.8125,1.62875 5.41625,0 8.9225,-4.60625 8.9225,-12.78375 0,-9.66875 -1.59125,-14.7675 -9.135,-14.7675 l -8.7125,0 0,-7.755 7.64875,0 c 8.6075,0 9.03,-8.81875 9.03,-13.0675 0,-8.395 -2.65625,-11.79375 -7.96625,-11.79375 -4.675,0 -9.98875,1.16875 -13.06875,1.80625 l 0,-7.75375 c 1.17,-0.21375 7.43875,-1.80625 12.855,-1.80625 10.94375,0 17.21125,4.67375 17.21125,20.505 0,7.22375 -2.55125,13.59625 -8.18125,15.61625 6.47875,0.42375 9.45375,7.54125 9.45375,17.95375 0,15.82875 -6.15875,21.77875 -17.9525,21.77875 m -48.867497,-68.1 c -9.55875,0 -12.74875,6.48375 -12.74875,29.85375 0,22.8425 3.19,30.49125 12.74875,30.49125 9.561247,0 12.748747,-7.64875 12.748747,-30.49125 0,-23.37 -3.1875,-29.85375 -12.748747,-29.85375 m 0,68.1 c -17.52875,0 -22.205,-12.74875 -22.205,-38.77625 0,-24.9675 4.67625,-37.0775 22.205,-37.0775 17.529997,0 22.202497,12.11 22.202497,37.0775 0,26.0275 -4.6725,38.77625 -22.202497,38.77625 m -52.91,-68.20375 c -5.845,0 -9.98625,0.63625 -9.98625,0.63625 l 0,31.02 9.98625,0 c 5.94875,0 10.0925,-3.93125 10.0925,-15.51 0,-10.625 -2.55,-16.14625 -10.0925,-16.14625 m -1.0625,39.4125 -8.92375,0 0,28.045 -9.2425,0 0,-74.365 c 0,0 9.13625,-0.7425 17.95375,-0.7425 16.14875,0 20.825,9.985 20.825,23.0525 0,16.15 -5.52625,24.01 -20.6125,24.01 m -48.0175,-6.48 0,34.525 -9.56125,0 0,-34.525 -19.015,-39.84 10.19625,0 14.02375,30.065 14.02374986,-30.065 9.66625004,0 -19.3337499,39.84 z m -49.58,-31.76375 0,66.28875 -9.24125,0 0,-66.28875 -16.36125,0 0,-8.07625 41.9625,0 0,8.07625 -16.36,0 z" + id="path5771" + style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none" /> + <path + d="m -129.96886,340.07111 c -1.50125,0.4425 -2.6975,0.595 -4.2625,0.595 -12.84,0 -31.7,-44.87 -31.7,-59.80375 0,-5.50125 1.30625,-7.335 3.1425,-8.90625 -15.7175,1.8325 -34.58,7.5975 -40.6075,14.9325 -1.30875,1.835 -2.095,4.71625 -2.095,8.3825 0,23.3175 24.8875,76.23375 42.44125,76.23375 8.12,0 21.81625,-13.36 33.08125,-31.43375" + id="path5775" + style="fill:#ff8700;fill-opacity:1;fill-rule:nonzero;stroke:none" /> + <path + d="m -138.16461,270.38299 c 16.2425,0 32.4875,2.62 32.4875,11.78875 0,18.60125 -11.78875,41.13125 -17.815,41.13125 -10.74,0 -24.10125,-29.86375 -24.10125,-44.7975 0,-6.81125 2.62,-8.1225 9.42875,-8.1225" + id="path5779" + style="fill:#ff8700;fill-opacity:1;fill-rule:nonzero;stroke:none" /> + </g> +</svg>
typo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.tif+0 −0 addedtypo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.webp+0 −0 addedtypo3/sysext/core/Tests/Functional/Imaging/ImageMagickFileTest.php+353 −0 added@@ -0,0 +1,353 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Core\Tests\Functional\Imaging; + +/* + * 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! + */ + +use org\bovigo\vfs\vfsStream; +use org\bovigo\vfs\vfsStreamDirectory; +use PHPUnit\Framework\MockObject\MockObject; +use TYPO3\CMS\Core\Exception; +use TYPO3\CMS\Core\Imaging\ImageMagickFile; +use TYPO3\CMS\Core\Type\File\FileInfo; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; + +class ImageMagickFileTest extends FunctionalTestCase +{ + /** + * @var vfsStreamDirectory + */ + private $directory; + + protected function setUp() + { + parent::setUp(); + + $fixturePath = __DIR__ . '/Fixtures'; + $structure = []; + $this->addFiles($structure, ['file.ai', 'file.ai.jpg'], $fixturePath . '/file.ai'); + $this->addFiles($structure, ['file.bmp', 'file.bmp.jpg'], $fixturePath . '/file.bmp'); + $this->addFiles($structure, ['file.gif', 'file.gif.jpg'], $fixturePath . '/file.gif'); + $this->addFiles($structure, ['file.fax', 'file.fax.jpg'], $fixturePath . '/file.fax'); + $this->addFiles($structure, ['file.jpg', 'file.jpg.png'], $fixturePath . '/file.jpg'); + $this->addFiles($structure, ['file.png', 'file.png.jpg'], $fixturePath . '/file.png'); + $this->addFiles($structure, ['file.svg', 'file.svg.jpg'], $fixturePath . '/file.svg'); + $this->addFiles($structure, ['file.tif', 'file.tif.jpg'], $fixturePath . '/file.tif'); + $this->addFiles($structure, ['file.webp', 'file.webp.jpg'], $fixturePath . '/file.webp'); + $this->addFiles($structure, ['file.pdf', 'file.pdf.jpg'], $fixturePath . '/file.pdf'); + $this->addFiles($structure, ['file.ps', 'file.ps.jpg'], $fixturePath . '/file.ps'); + $this->addFiles($structure, ['file.eps', 'file.eps.jpg'], $fixturePath . '/file.eps'); + $this->directory = vfsStream::setup('root', null, $structure); + } + + protected function tearDown() + { + unset($this->directory); + parent::tearDown(); + } + + /** + * @return array + */ + public function framesAreConsideredDataProvider(): array + { + return [ + 'file.pdf' => ['file.pdf', null, '\'pdf:{directory}/file.pdf\''], + 'file.pdf[0]' => ['file.pdf', 0, '\'pdf:{directory}/file.pdf[0]\''], + ]; + } + + /** + * @param string $fileName + * @param int|null $frame + * @param string $expectation + * + * @test + * @dataProvider framesAreConsideredDataProvider + */ + public function framesAreConsidered(string $fileName, ?int $frame, string $expectation) + { + $expectation = $this->substituteVariables($expectation); + $filePath = sprintf('%s/%s', $this->directory->url(), $fileName); + $file = ImageMagickFile::fromFilePath($filePath, $frame); + self::assertSame($expectation, (string)$file); + } + + /** + * @return array + */ + public function resultIsEscapedDataProvider(): array + { + // probably Windows system + if (DIRECTORY_SEPARATOR === '\\') { + return [ + 'without frame' => ['file.pdf', null, '"pdf:{directory}/file.pdf"'], + 'with first frame' => ['file.pdf', 0, '"pdf:{directory}/file.pdf[0]"'], + 'special literals' => ['\'`%$!".png', 0, '"png:{directory}/\'` $ .png[0]"'], + ]; + } + // probably Unix system + return [ + 'without frame' => ['file.pdf', null, '\'pdf:{directory}/file.pdf\''], + 'with first frame' => ['file.pdf', 0, '\'pdf:{directory}/file.pdf[0]\''], + 'special literals' => ['\'`%$!".png', 0, '\'png:{directory}/\'\\\'\'`%$!".png[0]\''], + ]; + } + + /** + * @param string $fileName + * @param int|null $frame + * @param string $expectation + * + * @test + * @dataProvider resultIsEscapedDataProvider + */ + public function resultIsEscaped(string $fileName, ?int $frame, string $expectation) + { + $expectation = $this->substituteVariables($expectation); + $filePath = sprintf('%s/%s', $this->directory->url(), $fileName); + $file = ImageMagickFile::fromFilePath($filePath, $frame); + self::assertSame($expectation, (string)$file); + } + + /** + * @return array + */ + public function fileStatementIsResolvedDataProvider(): array + { + return [ + 'file.ai' => ['file.ai', '\'pdf:{directory}/file.ai\''], + 'file.ai.jpg' => ['file.ai.jpg', '\'pdf:{directory}/file.ai.jpg\''], + 'file.gif' => ['file.gif', '\'gif:{directory}/file.gif\''], + 'file.gif.jpg' => ['file.gif.jpg', '\'gif:{directory}/file.gif.jpg\''], + 'file.jpg' => ['file.jpg', '\'jpg:{directory}/file.jpg\''], + 'file.jpg.png' => ['file.jpg.png', '\'jpg:{directory}/file.jpg.png\''], + 'file.png' => ['file.png', '\'png:{directory}/file.png\''], + 'file.png.jpg' => ['file.png.jpg', '\'png:{directory}/file.png.jpg\''], + 'file.svg' => ['file.svg', '\'svg:{directory}/file.svg\''], + 'file.svg.jpg' => ['file.svg.jpg', '\'svg:{directory}/file.svg.jpg\''], + 'file.tif' => ['file.tif', '\'tif:{directory}/file.tif\''], + 'file.tif.jpg' => ['file.tif.jpg', '\'tif:{directory}/file.tif.jpg\''], + 'file.webp' => ['file.webp', '\'webp:{directory}/file.webp\''], + 'file.webp.jpg' => ['file.webp.jpg', '\'webp:{directory}/file.webp.jpg\''], + 'file.pdf' => ['file.pdf', '\'pdf:{directory}/file.pdf\''], + 'file.pdf.jpg' => ['file.pdf.jpg', '\'pdf:{directory}/file.pdf.jpg\''], + // accepted, since postscript files are converted using 'jpg:' format + 'file.ps.jpg' => ['file.ps.jpg', '\'jpg:{directory}/file.ps.jpg\''], + 'file.eps.jpg' => ['file.eps.jpg', '\'jpg:{directory}/file.eps.jpg\''], + ]; + } + + /** + * @param string $fileName + * @param string $expectation + * + * @test + * @dataProvider fileStatementIsResolvedDataProvider + */ + public function fileStatementIsResolved(string $fileName, string $expectation) + { + $expectation = $this->substituteVariables($expectation); + $filePath = sprintf('%s/%s', $this->directory->url(), $fileName); + $file = ImageMagickFile::fromFilePath($filePath, null); + self::assertSame($expectation, (string)$file); + } + + /** + * In case mime-types cannot be resolved (or cannot be verified), allowed extensions + * are used as conversion format (e.g. 'file.ai.jpg' -> 'jpg:...'). + * + * @return array + */ + public function fileStatementIsResolvedForEnforcedMimeTypeDataProvider(): array + { + return [ + 'file.ai.jpg' => ['file.ai.jpg', '\'jpg:{directory}/file.ai.jpg\'', 'inode/x-empty'], + 'file.bmp.jpg' => ['file.bmp.jpg', '\'jpg:{directory}/file.bmp.jpg\'', 'inode/x-empty'], + 'file.fax.jpg' => ['file.fax.jpg', '\'jpg:{directory}/file.fax.jpg\'', 'inode/x-empty'], + 'file.gif.jpg' => ['file.gif.jpg', '\'jpg:{directory}/file.gif.jpg\'', 'inode/x-empty'], + 'file.jpg' => ['file.jpg', '\'jpg:{directory}/file.jpg\'', 'inode/x-empty'], + 'file.jpg.png' => ['file.jpg.png', '\'png:{directory}/file.jpg.png\'', 'inode/x-empty'], + 'file.png' => ['file.png', '\'png:{directory}/file.png\'', 'inode/x-empty'], + 'file.png.jpg' => ['file.png.jpg', '\'jpg:{directory}/file.png.jpg\'', 'inode/x-empty'], + 'file.svg.jpg' => ['file.svg.jpg', '\'jpg:{directory}/file.svg.jpg\'', 'inode/x-empty'], + 'file.tif' => ['file.tif', '\'tif:{directory}/file.tif\'', 'inode/x-empty'], + 'file.tif.jpg' => ['file.tif.jpg', '\'jpg:{directory}/file.tif.jpg\'', 'inode/x-empty'], + 'file.webp' => ['file.webp', '\'webp:{directory}/file.webp\'', 'inode/x-empty'], + 'file.webp.jpg' => ['file.webp.jpg', '\'jpg:{directory}/file.webp.jpg\'', 'inode/x-empty'], + 'file.pdf.jpg' => ['file.pdf.jpg', '\'jpg:{directory}/file.pdf.jpg\'', 'inode/x-empty'], + ]; + } + + /** + * @param string $fileName + * @param string $expectation + * @param string $mimeType + * + * @test + * @dataProvider fileStatementIsResolvedForEnforcedMimeTypeDataProvider + */ + public function fileStatementIsResolvedForEnforcedMimeType(string $fileName, string $expectation, string $mimeType) + { + $this->simulateNextFileInfoInvocation($mimeType); + $expectation = $this->substituteVariables($expectation); + $filePath = sprintf('%s/%s', $this->directory->url(), $fileName); + $file = ImageMagickFile::fromFilePath($filePath, null); + self::assertSame($expectation, (string)$file); + } + + /** + * @return array + */ + public function fileStatementIsResolvedForConfiguredMimeTypeDataProvider(): array + { + return [ + 'file.fax' => ['file.fax', '\'g3:{directory}/file.fax\''], + 'file.bmp' => ['file.bmp', '\'dib:{directory}/file.bmp\''], + ]; + } + + /** + * @param string $fileName + * @param string $expectation + * + * @test + * @dataProvider fileStatementIsResolvedForConfiguredMimeTypeDataProvider + */ + public function fileStatementIsResolvedForConfiguredMimeType(string $fileName, string $expectation) + { + $GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType']['g3'] = 'image/g3fax'; + $GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType']['fax'] = 'image/g3fax'; + $GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType']['dib'] = 'image/x-ms-bmp'; + $GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType']['bmp'] = 'image/x-ms-bmp'; + + $expectation = $this->substituteVariables($expectation); + $filePath = sprintf('%s/%s', $this->directory->url(), $fileName); + $file = ImageMagickFile::fromFilePath($filePath, null); + self::assertSame($expectation, (string)$file); + } + + /** + * @return array + */ + public function fileStatementIsDeniedDataProvider(): array + { + return [ + 'file.ps' => ['file.ps'], + 'file.eps' => ['file.eps'], + // denied since not defined in allowed extensions + 'file.ai' => ['file.ai', 'inode/x-empty'], + 'file.svg' => ['file.svg', 'inode/x-empty'], + 'file.pdf' => ['file.pdf', 'inode/x-empty'], + ]; + } + + /** + * @param string $fileName + * @param string|null $mimeType + * + * @test + * @dataProvider fileStatementIsDeniedDataProvider + */ + public function fileStatementIsDenied(string $fileName, string $mimeType = null) + { + self::expectException(Exception::class); + self::expectExceptionCode(1550060977); + + if ($mimeType !== null) { + $this->simulateNextFileInfoInvocation($mimeType); + } + + $filePath = sprintf('%s/%s', $this->directory->url(), $fileName); + ImageMagickFile::fromFilePath($filePath, null); + } + + /** + * @return array + */ + public function fileStatementIsDeniedForConfiguredMimeTypeDataProvider(): array + { + return [ + 'file.ps' => ['file.ps'], + 'file.eps' => ['file.eps'], + ]; + } + + /** + * @param string $fileName + * + * @test + * @dataProvider fileStatementIsDeniedForConfiguredMimeTypeDataProvider + */ + public function fileStatementIsDeniedForConfiguredMimeType(string $fileName) + { + $GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType']['ps'] = 'image/x-see-no-evil'; + $GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType']['eps'] = 'image/x-see-no-evil'; + + self::expectException(Exception::class); + self::expectExceptionCode(1550060977); + + $filePath = sprintf('%s/%s', $this->directory->url(), $fileName); + ImageMagickFile::fromFilePath($filePath, null); + } + + /** + * @param array $structure + * @param array $fileNames + * @param string $sourcePath + */ + private function addFiles(array &$structure, array $fileNames, string $sourcePath): void + { + $structure = array_merge( + $structure, + array_fill_keys( + $fileNames, + file_get_contents($sourcePath) + ) + ); + } + + /** + * @param string $value + * @return string + */ + private function substituteVariables(string $value): string + { + return str_replace( + ['{directory}'], + [$this->directory->url()], + $value + ); + } + + /** + * @param string $mimeType + * @param string[] $mimeExtensions + */ + private function simulateNextFileInfoInvocation(string $mimeType, array $mimeExtensions = []) + { + /** @var FileInfo|MockObject $fileInfo */ + $fileInfo = $this->getAccessibleMock( + FileInfo::class, + ['getMimeType', 'getMimeExtensions'], + [], + '', + false + ); + $fileInfo->expects(self::atLeastOnce())->method('getMimeType')->willReturn($mimeType); + $fileInfo->expects(self::atLeastOnce())->method('getMimeExtensions')->willReturn($mimeExtensions); + GeneralUtility::addInstance(FileInfo::class, $fileInfo); + } +}
2c04eeac4473[SECURITY] Enclose file type scope when invoking ImageMagick
18 files changed · +9547 −18
typo3/sysext/core/Classes/Imaging/GraphicalFunctions.php+17 −9 modified@@ -2456,8 +2456,11 @@ public function imageMagickIdentify($imagefile) return null; } - $frame = $this->addFrameSelection ? '[0]' : ''; - $cmd = CommandUtility::imageMagickCommand('identify', CommandUtility::escapeShellArgument($imagefile) . $frame); + $frame = $this->addFrameSelection ? 0 : null; + $cmd = CommandUtility::imageMagickCommand( + 'identify', + ImageMagickFile::fromFilePath($imagefile, $frame) + ); $returnVal = []; CommandUtility::exec($cmd, $returnVal); $splitstring = array_pop($returnVal); @@ -2500,8 +2503,13 @@ public function imageMagickExec($input, $output, $params, $frame = 0) } // If addFrameSelection is set in the Install Tool, a frame number is added to // select a specific page of the image (by default this will be the first page) - $frame = $this->addFrameSelection ? '[' . (int)$frame . ']' : ''; - $cmd = CommandUtility::imageMagickCommand('convert', $params . ' ' . CommandUtility::escapeShellArgument($input . $frame) . ' ' . CommandUtility::escapeShellArgument($output)); + $frame = $this->addFrameSelection ? (int)$frame : null; + $cmd = CommandUtility::imageMagickCommand( + 'convert', + $params + . ' ' . ImageMagickFile::fromFilePath($input, $frame) + . ' ' . CommandUtility::escapeShellArgument($output) + ); $this->IM_commands[] = [$output, $cmd]; $ret = CommandUtility::exec($cmd); // Change the permissions of the file @@ -2531,9 +2539,9 @@ public function combineExec($input, $overlay, $mask, $output) $parameters = '-compose over' . ' -quality ' . $this->jpegQuality . ' +matte ' - . CommandUtility::escapeShellArgument($input) . ' ' - . CommandUtility::escapeShellArgument($overlay) . ' ' - . CommandUtility::escapeShellArgument($theMask) . ' ' + . ImageMagickFile::fromFilePath($input) . ' ' + . ImageMagickFile::fromFilePath($overlay) . ' ' + . ImageMagickFile::fromFilePath($theMask) . ' ' . CommandUtility::escapeShellArgument($output); $cmd = CommandUtility::imageMagickCommand('combine', $parameters); $this->IM_commands[] = [$output, $cmd]; @@ -2578,7 +2586,7 @@ public static function gifCompress($theFile, $type) if (@rename($theFile, $temporaryName)) { $cmd = CommandUtility::imageMagickCommand( 'convert', - implode(' ', CommandUtility::escapeShellArguments([$temporaryName, $theFile])), + ImageMagickFile::fromFilePath($temporaryName) . ' ' . CommandUtility::escapeShellArgument($theFile), $gfxConf['processor_path_lzw'] ); CommandUtility::exec($cmd); @@ -2628,7 +2636,7 @@ public static function readPngGif($theFile, $output_png = false) $newFile = Environment::getPublicPath() . '/typo3temp/assets/images/' . md5($theFile . '|' . filemtime($theFile)) . ($output_png ? '.png' : '.gif'); $cmd = CommandUtility::imageMagickCommand( 'convert', - implode(' ', CommandUtility::escapeShellArguments([$theFile, $newFile])), + ImageMagickFile::fromFilePath($theFile) . ' ' . CommandUtility::escapeShellArgument($newFile), $GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_path'] ); CommandUtility::exec($cmd);
typo3/sysext/core/Classes/Imaging/ImageMagickFile.php+223 −0 added@@ -0,0 +1,223 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Core\Imaging; + +/* + * 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! + */ + +use TYPO3\CMS\Core\Exception; +use TYPO3\CMS\Core\Type\File\FileInfo; +use TYPO3\CMS\Core\Utility\CommandUtility; +use TYPO3\CMS\Core\Utility\GeneralUtility; + +/** + * Value object for file to be used for ImageMagick/GraphicsMagick invocation when + * being used as input file (implies and requires that file exists for some evaluations). + */ +class ImageMagickFile +{ + /** + * Path to input file to be processed + * + * @var string + */ + protected $filePath; + + /** + * Frame to be used (of multi-page document, e.g. PDF) + * + * @var int|null + */ + protected $frame; + + /** + * Whether file actually exists + * + * @var bool + */ + protected $fileExists; + + /** + * File extension as given in $filePath (e.g. 'file.png' -> 'png') + * + * @var string + */ + protected $fileExtension; + + /** + * Resolved mime-type of file + * + * @var string + */ + protected $mimeType; + + /** + * Resolved extension for mime-type (e.g. 'image/png' -> 'png') + * (might be empty if not defined in magic.mime database) + * + * @var string[] + * @see FileInfo::getMimeExtensions() + */ + protected $mimeExtensions = []; + + /** + * Result to be used for ImageMagick/GraphicsMagick invocation containing + * combination of resolved format prefix, $filePath and frame escaped to be + * used as CLI argument (e.g. "'png:file.png'") + * + * @var string + */ + protected $asArgument; + + /** + * File extensions that directly can be used (and are considered to be safe). + * + * @var string[] + */ + protected $allowedExtensions = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'tif', 'tiff', 'bmp', 'pcx', 'tga', 'ico']; + + /** + * File extensions that never shall be used. + * + * @var string[] + */ + protected $deniedExtensions = ['epi', 'eps', 'eps2', 'eps3', 'epsf', 'epsi', 'ept', 'ept2', 'ept3', 'msl', 'ps', 'ps2', 'ps3']; + + /** + * File mime-types that have to be matching. Adding custom mime-types is possible using + * $GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType'] + * + * @var string[] + * @see FileInfo::getMimeExtensions() + */ + protected $mimeTypeExtensionMap = [ + 'image/png' => 'png', + 'image/jpeg' => 'jpg', + 'image/gif' => 'gif', + 'image/heic' => 'heic', + 'image/heif' => 'heif', + 'image/webp' => 'webp', + 'image/svg' => 'svg', + 'image/svg+xml' => 'svg', + 'image/tiff' => 'tif', + 'application/pdf' => 'pdf', + ]; + + /** + * @param string $filePath + * @param int|null $frame + * @return ImageMagickFile + */ + public static function fromFilePath(string $filePath, int $frame = null): self + { + return GeneralUtility::makeInstance( + static::class, + $filePath, + $frame + ); + } + + /** + * @param string $filePath + * @param int|null $frame + * @throws Exception + */ + public function __construct(string $filePath, int $frame = null) + { + $this->frame = $frame; + $this->fileExists = file_exists($filePath); + $this->filePath = $filePath; + $this->fileExtension = pathinfo($filePath, PATHINFO_EXTENSION); + + if ($this->fileExists) { + $fileInfo = $this->getFileInfo($filePath); + $this->mimeType = $fileInfo->getMimeType(); + $this->mimeExtensions = $fileInfo->getMimeExtensions(); + } + + $this->asArgument = $this->escape( + $this->resolvePrefix() . $this->filePath + . ($this->frame !== null ? '[' . $this->frame . ']' : '') + ); + } + + /** + * @return string + */ + public function __toString(): string + { + return $this->asArgument; + } + + /** + * Resolves according ImageMagic/GraphicsMagic format (e.g. 'png:', 'jpg:', ...). + * + in case mime-type could be resolved and is configured, it takes precedence + * + otherwise resolved mime-type extension of mime.magick database is used if available + * (includes custom settings with $GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType']) + * + otherwise "safe" and allowed file extension is used (jpg, png, gif, webp, tif, ...) + * + potentially malicious script formats (eps, ps, ...) are not allowed + * + * @return string + * @throws Exception + */ + protected function resolvePrefix(): string + { + $prefixExtension = null; + $fileExtension = strtolower($this->fileExtension); + if ($this->mimeType !== null && !empty($this->mimeTypeExtensionMap[$this->mimeType])) { + $prefixExtension = $this->mimeTypeExtensionMap[$this->mimeType]; + } elseif (!empty($this->mimeExtensions) && strpos((string)$this->mimeType, 'image/') === 0) { + $prefixExtension = $this->mimeExtensions[0]; + } elseif ($this->isInAllowedExtensions($fileExtension)) { + $prefixExtension = $fileExtension; + } + if ($prefixExtension !== null && !in_array(strtolower($prefixExtension), $this->deniedExtensions, true)) { + return $prefixExtension . ':'; + } + throw new Exception( + sprintf( + 'Unsupported file %s (%s)', + basename($this->filePath), + $this->mimeType ?? 'unknown' + ), + 1550060977 + ); + } + + /** + * @param string $value + * @return string + */ + protected function escape(string $value): string + { + return CommandUtility::escapeShellArgument($value); + } + + /** + * @param string $extension + * @return bool + */ + protected function isInAllowedExtensions(string $extension): bool + { + return in_array($extension, $this->allowedExtensions, true); + } + + /** + * @param string $filePath + * @return FileInfo + */ + protected function getFileInfo(string $filePath): FileInfo + { + return GeneralUtility::makeInstance(FileInfo::class, $filePath); + } +}
typo3/sysext/core/Classes/Resource/OnlineMedia/Processing/PreviewProcessing.php+4 −4 modified@@ -16,6 +16,7 @@ use TYPO3\CMS\Core\Core\Environment; use TYPO3\CMS\Core\Imaging\GraphicalFunctions; +use TYPO3\CMS\Core\Imaging\ImageMagickFile; use TYPO3\CMS\Core\Resource\Driver\DriverInterface; use TYPO3\CMS\Core\Resource\File; use TYPO3\CMS\Core\Resource\OnlineMedia\Helpers\OnlineMediaHelperRegistry; @@ -138,11 +139,10 @@ protected function resizeImage($originalFileName, $temporaryFileName, $configura $arguments = CommandUtility::escapeShellArguments([ 'width' => $configuration['width'], 'height' => $configuration['height'], - 'originalFileName' => $originalFileName, - 'temporaryFileName' => $temporaryFileName, ]); - $parameters = '-sample ' . $arguments['width'] . 'x' . $arguments['height'] . ' ' - . $arguments['originalFileName'] . '[0] ' . $arguments['temporaryFileName']; + $parameters = '-sample ' . $arguments['width'] . 'x' . $arguments['height'] + . ' ' . ImageMagickFile::fromFilePath($originalFileName, 0) + . ' ' . CommandUtility::escapeShellArgument($temporaryFileName); $cmd = CommandUtility::imageMagickCommand('convert', $parameters) . ' 2>&1'; CommandUtility::exec($cmd);
typo3/sysext/core/Classes/Resource/Processing/LocalPreviewHelper.php+4 −4 modified@@ -15,6 +15,7 @@ */ use TYPO3\CMS\Core\Imaging\GraphicalFunctions; +use TYPO3\CMS\Core\Imaging\ImageMagickFile; use TYPO3\CMS\Core\Resource\File; use TYPO3\CMS\Core\Utility\CommandUtility; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -151,11 +152,10 @@ protected function generatePreviewFromFile(File $file, array $configuration, $ta $arguments = CommandUtility::escapeShellArguments([ 'width' => $configuration['width'], 'height' => $configuration['height'], - 'originalFileName' => $originalFileName, - 'targetFilePath' => $targetFilePath, ]); - $parameters = '-sample ' . $arguments['width'] . 'x' . $arguments['height'] . ' ' - . $arguments['originalFileName'] . '[0] ' . $arguments['targetFilePath']; + $parameters = '-sample ' . $arguments['width'] . 'x' . $arguments['height'] + . ' ' . ImageMagickFile::fromFilePath($originalFileName, 0) + . ' ' . CommandUtility::escapeShellArgument($targetFilePath); $cmd = CommandUtility::imageMagickCommand('convert', $parameters) . ' 2>&1'; CommandUtility::exec($cmd);
typo3/sysext/core/Classes/Type/File/FileInfo.php+42 −1 modified@@ -13,7 +13,9 @@ * * The TYPO3 project - inspiring people to share! */ + use TYPO3\CMS\Core\Type\TypeInterface; +use TYPO3\CMS\Core\Utility\GeneralUtility; /** * A SPL FileInfo class providing general information related to a file. @@ -23,6 +25,9 @@ class FileInfo extends \SplFileInfo implements TypeInterface /** * Return the mime type of a file. * + * TYPO3 specific settings in $GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType'] take + * precedence over native resolving. + * * @return string|bool Returns the mime type or FALSE if the mime type could not be discovered */ public function getMimeType() @@ -48,7 +53,7 @@ public function getMimeType() 'mimeType' => &$mimeType ]; - \TYPO3\CMS\Core\Utility\GeneralUtility::callUserFunction( + GeneralUtility::callUserFunction( $mimeTypeGuesser, $hookParameters, $this @@ -57,4 +62,40 @@ public function getMimeType() return $mimeType; } + + /** + * Returns the file extensions appropiate for a the MIME type detected in the file. For types that commonly have + * multiple file extensions, such as JPEG images, then the return value is multiple extensions, for instance that + * could be ['jpeg', 'jpg', 'jpe', 'jfif']. For unknown types not available in the magic.mime database + * (/etc/magic.mime, /etc/mime.types, ...), then return value is an empty array. + * + * TYPO3 specific settings in $GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType'] take + * precedence over native resolving. + * + * @return string[] + */ + public function getMimeExtensions(): array + { + $mimeExtensions = []; + if ($this->isFile()) { + $fileExtensionToMimeTypeMapping = $GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType']; + $mimeType = $this->getMimeType(); + if (in_array($mimeType, $fileExtensionToMimeTypeMapping, true)) { + $mimeExtensions = array_keys($fileExtensionToMimeTypeMapping, $mimeType, true); + } elseif (function_exists('finfo_file')) { + $fileInfo = new \finfo(); + $mimeExtensions = array_filter( + GeneralUtility::trimExplode( + '/', + (string)$fileInfo->file($this->getPathname(), FILEINFO_EXTENSION) + ), + function ($item) { + // filter invalid items ('???' is used if not found in magic.mime database) + return $item !== '' && $item !== '???'; + } + ); + } + } + return $mimeExtensions; + } }
typo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.ai+1018 −0 addedtypo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.bmp+0 −0 addedtypo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.eps+7842 −0 addedtypo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.fax+0 −0 addedtypo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.gif+0 −0 addedtypo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.jpg+0 −0 addedtypo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.pdf+0 −0 addedtypo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.png+0 −0 addedtypo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.ps+0 −0 addedtypo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.svg+44 −0 added@@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + version="1.1" + width="481.71875" + height="203.5625" + id="svg7592"> + <defs + id="defs7594" /> + <metadata + id="metadata7597"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + transform="translate(258,-219.15625)" + id="layer1"> + <path + d="m 140.60001,371.50349 c -5.205,0 -12.96125,-1.59375 -13.9175,-1.81 l 0,-7.75125 c 2.55125,0.53 9.135,1.62875 13.8125,1.62875 5.41625,0 8.9225,-4.60625 8.9225,-12.78375 0,-9.66875 -1.59125,-14.7675 -9.135,-14.7675 l -8.7125,0 0,-7.755 7.64875,0 c 8.6075,0 9.03,-8.81875 9.03,-13.0675 0,-8.395 -2.65625,-11.79375 -7.96625,-11.79375 -4.675,0 -9.98875,1.16875 -13.06875,1.80625 l 0,-7.75375 c 1.17,-0.21375 7.43875,-1.80625 12.855,-1.80625 10.94375,0 17.21125,4.67375 17.21125,20.505 0,7.22375 -2.55125,13.59625 -8.18125,15.61625 6.47875,0.42375 9.45375,7.54125 9.45375,17.95375 0,15.82875 -6.15875,21.77875 -17.9525,21.77875 m -48.867497,-68.1 c -9.55875,0 -12.74875,6.48375 -12.74875,29.85375 0,22.8425 3.19,30.49125 12.74875,30.49125 9.561247,0 12.748747,-7.64875 12.748747,-30.49125 0,-23.37 -3.1875,-29.85375 -12.748747,-29.85375 m 0,68.1 c -17.52875,0 -22.205,-12.74875 -22.205,-38.77625 0,-24.9675 4.67625,-37.0775 22.205,-37.0775 17.529997,0 22.202497,12.11 22.202497,37.0775 0,26.0275 -4.6725,38.77625 -22.202497,38.77625 m -52.91,-68.20375 c -5.845,0 -9.98625,0.63625 -9.98625,0.63625 l 0,31.02 9.98625,0 c 5.94875,0 10.0925,-3.93125 10.0925,-15.51 0,-10.625 -2.55,-16.14625 -10.0925,-16.14625 m -1.0625,39.4125 -8.92375,0 0,28.045 -9.2425,0 0,-74.365 c 0,0 9.13625,-0.7425 17.95375,-0.7425 16.14875,0 20.825,9.985 20.825,23.0525 0,16.15 -5.52625,24.01 -20.6125,24.01 m -48.0175,-6.48 0,34.525 -9.56125,0 0,-34.525 -19.015,-39.84 10.19625,0 14.02375,30.065 14.02374986,-30.065 9.66625004,0 -19.3337499,39.84 z m -49.58,-31.76375 0,66.28875 -9.24125,0 0,-66.28875 -16.36125,0 0,-8.07625 41.9625,0 0,8.07625 -16.36,0 z" + id="path5771" + style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none" /> + <path + d="m -129.96886,340.07111 c -1.50125,0.4425 -2.6975,0.595 -4.2625,0.595 -12.84,0 -31.7,-44.87 -31.7,-59.80375 0,-5.50125 1.30625,-7.335 3.1425,-8.90625 -15.7175,1.8325 -34.58,7.5975 -40.6075,14.9325 -1.30875,1.835 -2.095,4.71625 -2.095,8.3825 0,23.3175 24.8875,76.23375 42.44125,76.23375 8.12,0 21.81625,-13.36 33.08125,-31.43375" + id="path5775" + style="fill:#ff8700;fill-opacity:1;fill-rule:nonzero;stroke:none" /> + <path + d="m -138.16461,270.38299 c 16.2425,0 32.4875,2.62 32.4875,11.78875 0,18.60125 -11.78875,41.13125 -17.815,41.13125 -10.74,0 -24.10125,-29.86375 -24.10125,-44.7975 0,-6.81125 2.62,-8.1225 9.42875,-8.1225" + id="path5779" + style="fill:#ff8700;fill-opacity:1;fill-rule:nonzero;stroke:none" /> + </g> +</svg>
typo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.tif+0 −0 addedtypo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.webp+0 −0 addedtypo3/sysext/core/Tests/Functional/Imaging/ImageMagickFileTest.php+353 −0 added@@ -0,0 +1,353 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Core\Tests\Functional\Imaging; + +/* + * 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! + */ + +use org\bovigo\vfs\vfsStream; +use org\bovigo\vfs\vfsStreamDirectory; +use PHPUnit\Framework\MockObject\MockObject; +use TYPO3\CMS\Core\Exception; +use TYPO3\CMS\Core\Imaging\ImageMagickFile; +use TYPO3\CMS\Core\Type\File\FileInfo; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; + +class ImageMagickFileTest extends FunctionalTestCase +{ + /** + * @var vfsStreamDirectory + */ + private $directory; + + protected function setUp() + { + parent::setUp(); + + $fixturePath = __DIR__ . '/Fixtures'; + $structure = []; + $this->addFiles($structure, ['file.ai', 'file.ai.jpg'], $fixturePath . '/file.ai'); + $this->addFiles($structure, ['file.bmp', 'file.bmp.jpg'], $fixturePath . '/file.bmp'); + $this->addFiles($structure, ['file.gif', 'file.gif.jpg'], $fixturePath . '/file.gif'); + $this->addFiles($structure, ['file.fax', 'file.fax.jpg'], $fixturePath . '/file.fax'); + $this->addFiles($structure, ['file.jpg', 'file.jpg.png'], $fixturePath . '/file.jpg'); + $this->addFiles($structure, ['file.png', 'file.png.jpg'], $fixturePath . '/file.png'); + $this->addFiles($structure, ['file.svg', 'file.svg.jpg'], $fixturePath . '/file.svg'); + $this->addFiles($structure, ['file.tif', 'file.tif.jpg'], $fixturePath . '/file.tif'); + $this->addFiles($structure, ['file.webp', 'file.webp.jpg'], $fixturePath . '/file.webp'); + $this->addFiles($structure, ['file.pdf', 'file.pdf.jpg'], $fixturePath . '/file.pdf'); + $this->addFiles($structure, ['file.ps', 'file.ps.jpg'], $fixturePath . '/file.ps'); + $this->addFiles($structure, ['file.eps', 'file.eps.jpg'], $fixturePath . '/file.eps'); + $this->directory = vfsStream::setup('root', null, $structure); + } + + protected function tearDown() + { + unset($this->directory); + parent::tearDown(); + } + + /** + * @return array + */ + public function framesAreConsideredDataProvider(): array + { + return [ + 'file.pdf' => ['file.pdf', null, '\'pdf:{directory}/file.pdf\''], + 'file.pdf[0]' => ['file.pdf', 0, '\'pdf:{directory}/file.pdf[0]\''], + ]; + } + + /** + * @param string $fileName + * @param int|null $frame + * @param string $expectation + * + * @test + * @dataProvider framesAreConsideredDataProvider + */ + public function framesAreConsidered(string $fileName, ?int $frame, string $expectation) + { + $expectation = $this->substituteVariables($expectation); + $filePath = sprintf('%s/%s', $this->directory->url(), $fileName); + $file = ImageMagickFile::fromFilePath($filePath, $frame); + self::assertSame($expectation, (string)$file); + } + + /** + * @return array + */ + public function resultIsEscapedDataProvider(): array + { + // probably Windows system + if (DIRECTORY_SEPARATOR === '\\') { + return [ + 'without frame' => ['file.pdf', null, '"pdf:{directory}/file.pdf"'], + 'with first frame' => ['file.pdf', 0, '"pdf:{directory}/file.pdf[0]"'], + 'special literals' => ['\'`%$!".png', 0, '"png:{directory}/\'` $ .png[0]"'], + ]; + } + // probably Unix system + return [ + 'without frame' => ['file.pdf', null, '\'pdf:{directory}/file.pdf\''], + 'with first frame' => ['file.pdf', 0, '\'pdf:{directory}/file.pdf[0]\''], + 'special literals' => ['\'`%$!".png', 0, '\'png:{directory}/\'\\\'\'`%$!".png[0]\''], + ]; + } + + /** + * @param string $fileName + * @param int|null $frame + * @param string $expectation + * + * @test + * @dataProvider resultIsEscapedDataProvider + */ + public function resultIsEscaped(string $fileName, ?int $frame, string $expectation) + { + $expectation = $this->substituteVariables($expectation); + $filePath = sprintf('%s/%s', $this->directory->url(), $fileName); + $file = ImageMagickFile::fromFilePath($filePath, $frame); + self::assertSame($expectation, (string)$file); + } + + /** + * @return array + */ + public function fileStatementIsResolvedDataProvider(): array + { + return [ + 'file.ai' => ['file.ai', '\'pdf:{directory}/file.ai\''], + 'file.ai.jpg' => ['file.ai.jpg', '\'pdf:{directory}/file.ai.jpg\''], + 'file.gif' => ['file.gif', '\'gif:{directory}/file.gif\''], + 'file.gif.jpg' => ['file.gif.jpg', '\'gif:{directory}/file.gif.jpg\''], + 'file.jpg' => ['file.jpg', '\'jpg:{directory}/file.jpg\''], + 'file.jpg.png' => ['file.jpg.png', '\'jpg:{directory}/file.jpg.png\''], + 'file.png' => ['file.png', '\'png:{directory}/file.png\''], + 'file.png.jpg' => ['file.png.jpg', '\'png:{directory}/file.png.jpg\''], + 'file.svg' => ['file.svg', '\'svg:{directory}/file.svg\''], + 'file.svg.jpg' => ['file.svg.jpg', '\'svg:{directory}/file.svg.jpg\''], + 'file.tif' => ['file.tif', '\'tif:{directory}/file.tif\''], + 'file.tif.jpg' => ['file.tif.jpg', '\'tif:{directory}/file.tif.jpg\''], + 'file.webp' => ['file.webp', '\'webp:{directory}/file.webp\''], + 'file.webp.jpg' => ['file.webp.jpg', '\'webp:{directory}/file.webp.jpg\''], + 'file.pdf' => ['file.pdf', '\'pdf:{directory}/file.pdf\''], + 'file.pdf.jpg' => ['file.pdf.jpg', '\'pdf:{directory}/file.pdf.jpg\''], + // accepted, since postscript files are converted using 'jpg:' format + 'file.ps.jpg' => ['file.ps.jpg', '\'jpg:{directory}/file.ps.jpg\''], + 'file.eps.jpg' => ['file.eps.jpg', '\'jpg:{directory}/file.eps.jpg\''], + ]; + } + + /** + * @param string $fileName + * @param string $expectation + * + * @test + * @dataProvider fileStatementIsResolvedDataProvider + */ + public function fileStatementIsResolved(string $fileName, string $expectation) + { + $expectation = $this->substituteVariables($expectation); + $filePath = sprintf('%s/%s', $this->directory->url(), $fileName); + $file = ImageMagickFile::fromFilePath($filePath, null); + self::assertSame($expectation, (string)$file); + } + + /** + * In case mime-types cannot be resolved (or cannot be verified), allowed extensions + * are used as conversion format (e.g. 'file.ai.jpg' -> 'jpg:...'). + * + * @return array + */ + public function fileStatementIsResolvedForEnforcedMimeTypeDataProvider(): array + { + return [ + 'file.ai.jpg' => ['file.ai.jpg', '\'jpg:{directory}/file.ai.jpg\'', 'inode/x-empty'], + 'file.bmp.jpg' => ['file.bmp.jpg', '\'jpg:{directory}/file.bmp.jpg\'', 'inode/x-empty'], + 'file.fax.jpg' => ['file.fax.jpg', '\'jpg:{directory}/file.fax.jpg\'', 'inode/x-empty'], + 'file.gif.jpg' => ['file.gif.jpg', '\'jpg:{directory}/file.gif.jpg\'', 'inode/x-empty'], + 'file.jpg' => ['file.jpg', '\'jpg:{directory}/file.jpg\'', 'inode/x-empty'], + 'file.jpg.png' => ['file.jpg.png', '\'png:{directory}/file.jpg.png\'', 'inode/x-empty'], + 'file.png' => ['file.png', '\'png:{directory}/file.png\'', 'inode/x-empty'], + 'file.png.jpg' => ['file.png.jpg', '\'jpg:{directory}/file.png.jpg\'', 'inode/x-empty'], + 'file.svg.jpg' => ['file.svg.jpg', '\'jpg:{directory}/file.svg.jpg\'', 'inode/x-empty'], + 'file.tif' => ['file.tif', '\'tif:{directory}/file.tif\'', 'inode/x-empty'], + 'file.tif.jpg' => ['file.tif.jpg', '\'jpg:{directory}/file.tif.jpg\'', 'inode/x-empty'], + 'file.webp' => ['file.webp', '\'webp:{directory}/file.webp\'', 'inode/x-empty'], + 'file.webp.jpg' => ['file.webp.jpg', '\'jpg:{directory}/file.webp.jpg\'', 'inode/x-empty'], + 'file.pdf.jpg' => ['file.pdf.jpg', '\'jpg:{directory}/file.pdf.jpg\'', 'inode/x-empty'], + ]; + } + + /** + * @param string $fileName + * @param string $expectation + * @param string $mimeType + * + * @test + * @dataProvider fileStatementIsResolvedForEnforcedMimeTypeDataProvider + */ + public function fileStatementIsResolvedForEnforcedMimeType(string $fileName, string $expectation, string $mimeType) + { + $this->simulateNextFileInfoInvocation($mimeType); + $expectation = $this->substituteVariables($expectation); + $filePath = sprintf('%s/%s', $this->directory->url(), $fileName); + $file = ImageMagickFile::fromFilePath($filePath, null); + self::assertSame($expectation, (string)$file); + } + + /** + * @return array + */ + public function fileStatementIsResolvedForConfiguredMimeTypeDataProvider(): array + { + return [ + 'file.fax' => ['file.fax', '\'g3:{directory}/file.fax\''], + 'file.bmp' => ['file.bmp', '\'dib:{directory}/file.bmp\''], + ]; + } + + /** + * @param string $fileName + * @param string $expectation + * + * @test + * @dataProvider fileStatementIsResolvedForConfiguredMimeTypeDataProvider + */ + public function fileStatementIsResolvedForConfiguredMimeType(string $fileName, string $expectation) + { + $GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType']['g3'] = 'image/g3fax'; + $GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType']['fax'] = 'image/g3fax'; + $GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType']['dib'] = 'image/x-ms-bmp'; + $GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType']['bmp'] = 'image/x-ms-bmp'; + + $expectation = $this->substituteVariables($expectation); + $filePath = sprintf('%s/%s', $this->directory->url(), $fileName); + $file = ImageMagickFile::fromFilePath($filePath, null); + self::assertSame($expectation, (string)$file); + } + + /** + * @return array + */ + public function fileStatementIsDeniedDataProvider(): array + { + return [ + 'file.ps' => ['file.ps'], + 'file.eps' => ['file.eps'], + // denied since not defined in allowed extensions + 'file.ai' => ['file.ai', 'inode/x-empty'], + 'file.svg' => ['file.svg', 'inode/x-empty'], + 'file.pdf' => ['file.pdf', 'inode/x-empty'], + ]; + } + + /** + * @param string $fileName + * @param string|null $mimeType + * + * @test + * @dataProvider fileStatementIsDeniedDataProvider + */ + public function fileStatementIsDenied(string $fileName, string $mimeType = null) + { + self::expectException(Exception::class); + self::expectExceptionCode(1550060977); + + if ($mimeType !== null) { + $this->simulateNextFileInfoInvocation($mimeType); + } + + $filePath = sprintf('%s/%s', $this->directory->url(), $fileName); + ImageMagickFile::fromFilePath($filePath, null); + } + + /** + * @return array + */ + public function fileStatementIsDeniedForConfiguredMimeTypeDataProvider(): array + { + return [ + 'file.ps' => ['file.ps'], + 'file.eps' => ['file.eps'], + ]; + } + + /** + * @param string $fileName + * + * @test + * @dataProvider fileStatementIsDeniedForConfiguredMimeTypeDataProvider + */ + public function fileStatementIsDeniedForConfiguredMimeType(string $fileName) + { + $GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType']['ps'] = 'image/x-see-no-evil'; + $GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType']['eps'] = 'image/x-see-no-evil'; + + self::expectException(Exception::class); + self::expectExceptionCode(1550060977); + + $filePath = sprintf('%s/%s', $this->directory->url(), $fileName); + ImageMagickFile::fromFilePath($filePath, null); + } + + /** + * @param array $structure + * @param array $fileNames + * @param string $sourcePath + */ + private function addFiles(array &$structure, array $fileNames, string $sourcePath): void + { + $structure = array_merge( + $structure, + array_fill_keys( + $fileNames, + file_get_contents($sourcePath) + ) + ); + } + + /** + * @param string $value + * @return string + */ + private function substituteVariables(string $value): string + { + return str_replace( + ['{directory}'], + [$this->directory->url()], + $value + ); + } + + /** + * @param string $mimeType + * @param string[] $mimeExtensions + */ + private function simulateNextFileInfoInvocation(string $mimeType, array $mimeExtensions = []) + { + /** @var FileInfo|MockObject $fileInfo */ + $fileInfo = $this->getAccessibleMock( + FileInfo::class, + ['getMimeType', 'getMimeExtensions'], + [], + '', + false + ); + $fileInfo->expects(self::atLeastOnce())->method('getMimeType')->willReturn($mimeType); + $fileInfo->expects(self::atLeastOnce())->method('getMimeExtensions')->willReturn($mimeExtensions); + GeneralUtility::addInstance(FileInfo::class, $fileInfo); + } +}
51fdb774a57e[SECURITY] Enclose file type scope when invoking ImageMagick
18 files changed · +9547 −18
typo3/sysext/core/Classes/Imaging/GraphicalFunctions.php+17 −9 modified@@ -2476,8 +2476,11 @@ public function imageMagickIdentify($imagefile) return null; } - $frame = $this->addFrameSelection ? '[0]' : ''; - $cmd = CommandUtility::imageMagickCommand('identify', CommandUtility::escapeShellArgument($imagefile) . $frame); + $frame = $this->addFrameSelection ? 0 : null; + $cmd = CommandUtility::imageMagickCommand( + 'identify', + ImageMagickFile::fromFilePath($imagefile, $frame) + ); $returnVal = []; CommandUtility::exec($cmd, $returnVal); $splitstring = array_pop($returnVal); @@ -2520,8 +2523,13 @@ public function imageMagickExec($input, $output, $params, $frame = 0) } // If addFrameSelection is set in the Install Tool, a frame number is added to // select a specific page of the image (by default this will be the first page) - $frame = $this->addFrameSelection ? '[' . (int)$frame . ']' : ''; - $cmd = CommandUtility::imageMagickCommand('convert', $params . ' ' . CommandUtility::escapeShellArgument($input . $frame) . ' ' . CommandUtility::escapeShellArgument($output)); + $frame = $this->addFrameSelection ? (int)$frame : null; + $cmd = CommandUtility::imageMagickCommand( + 'convert', + $params + . ' ' . ImageMagickFile::fromFilePath($input, $frame) + . ' ' . CommandUtility::escapeShellArgument($output) + ); $this->IM_commands[] = [$output, $cmd]; $ret = CommandUtility::exec($cmd); // Change the permissions of the file @@ -2549,9 +2557,9 @@ public function combineExec($input, $overlay, $mask, $output) $this->imageMagickExec($mask, $theMask, '-colorspace GRAY +matte'); $parameters = '-compose over +matte ' - . CommandUtility::escapeShellArgument($input) . ' ' - . CommandUtility::escapeShellArgument($overlay) . ' ' - . CommandUtility::escapeShellArgument($theMask) . ' ' + . ImageMagickFile::fromFilePath($input) . ' ' + . ImageMagickFile::fromFilePath($overlay) . ' ' + . ImageMagickFile::fromFilePath($theMask) . ' ' . CommandUtility::escapeShellArgument($output); $cmd = CommandUtility::imageMagickCommand('combine', $parameters); $this->IM_commands[] = [$output, $cmd]; @@ -2596,7 +2604,7 @@ public static function gifCompress($theFile, $type) if (@rename($theFile, $temporaryName)) { $cmd = CommandUtility::imageMagickCommand( 'convert', - implode(' ', CommandUtility::escapeShellArguments([$temporaryName, $theFile])), + ImageMagickFile::fromFilePath($temporaryName) . ' ' . CommandUtility::escapeShellArgument($theFile), $gfxConf['processor_path_lzw'] ); CommandUtility::exec($cmd); @@ -2646,7 +2654,7 @@ public static function readPngGif($theFile, $output_png = false) $newFile = PATH_site . 'typo3temp/assets/images/' . md5($theFile . '|' . filemtime($theFile)) . ($output_png ? '.png' : '.gif'); $cmd = CommandUtility::imageMagickCommand( 'convert', - implode(' ', CommandUtility::escapeShellArguments([$theFile, $newFile])), + ImageMagickFile::fromFilePath($theFile) . ' ' . CommandUtility::escapeShellArgument($newFile), $GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_path'] ); CommandUtility::exec($cmd);
typo3/sysext/core/Classes/Imaging/ImageMagickFile.php+223 −0 added@@ -0,0 +1,223 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Core\Imaging; + +/* + * 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! + */ + +use TYPO3\CMS\Core\Exception; +use TYPO3\CMS\Core\Type\File\FileInfo; +use TYPO3\CMS\Core\Utility\CommandUtility; +use TYPO3\CMS\Core\Utility\GeneralUtility; + +/** + * Value object for file to be used for ImageMagick/GraphicsMagick invocation when + * being used as input file (implies and requires that file exists for some evaluations). + */ +class ImageMagickFile +{ + /** + * Path to input file to be processed + * + * @var string + */ + protected $filePath; + + /** + * Frame to be used (of multi-page document, e.g. PDF) + * + * @var int|null + */ + protected $frame; + + /** + * Whether file actually exists + * + * @var bool + */ + protected $fileExists; + + /** + * File extension as given in $filePath (e.g. 'file.png' -> 'png') + * + * @var string + */ + protected $fileExtension; + + /** + * Resolved mime-type of file + * + * @var string + */ + protected $mimeType; + + /** + * Resolved extension for mime-type (e.g. 'image/png' -> 'png') + * (might be empty if not defined in magic.mime database) + * + * @var string[] + * @see FileInfo::getMimeExtensions() + */ + protected $mimeExtensions = []; + + /** + * Result to be used for ImageMagick/GraphicsMagick invocation containing + * combination of resolved format prefix, $filePath and frame escaped to be + * used as CLI argument (e.g. "'png:file.png'") + * + * @var string + */ + protected $asArgument; + + /** + * File extensions that directly can be used (and are considered to be safe). + * + * @var string[] + */ + protected $allowedExtensions = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'tif', 'tiff', 'bmp', 'pcx', 'tga', 'ico']; + + /** + * File extensions that never shall be used. + * + * @var string[] + */ + protected $deniedExtensions = ['epi', 'eps', 'eps2', 'eps3', 'epsf', 'epsi', 'ept', 'ept2', 'ept3', 'msl', 'ps', 'ps2', 'ps3']; + + /** + * File mime-types that have to be matching. Adding custom mime-types is possible using + * $GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType'] + * + * @var string[] + * @see FileInfo::getMimeExtensions() + */ + protected $mimeTypeExtensionMap = [ + 'image/png' => 'png', + 'image/jpeg' => 'jpg', + 'image/gif' => 'gif', + 'image/heic' => 'heic', + 'image/heif' => 'heif', + 'image/webp' => 'webp', + 'image/svg' => 'svg', + 'image/svg+xml' => 'svg', + 'image/tiff' => 'tif', + 'application/pdf' => 'pdf', + ]; + + /** + * @param string $filePath + * @param int|null $frame + * @return ImageMagickFile + */ + public static function fromFilePath(string $filePath, int $frame = null): self + { + return GeneralUtility::makeInstance( + static::class, + $filePath, + $frame + ); + } + + /** + * @param string $filePath + * @param int|null $frame + * @throws Exception + */ + public function __construct(string $filePath, int $frame = null) + { + $this->frame = $frame; + $this->fileExists = file_exists($filePath); + $this->filePath = $filePath; + $this->fileExtension = pathinfo($filePath, PATHINFO_EXTENSION); + + if ($this->fileExists) { + $fileInfo = $this->getFileInfo($filePath); + $this->mimeType = $fileInfo->getMimeType(); + $this->mimeExtensions = $fileInfo->getMimeExtensions(); + } + + $this->asArgument = $this->escape( + $this->resolvePrefix() . $this->filePath + . ($this->frame !== null ? '[' . $this->frame . ']' : '') + ); + } + + /** + * @return string + */ + public function __toString(): string + { + return $this->asArgument; + } + + /** + * Resolves according ImageMagic/GraphicsMagic format (e.g. 'png:', 'jpg:', ...). + * + in case mime-type could be resolved and is configured, it takes precedence + * + otherwise resolved mime-type extension of mime.magick database is used if available + * (includes custom settings with $GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType']) + * + otherwise "safe" and allowed file extension is used (jpg, png, gif, webp, tif, ...) + * + potentially malicious script formats (eps, ps, ...) are not allowed + * + * @return string + * @throws Exception + */ + protected function resolvePrefix(): string + { + $prefixExtension = null; + $fileExtension = strtolower($this->fileExtension); + if ($this->mimeType !== null && !empty($this->mimeTypeExtensionMap[$this->mimeType])) { + $prefixExtension = $this->mimeTypeExtensionMap[$this->mimeType]; + } elseif (!empty($this->mimeExtensions) && strpos((string)$this->mimeType, 'image/') === 0) { + $prefixExtension = $this->mimeExtensions[0]; + } elseif ($this->isInAllowedExtensions($fileExtension)) { + $prefixExtension = $fileExtension; + } + if ($prefixExtension !== null && !in_array(strtolower($prefixExtension), $this->deniedExtensions, true)) { + return $prefixExtension . ':'; + } + throw new Exception( + sprintf( + 'Unsupported file %s (%s)', + basename($this->filePath), + $this->mimeType ?? 'unknown' + ), + 1550060977 + ); + } + + /** + * @param string $value + * @return string + */ + protected function escape(string $value): string + { + return CommandUtility::escapeShellArgument($value); + } + + /** + * @param string $extension + * @return bool + */ + protected function isInAllowedExtensions(string $extension): bool + { + return in_array($extension, $this->allowedExtensions, true); + } + + /** + * @param string $filePath + * @return FileInfo + */ + protected function getFileInfo(string $filePath): FileInfo + { + return GeneralUtility::makeInstance(FileInfo::class, $filePath); + } +}
typo3/sysext/core/Classes/Resource/OnlineMedia/Processing/PreviewProcessing.php+4 −4 modified@@ -15,6 +15,7 @@ */ use TYPO3\CMS\Core\Imaging\GraphicalFunctions; +use TYPO3\CMS\Core\Imaging\ImageMagickFile; use TYPO3\CMS\Core\Resource\Driver\DriverInterface; use TYPO3\CMS\Core\Resource\File; use TYPO3\CMS\Core\Resource\OnlineMedia\Helpers\OnlineMediaHelperRegistry; @@ -136,11 +137,10 @@ protected function resizeImage($originalFileName, $temporaryFileName, $configura $arguments = CommandUtility::escapeShellArguments([ 'width' => $configuration['width'], 'height' => $configuration['height'], - 'originalFileName' => $originalFileName, - 'temporaryFileName' => $temporaryFileName, ]); - $parameters = '-sample ' . $arguments['width'] . 'x' . $arguments['height'] . ' ' - . $arguments['originalFileName'] . '[0] ' . $arguments['temporaryFileName']; + $parameters = '-sample ' . $arguments['width'] . 'x' . $arguments['height'] + . ' ' . ImageMagickFile::fromFilePath($originalFileName, 0) + . ' ' . CommandUtility::escapeShellArgument($temporaryFileName); $cmd = CommandUtility::imageMagickCommand('convert', $parameters) . ' 2>&1'; CommandUtility::exec($cmd);
typo3/sysext/core/Classes/Resource/Processing/LocalPreviewHelper.php+4 −4 modified@@ -15,6 +15,7 @@ */ use TYPO3\CMS\Core\Imaging\GraphicalFunctions; +use TYPO3\CMS\Core\Imaging\ImageMagickFile; use TYPO3\CMS\Core\Resource\File; use TYPO3\CMS\Core\Utility\CommandUtility; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -154,11 +155,10 @@ protected function generatePreviewFromFile(File $file, array $configuration, $ta $arguments = CommandUtility::escapeShellArguments([ 'width' => $configuration['width'], 'height' => $configuration['height'], - 'originalFileName' => $originalFileName, - 'targetFilePath' => $targetFilePath, ]); - $parameters = '-sample ' . $arguments['width'] . 'x' . $arguments['height'] . ' ' - . $arguments['originalFileName'] . '[0] ' . $arguments['targetFilePath']; + $parameters = '-sample ' . $arguments['width'] . 'x' . $arguments['height'] + . ' ' . ImageMagickFile::fromFilePath($originalFileName, 0) + . ' ' . CommandUtility::escapeShellArgument($targetFilePath); $cmd = CommandUtility::imageMagickCommand('convert', $parameters) . ' 2>&1'; CommandUtility::exec($cmd);
typo3/sysext/core/Classes/Type/File/FileInfo.php+43 −1 modified@@ -13,7 +13,9 @@ * * The TYPO3 project - inspiring people to share! */ + use TYPO3\CMS\Core\Type\TypeInterface; +use TYPO3\CMS\Core\Utility\GeneralUtility; /** * A SPL FileInfo class providing general information related to a file. @@ -23,6 +25,9 @@ class FileInfo extends \SplFileInfo implements TypeInterface /** * Return the mime type of a file. * + * TYPO3 specific settings in $GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType'] take + * precedence over native resolving. + * * @return string|bool Returns the mime type or FALSE if the mime type could not be discovered */ public function getMimeType() @@ -51,7 +56,7 @@ public function getMimeType() 'mimeType' => &$mimeType ]; - \TYPO3\CMS\Core\Utility\GeneralUtility::callUserFunction( + GeneralUtility::callUserFunction( $mimeTypeGuesser, $hookParameters, $this @@ -61,4 +66,41 @@ public function getMimeType() return $mimeType; } + + /** + * Returns the file extensions appropiate for a the MIME type detected in the file. For types that commonly have + * multiple file extensions, such as JPEG images, then the return value is multiple extensions, for instance that + * could be ['jpeg', 'jpg', 'jpe', 'jfif']. For unknown types not available in the magic.mime database + * (/etc/magic.mime, /etc/mime.types, ...), then return value is an empty array. + * + * TYPO3 specific settings in $GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType'] take + * precedence over native resolving. + * + * @return string[] + */ + public function getMimeExtensions(): array + { + $mimeExtensions = []; + if ($this->isFile()) { + $fileExtensionToMimeTypeMapping = $GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType']; + $mimeType = $this->getMimeType(); + if (in_array($mimeType, $fileExtensionToMimeTypeMapping, true)) { + $mimeExtensions = array_keys($fileExtensionToMimeTypeMapping, $mimeType, true); + // extraction using magic.mime database with FILEINFO_EXTENSION was introduced in PHP 7.2.0 + } elseif (function_exists('finfo_file') && defined('FILEINFO_EXTENSION')) { + $fileInfo = new \finfo(); + $mimeExtensions = array_filter( + GeneralUtility::trimExplode( + '/', + (string)$fileInfo->file($this->getPathname(), FILEINFO_EXTENSION) + ), + function ($item) { + // filter invalid items ('???' is used if not found in magic.mime database) + return $item !== '' && $item !== '???'; + } + ); + } + } + return $mimeExtensions; + } }
typo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.ai+1018 −0 addedtypo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.bmp+0 −0 addedtypo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.eps+7842 −0 addedtypo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.fax+0 −0 addedtypo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.gif+0 −0 addedtypo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.jpg+0 −0 addedtypo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.pdf+0 −0 addedtypo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.png+0 −0 addedtypo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.ps+0 −0 addedtypo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.svg+44 −0 added@@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + version="1.1" + width="481.71875" + height="203.5625" + id="svg7592"> + <defs + id="defs7594" /> + <metadata + id="metadata7597"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + transform="translate(258,-219.15625)" + id="layer1"> + <path + d="m 140.60001,371.50349 c -5.205,0 -12.96125,-1.59375 -13.9175,-1.81 l 0,-7.75125 c 2.55125,0.53 9.135,1.62875 13.8125,1.62875 5.41625,0 8.9225,-4.60625 8.9225,-12.78375 0,-9.66875 -1.59125,-14.7675 -9.135,-14.7675 l -8.7125,0 0,-7.755 7.64875,0 c 8.6075,0 9.03,-8.81875 9.03,-13.0675 0,-8.395 -2.65625,-11.79375 -7.96625,-11.79375 -4.675,0 -9.98875,1.16875 -13.06875,1.80625 l 0,-7.75375 c 1.17,-0.21375 7.43875,-1.80625 12.855,-1.80625 10.94375,0 17.21125,4.67375 17.21125,20.505 0,7.22375 -2.55125,13.59625 -8.18125,15.61625 6.47875,0.42375 9.45375,7.54125 9.45375,17.95375 0,15.82875 -6.15875,21.77875 -17.9525,21.77875 m -48.867497,-68.1 c -9.55875,0 -12.74875,6.48375 -12.74875,29.85375 0,22.8425 3.19,30.49125 12.74875,30.49125 9.561247,0 12.748747,-7.64875 12.748747,-30.49125 0,-23.37 -3.1875,-29.85375 -12.748747,-29.85375 m 0,68.1 c -17.52875,0 -22.205,-12.74875 -22.205,-38.77625 0,-24.9675 4.67625,-37.0775 22.205,-37.0775 17.529997,0 22.202497,12.11 22.202497,37.0775 0,26.0275 -4.6725,38.77625 -22.202497,38.77625 m -52.91,-68.20375 c -5.845,0 -9.98625,0.63625 -9.98625,0.63625 l 0,31.02 9.98625,0 c 5.94875,0 10.0925,-3.93125 10.0925,-15.51 0,-10.625 -2.55,-16.14625 -10.0925,-16.14625 m -1.0625,39.4125 -8.92375,0 0,28.045 -9.2425,0 0,-74.365 c 0,0 9.13625,-0.7425 17.95375,-0.7425 16.14875,0 20.825,9.985 20.825,23.0525 0,16.15 -5.52625,24.01 -20.6125,24.01 m -48.0175,-6.48 0,34.525 -9.56125,0 0,-34.525 -19.015,-39.84 10.19625,0 14.02375,30.065 14.02374986,-30.065 9.66625004,0 -19.3337499,39.84 z m -49.58,-31.76375 0,66.28875 -9.24125,0 0,-66.28875 -16.36125,0 0,-8.07625 41.9625,0 0,8.07625 -16.36,0 z" + id="path5771" + style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none" /> + <path + d="m -129.96886,340.07111 c -1.50125,0.4425 -2.6975,0.595 -4.2625,0.595 -12.84,0 -31.7,-44.87 -31.7,-59.80375 0,-5.50125 1.30625,-7.335 3.1425,-8.90625 -15.7175,1.8325 -34.58,7.5975 -40.6075,14.9325 -1.30875,1.835 -2.095,4.71625 -2.095,8.3825 0,23.3175 24.8875,76.23375 42.44125,76.23375 8.12,0 21.81625,-13.36 33.08125,-31.43375" + id="path5775" + style="fill:#ff8700;fill-opacity:1;fill-rule:nonzero;stroke:none" /> + <path + d="m -138.16461,270.38299 c 16.2425,0 32.4875,2.62 32.4875,11.78875 0,18.60125 -11.78875,41.13125 -17.815,41.13125 -10.74,0 -24.10125,-29.86375 -24.10125,-44.7975 0,-6.81125 2.62,-8.1225 9.42875,-8.1225" + id="path5779" + style="fill:#ff8700;fill-opacity:1;fill-rule:nonzero;stroke:none" /> + </g> +</svg>
typo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.tif+0 −0 addedtypo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.webp+0 −0 addedtypo3/sysext/core/Tests/Functional/Imaging/ImageMagickFileTest.php+352 −0 added@@ -0,0 +1,352 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Core\Tests\Functional\Imaging; + +/* + * 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! + */ + +use org\bovigo\vfs\vfsStream; +use org\bovigo\vfs\vfsStreamDirectory; +use TYPO3\CMS\Core\Exception; +use TYPO3\CMS\Core\Imaging\ImageMagickFile; +use TYPO3\CMS\Core\Type\File\FileInfo; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; + +class ImageMagickFileTest extends FunctionalTestCase +{ + /** + * @var vfsStreamDirectory + */ + private $directory; + + protected function setUp() + { + parent::setUp(); + + $fixturePath = __DIR__ . '/Fixtures'; + $structure = []; + $this->addFiles($structure, ['file.ai', 'file.ai.jpg'], $fixturePath . '/file.ai'); + $this->addFiles($structure, ['file.bmp', 'file.bmp.jpg'], $fixturePath . '/file.bmp'); + $this->addFiles($structure, ['file.gif', 'file.gif.jpg'], $fixturePath . '/file.gif'); + $this->addFiles($structure, ['file.fax', 'file.fax.jpg'], $fixturePath . '/file.fax'); + $this->addFiles($structure, ['file.jpg', 'file.jpg.png'], $fixturePath . '/file.jpg'); + $this->addFiles($structure, ['file.png', 'file.png.jpg'], $fixturePath . '/file.png'); + $this->addFiles($structure, ['file.svg', 'file.svg.jpg'], $fixturePath . '/file.svg'); + $this->addFiles($structure, ['file.tif', 'file.tif.jpg'], $fixturePath . '/file.tif'); + $this->addFiles($structure, ['file.webp', 'file.webp.jpg'], $fixturePath . '/file.webp'); + $this->addFiles($structure, ['file.pdf', 'file.pdf.jpg'], $fixturePath . '/file.pdf'); + $this->addFiles($structure, ['file.ps', 'file.ps.jpg'], $fixturePath . '/file.ps'); + $this->addFiles($structure, ['file.eps', 'file.eps.jpg'], $fixturePath . '/file.eps'); + $this->directory = vfsStream::setup('root', null, $structure); + } + + protected function tearDown() + { + unset($this->directory); + parent::tearDown(); + } + + /** + * @return array + */ + public function framesAreConsideredDataProvider(): array + { + return [ + 'file.pdf' => ['file.pdf', null, '\'pdf:{directory}/file.pdf\''], + 'file.pdf[0]' => ['file.pdf', 0, '\'pdf:{directory}/file.pdf[0]\''], + ]; + } + + /** + * @param string $fileName + * @param int|null $frame + * @param string $expectation + * + * @test + * @dataProvider framesAreConsideredDataProvider + */ + public function framesAreConsidered(string $fileName, $frame, string $expectation) + { + $expectation = $this->substituteVariables($expectation); + $filePath = sprintf('%s/%s', $this->directory->url(), $fileName); + $file = ImageMagickFile::fromFilePath($filePath, $frame); + self::assertSame($expectation, (string)$file); + } + + /** + * @return array + */ + public function resultIsEscapedDataProvider(): array + { + // probably Windows system + if (DIRECTORY_SEPARATOR === '\\') { + return [ + 'without frame' => ['file.pdf', null, '"pdf:{directory}/file.pdf"'], + 'with first frame' => ['file.pdf', 0, '"pdf:{directory}/file.pdf[0]"'], + 'special literals' => ['\'`%$!".png', 0, '"png:{directory}/\'` $ .png[0]"'], + ]; + } + // probably Unix system + return [ + 'without frame' => ['file.pdf', null, '\'pdf:{directory}/file.pdf\''], + 'with first frame' => ['file.pdf', 0, '\'pdf:{directory}/file.pdf[0]\''], + 'special literals' => ['\'`%$!".png', 0, '\'png:{directory}/\'\\\'\'`%$!".png[0]\''], + ]; + } + + /** + * @param string $fileName + * @param int|null $frame + * @param string $expectation + * + * @test + * @dataProvider resultIsEscapedDataProvider + */ + public function resultIsEscaped(string $fileName, $frame, string $expectation) + { + $expectation = $this->substituteVariables($expectation); + $filePath = sprintf('%s/%s', $this->directory->url(), $fileName); + $file = ImageMagickFile::fromFilePath($filePath, $frame); + self::assertSame($expectation, (string)$file); + } + + /** + * @return array + */ + public function fileStatementIsResolvedDataProvider(): array + { + return [ + 'file.ai' => ['file.ai', '\'pdf:{directory}/file.ai\''], + 'file.ai.jpg' => ['file.ai.jpg', '\'pdf:{directory}/file.ai.jpg\''], + 'file.gif' => ['file.gif', '\'gif:{directory}/file.gif\''], + 'file.gif.jpg' => ['file.gif.jpg', '\'gif:{directory}/file.gif.jpg\''], + 'file.jpg' => ['file.jpg', '\'jpg:{directory}/file.jpg\''], + 'file.jpg.png' => ['file.jpg.png', '\'jpg:{directory}/file.jpg.png\''], + 'file.png' => ['file.png', '\'png:{directory}/file.png\''], + 'file.png.jpg' => ['file.png.jpg', '\'png:{directory}/file.png.jpg\''], + 'file.svg' => ['file.svg', '\'svg:{directory}/file.svg\''], + 'file.svg.jpg' => ['file.svg.jpg', '\'svg:{directory}/file.svg.jpg\''], + 'file.tif' => ['file.tif', '\'tif:{directory}/file.tif\''], + 'file.tif.jpg' => ['file.tif.jpg', '\'tif:{directory}/file.tif.jpg\''], + 'file.webp' => ['file.webp', '\'webp:{directory}/file.webp\''], + 'file.webp.jpg' => ['file.webp.jpg', '\'webp:{directory}/file.webp.jpg\''], + 'file.pdf' => ['file.pdf', '\'pdf:{directory}/file.pdf\''], + 'file.pdf.jpg' => ['file.pdf.jpg', '\'pdf:{directory}/file.pdf.jpg\''], + // accepted, since postscript files are converted using 'jpg:' format + 'file.ps.jpg' => ['file.ps.jpg', '\'jpg:{directory}/file.ps.jpg\''], + 'file.eps.jpg' => ['file.eps.jpg', '\'jpg:{directory}/file.eps.jpg\''], + ]; + } + + /** + * @param string $fileName + * @param string $expectation + * + * @test + * @dataProvider fileStatementIsResolvedDataProvider + */ + public function fileStatementIsResolved(string $fileName, string $expectation) + { + $expectation = $this->substituteVariables($expectation); + $filePath = sprintf('%s/%s', $this->directory->url(), $fileName); + $file = ImageMagickFile::fromFilePath($filePath, null); + self::assertSame($expectation, (string)$file); + } + + /** + * In case mime-types cannot be resolved (or cannot be verified), allowed extensions + * are used as conversion format (e.g. 'file.ai.jpg' -> 'jpg:...'). + * + * @return array + */ + public function fileStatementIsResolvedForEnforcedMimeTypeDataProvider(): array + { + return [ + 'file.ai.jpg' => ['file.ai.jpg', '\'jpg:{directory}/file.ai.jpg\'', 'inode/x-empty'], + 'file.bmp.jpg' => ['file.bmp.jpg', '\'jpg:{directory}/file.bmp.jpg\'', 'inode/x-empty'], + 'file.fax.jpg' => ['file.fax.jpg', '\'jpg:{directory}/file.fax.jpg\'', 'inode/x-empty'], + 'file.gif.jpg' => ['file.gif.jpg', '\'jpg:{directory}/file.gif.jpg\'', 'inode/x-empty'], + 'file.jpg' => ['file.jpg', '\'jpg:{directory}/file.jpg\'', 'inode/x-empty'], + 'file.jpg.png' => ['file.jpg.png', '\'png:{directory}/file.jpg.png\'', 'inode/x-empty'], + 'file.png' => ['file.png', '\'png:{directory}/file.png\'', 'inode/x-empty'], + 'file.png.jpg' => ['file.png.jpg', '\'jpg:{directory}/file.png.jpg\'', 'inode/x-empty'], + 'file.svg.jpg' => ['file.svg.jpg', '\'jpg:{directory}/file.svg.jpg\'', 'inode/x-empty'], + 'file.tif' => ['file.tif', '\'tif:{directory}/file.tif\'', 'inode/x-empty'], + 'file.tif.jpg' => ['file.tif.jpg', '\'jpg:{directory}/file.tif.jpg\'', 'inode/x-empty'], + 'file.webp' => ['file.webp', '\'webp:{directory}/file.webp\'', 'inode/x-empty'], + 'file.webp.jpg' => ['file.webp.jpg', '\'jpg:{directory}/file.webp.jpg\'', 'inode/x-empty'], + 'file.pdf.jpg' => ['file.pdf.jpg', '\'jpg:{directory}/file.pdf.jpg\'', 'inode/x-empty'], + ]; + } + + /** + * @param string $fileName + * @param string $expectation + * @param string $mimeType + * + * @test + * @dataProvider fileStatementIsResolvedForEnforcedMimeTypeDataProvider + */ + public function fileStatementIsResolvedForEnforcedMimeType(string $fileName, string $expectation, string $mimeType) + { + $this->simulateNextFileInfoInvocation($mimeType); + $expectation = $this->substituteVariables($expectation); + $filePath = sprintf('%s/%s', $this->directory->url(), $fileName); + $file = ImageMagickFile::fromFilePath($filePath, null); + self::assertSame($expectation, (string)$file); + } + + /** + * @return array + */ + public function fileStatementIsResolvedForConfiguredMimeTypeDataProvider(): array + { + return [ + 'file.fax' => ['file.fax', '\'g3:{directory}/file.fax\''], + 'file.bmp' => ['file.bmp', '\'dib:{directory}/file.bmp\''], + ]; + } + + /** + * @param string $fileName + * @param string $expectation + * + * @test + * @dataProvider fileStatementIsResolvedForConfiguredMimeTypeDataProvider + */ + public function fileStatementIsResolvedForConfiguredMimeType(string $fileName, string $expectation) + { + $GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType']['g3'] = 'image/g3fax'; + $GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType']['fax'] = 'image/g3fax'; + $GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType']['dib'] = 'image/x-ms-bmp'; + $GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType']['bmp'] = 'image/x-ms-bmp'; + + $expectation = $this->substituteVariables($expectation); + $filePath = sprintf('%s/%s', $this->directory->url(), $fileName); + $file = ImageMagickFile::fromFilePath($filePath, null); + self::assertSame($expectation, (string)$file); + } + + /** + * @return array + */ + public function fileStatementIsDeniedDataProvider(): array + { + return [ + 'file.ps' => ['file.ps'], + 'file.eps' => ['file.eps'], + // denied since not defined in allowed extensions + 'file.ai' => ['file.ai', 'inode/x-empty'], + 'file.svg' => ['file.svg', 'inode/x-empty'], + 'file.pdf' => ['file.pdf', 'inode/x-empty'], + ]; + } + + /** + * @param string $fileName + * @param string|null $mimeType + * + * @test + * @dataProvider fileStatementIsDeniedDataProvider + */ + public function fileStatementIsDenied(string $fileName, string $mimeType = null) + { + self::expectException(Exception::class); + self::expectExceptionCode(1550060977); + + if ($mimeType !== null) { + $this->simulateNextFileInfoInvocation($mimeType); + } + + $filePath = sprintf('%s/%s', $this->directory->url(), $fileName); + ImageMagickFile::fromFilePath($filePath, null); + } + + /** + * @return array + */ + public function fileStatementIsDeniedForConfiguredMimeTypeDataProvider(): array + { + return [ + 'file.ps' => ['file.ps'], + 'file.eps' => ['file.eps'], + ]; + } + + /** + * @param string $fileName + * + * @test + * @dataProvider fileStatementIsDeniedForConfiguredMimeTypeDataProvider + */ + public function fileStatementIsDeniedForConfiguredMimeType(string $fileName) + { + $GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType']['ps'] = 'image/x-see-no-evil'; + $GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['fileExtensionToMimeType']['eps'] = 'image/x-see-no-evil'; + + self::expectException(Exception::class); + self::expectExceptionCode(1550060977); + + $filePath = sprintf('%s/%s', $this->directory->url(), $fileName); + ImageMagickFile::fromFilePath($filePath, null); + } + + /** + * @param array $structure + * @param array $fileNames + * @param string $sourcePath + */ + private function addFiles(array &$structure, array $fileNames, string $sourcePath) + { + $structure = array_merge( + $structure, + array_fill_keys( + $fileNames, + file_get_contents($sourcePath) + ) + ); + } + + /** + * @param string $value + * @return string + */ + private function substituteVariables(string $value): string + { + return str_replace( + ['{directory}'], + [$this->directory->url()], + $value + ); + } + + /** + * @param string $mimeType + * @param string[] $mimeExtensions + */ + private function simulateNextFileInfoInvocation(string $mimeType, array $mimeExtensions = []) + { + /** @var FileInfo|\PHPUnit_Framework_MockObject_MockObject $fileInfo */ + $fileInfo = $this->getAccessibleMock( + FileInfo::class, + ['getMimeType', 'getMimeExtensions'], + [], + '', + false + ); + $fileInfo->expects(self::atLeastOnce())->method('getMimeType')->willReturn($mimeType); + $fileInfo->expects(self::atLeastOnce())->method('getMimeExtensions')->willReturn($mimeExtensions); + GeneralUtility::addInstance(FileInfo::class, $fileInfo); + } +}
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
11- github.com/advisories/GHSA-3w4h-r27h-4r2wghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2019-11832ghsaADVISORY
- www.securityfocus.com/bid/108305mitrevdb-entryx_refsource_BID
- github.com/FriendsOfPHP/security-advisories/blob/master/typo3/cms-core/CVE-2019-11832.yamlghsaWEB
- github.com/FriendsOfPHP/security-advisories/blob/master/typo3/cms/CVE-2019-11832.yamlghsaWEB
- github.com/TYPO3/typo3/commit/2c04eeac44733fda491f92c697f88c1337d19c79ghsaWEB
- github.com/TYPO3/typo3/commit/51fdb774a57ee30e8d60c0e33b4a0b92d775739eghsaWEB
- github.com/TYPO3/typo3/commit/e845d90b82b2f72ab12a9e37f15082297832becaghsaWEB
- github.com/github/advisory-database/pull/3530ghsaWEB
- typo3.org/security/advisory/typo3-core-sa-2019-012ghsaWEB
- typo3.org/security/advisory/typo3-core-sa-2019-012/mitrex_refsource_MISC
News mentions
0No linked articles in our index yet.