aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJoas Schilling <213943+nickvergessen@users.noreply.github.com>2022-01-11 13:35:56 +0100
committerGitHub <noreply@github.com>2022-01-11 13:35:56 +0100
commitc47406ad3cc1606d7357af6c32c4fa7dd0ac53a7 (patch)
tree77f9931990cd270451f9f738e14bb852704f26f2
parentb23934a45ec584e398835635584461a02c9b1dde (diff)
parentd3d65e5c889fc3922efc7a8c764027763bc4764f (diff)
downloadnextcloud-server-c47406ad3cc1606d7357af6c32c4fa7dd0ac53a7.tar.gz
nextcloud-server-c47406ad3cc1606d7357af6c32c4fa7dd0ac53a7.zip
Merge pull request #30291 from nextcloud/image-memory-limit
Prevent loading images that would require too much memory.
-rw-r--r--config/config.sample.php10
-rw-r--r--lib/private/legacy/OC_Image.php103
-rw-r--r--tests/data/testimage-badheader.jpgbin0 -> 103 bytes
-rw-r--r--tests/lib/ImageTest.php17
4 files changed, 129 insertions, 1 deletions
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
@@ -1044,6 +1044,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 */
@@ -564,6 +568,71 @@ class OC_Image implements \OCP\IImage {
}
/**
+ * 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.
*
* @param bool|string $imagePath The path to 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
--- /dev/null
+++ b/tests/data/testimage-badheader.jpg
Binary files 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());
+ }
}