VYPR
High severityNVD Advisory· Published May 9, 2019· Updated Aug 4, 2024

CVE-2019-11832

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.

PackageAffected versionsPatched versions
typo3/cms-corePackagist
>= 8.0.0, < 8.7.258.7.25
typo3/cms-corePackagist
>= 9.0.0, < 9.5.69.5.6
typo3/cmsPackagist
>= 8.0.0, < 8.7.258.7.25
typo3/cmsPackagist
>= 9.0.0, < 9.5.69.5.6

Affected products

3

Patches

3
e845d90b82b2

[SECURITY] Enclose file type scope when invoking ImageMagick

https://github.com/TYPO3/typo3Oliver HaderMay 7, 2019via ghsa
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 added
  • typo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.bmp+0 0 added
  • typo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.eps+7842 0 added
  • typo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.fax+0 0 added
  • typo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.gif+0 0 added
  • typo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.jpg+0 0 added
  • typo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.pdf+0 0 added
  • typo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.png+0 0 added
  • typo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.ps+0 0 added
  • typo3/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 added
  • typo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.webp+0 0 added
  • typo3/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

https://github.com/TYPO3/typo3Oliver HaderMay 7, 2019via ghsa
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 added
  • typo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.bmp+0 0 added
  • typo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.eps+7842 0 added
  • typo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.fax+0 0 added
  • typo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.gif+0 0 added
  • typo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.jpg+0 0 added
  • typo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.pdf+0 0 added
  • typo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.png+0 0 added
  • typo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.ps+0 0 added
  • typo3/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 added
  • typo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.webp+0 0 added
  • typo3/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

https://github.com/TYPO3/typo3Oliver HaderMay 7, 2019via ghsa
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 added
  • typo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.bmp+0 0 added
  • typo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.eps+7842 0 added
  • typo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.fax+0 0 added
  • typo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.gif+0 0 added
  • typo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.jpg+0 0 added
  • typo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.pdf+0 0 added
  • typo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.png+0 0 added
  • typo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.ps+0 0 added
  • typo3/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 added
  • typo3/sysext/core/Tests/Functional/Imaging/Fixtures/file.webp+0 0 added
  • typo3/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

News mentions

0

No linked articles in our index yet.