diff --git a/config/config.sample.php b/config/config.sample.php index 0211d08676a..3eadbc61290 100644 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -1043,6 +1043,16 @@ $CONFIG = [ */ 'preview_max_filesize_image' => 50, +/** + * max memory for generating image previews with imagegd (default behavior) + * Reads the image dimensions from the header and assumes 32 bits per pixel. + * If creating the image would allocate more memory, preview generation will + * be disabled and the default mimetype icon is shown. Set to -1 for no limit. + * + * Defaults to ``128`` megabytes + */ +'preview_max_memory' => 128, + /** * custom path for LibreOffice/OpenOffice binary * diff --git a/lib/private/legacy/OC_Image.php b/lib/private/legacy/OC_Image.php index 333a9d919ef..3988eb8eaa5 100644 --- a/lib/private/legacy/OC_Image.php +++ b/lib/private/legacy/OC_Image.php @@ -46,6 +46,10 @@ use OCP\IImage; * Class for basic image manipulation */ class OC_Image implements \OCP\IImage { + + // Default memory limit for images to load (128 MBytes). + protected const DEFAULT_MEMORY_LIMIT = 128; + /** @var false|resource|\GdImage */ protected $resource = false; // tmp resource. /** @var int */ @@ -563,6 +567,71 @@ class OC_Image implements \OCP\IImage { return false; } + /** + * Check if allocating an image with the given size is allowed. + * + * @param int $width The image width. + * @param int $height The image height. + * @return bool true if allocating is allowed, false otherwise + */ + private function checkImageMemory($width, $height) { + $memory_limit = $this->config->getSystemValueInt('preview_max_memory', self::DEFAULT_MEMORY_LIMIT); + if ($memory_limit < 0) { + // Not limited. + return true; + } + + // Assume 32 bits per pixel. + if ($width * $height * 4 > $memory_limit * 1024 * 1024) { + $this->logger->debug('Image size of ' . $width . 'x' . $height . ' would exceed allowed memory limit of ' . $memory_limit); + return false; + } + + return true; + } + + /** + * Check if loading an image file from the given path is allowed. + * + * @param string $path The path to a local file. + * @return bool true if allocating is allowed, false otherwise + */ + private function checkImageSize($path) { + $size = getimagesize($path); + if (!$size) { + return true; + } + + $width = $size[0]; + $height = $size[1]; + if (!$this->checkImageMemory($width, $height)) { + return false; + } + + return true; + } + + /** + * Check if loading an image from the given data is allowed. + * + * @param string $data A string of image data as read from a file. + * @return bool true if allocating is allowed, false otherwise + */ + private function checkImageDataSize($data) { + $size = getimagesizefromstring($data); + if (!$size) { + return true; + } + + $width = $size[0]; + $height = $size[1]; + if (!$this->checkImageMemory($width, $height)) { + return false; + } + + return true; + } + /** * Loads an image from a local file. * @@ -578,6 +647,9 @@ class OC_Image implements \OCP\IImage { switch ($iType) { case IMAGETYPE_GIF: if (imagetypes() & IMG_GIF) { + if (!$this->checkImageSize($imagePath)) { + return false; + } $this->resource = imagecreatefromgif($imagePath); if ($this->resource) { // Preserve transparency @@ -592,6 +664,9 @@ class OC_Image implements \OCP\IImage { break; case IMAGETYPE_JPEG: if (imagetypes() & IMG_JPG) { + if (!$this->checkImageSize($imagePath)) { + return false; + } if (getimagesize($imagePath) !== false) { $this->resource = @imagecreatefromjpeg($imagePath); } else { @@ -603,6 +678,9 @@ class OC_Image implements \OCP\IImage { break; case IMAGETYPE_PNG: if (imagetypes() & IMG_PNG) { + if (!$this->checkImageSize($imagePath)) { + return false; + } $this->resource = @imagecreatefrompng($imagePath); if ($this->resource) { // Preserve transparency @@ -617,6 +695,9 @@ class OC_Image implements \OCP\IImage { break; case IMAGETYPE_XBM: if (imagetypes() & IMG_XPM) { + if (!$this->checkImageSize($imagePath)) { + return false; + } $this->resource = @imagecreatefromxbm($imagePath); } else { $this->logger->debug('OC_Image->loadFromFile, XBM/XPM images not supported: ' . $imagePath, ['app' => 'core']); @@ -624,6 +705,9 @@ class OC_Image implements \OCP\IImage { break; case IMAGETYPE_WBMP: if (imagetypes() & IMG_WBMP) { + if (!$this->checkImageSize($imagePath)) { + return false; + } $this->resource = @imagecreatefromwbmp($imagePath); } else { $this->logger->debug('OC_Image->loadFromFile, WBMP images not supported: ' . $imagePath, ['app' => 'core']); @@ -634,6 +718,9 @@ class OC_Image implements \OCP\IImage { break; case IMAGETYPE_WEBP: if (imagetypes() & IMG_WEBP) { + if (!$this->checkImageSize($imagePath)) { + return false; + } $this->resource = @imagecreatefromwebp($imagePath); } else { $this->logger->debug('OC_Image->loadFromFile, webp images not supported: ' . $imagePath, ['app' => 'core']); @@ -666,7 +753,11 @@ class OC_Image implements \OCP\IImage { default: // this is mostly file created from encrypted file - $this->resource = imagecreatefromstring(file_get_contents($imagePath)); + $data = file_get_contents($imagePath); + if (!$this->checkImageDataSize($data)) { + return false; + } + $this->resource = imagecreatefromstring($data); $iType = IMAGETYPE_PNG; $this->logger->debug('OC_Image->loadFromFile, Default', ['app' => 'core']); break; @@ -689,6 +780,9 @@ class OC_Image implements \OCP\IImage { if (!is_string($str)) { return false; } + if (!$this->checkImageDataSize($str)) { + return false; + } $this->resource = @imagecreatefromstring($str); if ($this->fileInfo) { $this->mimeType = $this->fileInfo->buffer($str); @@ -717,6 +811,9 @@ class OC_Image implements \OCP\IImage { } $data = base64_decode($str); if ($data) { // try to load from string data + if (!$this->checkImageDataSize($data)) { + return false; + } $this->resource = @imagecreatefromstring($data); if ($this->fileInfo) { $this->mimeType = $this->fileInfo->buffer($data); @@ -793,6 +890,10 @@ class OC_Image implements \OCP\IImage { } } } + if (!$this->checkImageMemory($meta['width'], $meta['height'])) { + fclose($fh); + return false; + } // create gd image $im = imagecreatetruecolor($meta['width'], $meta['height']); if ($im == false) { diff --git a/tests/data/testimage-badheader.jpg b/tests/data/testimage-badheader.jpg new file mode 100644 index 00000000000..b876804eb4e Binary files /dev/null and b/tests/data/testimage-badheader.jpg differ diff --git a/tests/lib/ImageTest.php b/tests/lib/ImageTest.php index 5b83c4ac57f..e6818c7e243 100644 --- a/tests/lib/ImageTest.php +++ b/tests/lib/ImageTest.php @@ -142,6 +142,10 @@ class ImageTest extends \Test\TestCase { ->method('getAppValue') ->with('preview', 'jpeg_quality', 90) ->willReturn(null); + $config->expects($this->once()) + ->method('getSystemValueInt') + ->with('preview_max_memory', 128) + ->willReturn(128); $img = new \OC_Image(null, null, $config); $img->loadFromFile(OC::$SERVERROOT.'/tests/data/testimage.jpg'); $raw = imagecreatefromstring(file_get_contents(OC::$SERVERROOT.'/tests/data/testimage.jpg')); @@ -363,4 +367,17 @@ class ImageTest extends \Test\TestCase { $img->save($tempFile, $mimeType); $this->assertEquals($mimeType, image_type_to_mime_type(exif_imagetype($tempFile))); } + + public function testMemoryLimitFromFile() { + $img = new \OC_Image(); + $img->loadFromFile(OC::$SERVERROOT.'/tests/data/testimage-badheader.jpg'); + $this->assertFalse($img->valid()); + } + + public function testMemoryLimitFromData() { + $data = file_get_contents(OC::$SERVERROOT.'/tests/data/testimage-badheader.jpg'); + $img = new \OC_Image(); + $img->loadFromData($data); + $this->assertFalse($img->valid()); + } }