From 0b844b9d0eee0a97188c580058209a2f600a44d9 Mon Sep 17 00:00:00 2001 From: Benjamin Gaussorgues Date: Wed, 8 Jul 2026 15:58:28 +0200 Subject: [PATCH 1/2] feat(previews): add file signature check before opening files Signed-off-by: Benjamin Gaussorgues --- lib/private/Preview/Bitmap.php | 50 ++++++++++++++++++++++++++++- lib/private/Preview/Font.php | 18 +++++++++++ lib/private/Preview/HEIC.php | 14 ++++++-- lib/private/Preview/Illustrator.php | 15 +++++++++ lib/private/Preview/PDF.php | 15 +++++++++ lib/private/Preview/Photoshop.php | 15 +++++++++ lib/private/Preview/Postscript.php | 15 +++++++++ lib/private/Preview/SGI.php | 17 +++++++++- lib/private/Preview/TGA.php | 17 +++++++++- lib/private/Preview/TIFF.php | 20 ++++++++++++ 10 files changed, 191 insertions(+), 5 deletions(-) diff --git a/lib/private/Preview/Bitmap.php b/lib/private/Preview/Bitmap.php index 84c7b4a8968cd..044ba490cc1c1 100644 --- a/lib/private/Preview/Bitmap.php +++ b/lib/private/Preview/Bitmap.php @@ -37,6 +37,25 @@ * @package OC\Preview */ abstract class Bitmap extends ProviderV2 { + /** + * List of MIME types that this preview provider is allowed to process. + * + * These should correspond to the MIME types *identified* by Imagemagick + * for files to be processed by this provider. These do / will not + * necessarily need to match the MIME types stored in the database + * (which are identified by IMimeTypeDetector). + * + * @return string Regular expression + */ + abstract protected function getAllowedMimeTypes(): string; + + /** + * @return list + */ + abstract protected function getMagicStrings(): array; + + abstract protected function getImagickFormatHint(): string; + /** * {@inheritDoc} */ @@ -86,12 +105,25 @@ public function getThumbnail(File $file, int $maxX, int $maxY): ?IImage { * @param int $maxY * * @return \Imagick + * + * @throws \Exception */ private function getResizedPreview($tmpPath, $maxX, $maxY) { $bp = new Imagick(); + if (!$this->isMagicStringSupported($tmpPath)) { + throw new \Exception('Invalid image type: magic string not recognized'); + } + + // Validate mime type + $bp->pingImage($this->getImagickFormatHint() . ':' . $tmpPath . '[0]'); + $mimeType = $bp->getImageMimeType(); + if (!preg_match($this->getAllowedMimeTypes(), $mimeType)) { + throw new \Exception('File mime type does not match the preview provider: ' . $mimeType); + } + // Layer 0 contains either the bitmap or a flat representation of all vector layers - $bp->readImage($tmpPath . '[0]'); + $bp->readImage($this->getImagickFormatHint() . ':' . $tmpPath . '[0]'); $bp = $this->resize($bp, $maxX, $maxY); @@ -100,6 +132,22 @@ private function getResizedPreview($tmpPath, $maxX, $maxY) { return $bp; } + private function isMagicStringSupported(string $filepath): bool { + $signatures = $this->getMagicStrings(); + if (empty($signatures)) { + return true; + } + $length = array_reduce($signatures, static fn (int $carry, string $signature) => max($carry, strlen($signature)), 0); + $firstBytes = file_get_contents($filepath, false, null, 0, $length); + foreach ($signatures as $signature) { + if (str_starts_with($firstBytes, $signature)) { + return true; + } + } + + return false; + } + /** * Returns a resized \Imagick object * diff --git a/lib/private/Preview/Font.php b/lib/private/Preview/Font.php index baf29e1defcfa..bf5934529f6ca 100644 --- a/lib/private/Preview/Font.php +++ b/lib/private/Preview/Font.php @@ -30,4 +30,22 @@ class Font extends Bitmap { public function getMimeType(): string { return '/application\/(?:font-sfnt|x-font$)/'; } + + /** + * {@inheritDoc} + */ + protected function getAllowedMimeTypes(): string { + return '/(application|image)\/(?:font-sfnt|x-font|x-otf|x-ttf|x-pfb$)/'; + } + + protected function getMagicStrings(): array { + return [ + "\x00\x01\x00\x00\x00", // TTF + 'OTTO', // OTF + ]; + } + + protected function getImagickFormatHint(): string { + return 'ttf'; + } } diff --git a/lib/private/Preview/HEIC.php b/lib/private/Preview/HEIC.php index 71df98f9ac66e..ce3de1eea0b0f 100644 --- a/lib/private/Preview/HEIC.php +++ b/lib/private/Preview/HEIC.php @@ -44,7 +44,7 @@ class HEIC extends ProviderV2 { * {@inheritDoc} */ public function getMimeType(): string { - return '/image\/hei(f|c)/'; + return '/image\/(x-)?hei(f|c)/'; } /** @@ -108,12 +108,22 @@ public function getThumbnail(File $file, int $maxX, int $maxY): ?IImage { * @param int $maxY * * @return \Imagick + * + * @throws \Exception */ private function getResizedPreview($tmpPath, $maxX, $maxY) { $bp = new \Imagick(); + // Some HEIC files just contain (or at least are identified as) other formats + // like JPEG. We just need to check if the image is safe to process. + $bp->pingImage('heic:' . $tmpPath . '[0]'); + $mimeType = $bp->getImageMimeType(); + if (!preg_match('/^image\/(x-)?(png|jpeg|gif|bmp|tiff|webp|hei(f|c)|avif)$/', $mimeType)) { + throw new \Exception('File mime type does not match the preview provider: ' . $mimeType); + } + // Layer 0 contains either the bitmap or a flat representation of all vector layers - $bp->readImage($tmpPath . '[0]'); + $bp->readImage('heic:' . $tmpPath . '[0]'); // Fix orientation from EXIF $bp->autoOrient(); diff --git a/lib/private/Preview/Illustrator.php b/lib/private/Preview/Illustrator.php index 3d926c304c549..2c7deb02cb755 100644 --- a/lib/private/Preview/Illustrator.php +++ b/lib/private/Preview/Illustrator.php @@ -31,4 +31,19 @@ class Illustrator extends Bitmap { public function getMimeType(): string { return '/application\/illustrator/'; } + + /** + * {@inheritDoc} + */ + protected function getAllowedMimeTypes(): string { + return '/application\/(illustrator|pdf)/'; + } + + protected function getMagicStrings(): array { + return ["\x25\x50\x44\x46"]; + } + + protected function getImagickFormatHint(): string { + return 'ai'; + } } diff --git a/lib/private/Preview/PDF.php b/lib/private/Preview/PDF.php index 405fd1545f967..194f1c4ce7d7a 100644 --- a/lib/private/Preview/PDF.php +++ b/lib/private/Preview/PDF.php @@ -31,4 +31,19 @@ class PDF extends Bitmap { public function getMimeType(): string { return '/application\/pdf/'; } + + /** + * {@inheritDoc} + */ + protected function getAllowedMimeTypes(): string { + return '/application\/pdf/'; + } + + protected function getMagicStrings(): array { + return ['%PDF-']; + } + + protected function getImagickFormatHint(): string { + return 'pdf'; + } } diff --git a/lib/private/Preview/Photoshop.php b/lib/private/Preview/Photoshop.php index 9d79abf8191c2..9bf226a801ccb 100644 --- a/lib/private/Preview/Photoshop.php +++ b/lib/private/Preview/Photoshop.php @@ -31,4 +31,19 @@ class Photoshop extends Bitmap { public function getMimeType(): string { return '/application\/x-photoshop/'; } + + /** + * {@inheritDoc} + */ + protected function getAllowedMimeTypes(): string { + return '/(application|image)\/(x-photoshop|x-psd)/'; + } + + protected function getMagicStrings(): array { + return ['8BPS']; + } + + protected function getImagickFormatHint(): string { + return 'psd'; + } } diff --git a/lib/private/Preview/Postscript.php b/lib/private/Preview/Postscript.php index 06ea7fc803757..e2b6c022b639b 100644 --- a/lib/private/Preview/Postscript.php +++ b/lib/private/Preview/Postscript.php @@ -31,4 +31,19 @@ class Postscript extends Bitmap { public function getMimeType(): string { return '/application\/postscript/'; } + + /** + * {@inheritDoc} + */ + protected function getAllowedMimeTypes(): string { + return '/application\/postscript/'; + } + + protected function getMagicStrings(): array { + return ['%!PS']; + } + + protected function getImagickFormatHint(): string { + return 'ps'; + } } diff --git a/lib/private/Preview/SGI.php b/lib/private/Preview/SGI.php index 9f392d2252f25..a9cd9528093ca 100644 --- a/lib/private/Preview/SGI.php +++ b/lib/private/Preview/SGI.php @@ -28,6 +28,21 @@ class SGI extends Bitmap { * {@inheritDoc} */ public function getMimeType(): string { - return '/image\/sgi/'; + return '/image\/(x-)?sgi/'; + } + + /** + * {@inheritDoc} + */ + protected function getAllowedMimeTypes(): string { + return '/image\/(x-)?sgi/'; + } + + protected function getMagicStrings(): array { + return ["\x01\xDA"]; + } + + protected function getImagickFormatHint(): string { + return 'sgi'; } } diff --git a/lib/private/Preview/TGA.php b/lib/private/Preview/TGA.php index cb591be22319f..2044c794f4820 100644 --- a/lib/private/Preview/TGA.php +++ b/lib/private/Preview/TGA.php @@ -28,6 +28,21 @@ class TGA extends Bitmap { * {@inheritDoc} */ public function getMimeType(): string { - return '/image\/t(ar)?ga/'; + return '/image\/(x-)?t(ar)?ga/'; + } + + /** + * {@inheritDoc} + */ + protected function getAllowedMimeTypes(): string { + return '/image\/(x-)?t(ar)?ga/'; + } + + protected function getMagicStrings(): array { + return []; + } + + protected function getImagickFormatHint(): string { + return 'tga'; } } diff --git a/lib/private/Preview/TIFF.php b/lib/private/Preview/TIFF.php index 1a5d957d3ee4b..8c0d0706443de 100644 --- a/lib/private/Preview/TIFF.php +++ b/lib/private/Preview/TIFF.php @@ -31,4 +31,24 @@ class TIFF extends Bitmap { public function getMimeType(): string { return '/image\/tiff/'; } + + /** + * {@inheritDoc} + */ + protected function getAllowedMimeTypes(): string { + return '/image\/tiff/'; + } + + protected function getMagicStrings(): array { + return [ + "II*\x00", + "MM\x00*", + "II+\x00", + "MM\x00+", + ]; + } + + protected function getImagickFormatHint(): string { + return 'tiff'; + } } From 877e24def51ad6bf8e81112cb753a4d8e1f59291 Mon Sep 17 00:00:00 2001 From: Benjamin Gaussorgues Date: Tue, 28 Jul 2026 10:23:46 +0200 Subject: [PATCH 2/2] Revert "fix: Disable imagick preview providers" This reverts commit 706fe8f8179e288b43800e88f022d68357673f00. Signed-off-by: Benjamin Gaussorgues --- lib/private/Preview/IMagickSupport.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/private/Preview/IMagickSupport.php b/lib/private/Preview/IMagickSupport.php index ab7a3a381529b..e22ae93ab940e 100644 --- a/lib/private/Preview/IMagickSupport.php +++ b/lib/private/Preview/IMagickSupport.php @@ -20,12 +20,10 @@ public function __construct(ICacheFactory $cacheFactory) { } public function hasExtension(): bool { - return false; return !is_null($this->imagick); } public function supportsFormat(string $format): bool { - return false; if (is_null($this->imagick)) { return false; }