diff --git a/CHANGES.rst b/CHANGES.rst index 635b7cc73a5..05ce203ddac 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -27,6 +27,9 @@ Changelog (Pillow) combination of \r and \n as line endings. [rickprice] +- Fix CVE-2022-45199: Pillow before 9.3.0 allows denial of service via SAMPLESPERPIXEL. + [rickprice] + - Fix CVE-2021-28676: FliDecode did not properly check that the block advance was non-zero, potentally leading to an infinite loop on load. [rickprice] diff --git a/Tests/images/oom-225817ca0f8c663be7ab4b9e717b02c661e66834.tif b/Tests/images/oom-225817ca0f8c663be7ab4b9e717b02c661e66834.tif new file mode 100644 index 00000000000..01dca594f53 Binary files /dev/null and b/Tests/images/oom-225817ca0f8c663be7ab4b9e717b02c661e66834.tif differ diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index be00a4f9cb8..34cbb7b67ad 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -2,7 +2,9 @@ import sys from io import BytesIO -from PIL import Image, TiffImagePlugin +import pytest + +from PIL import Image, ImageFile, TiffImagePlugin, UnidentifiedImageError from PIL._util import py3 from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION @@ -13,7 +15,6 @@ class TestFileTiff(PillowTestCase): def test_sanity(self): - filename = self.tempfile("temp.tif") hopper("RGB").save(filename) @@ -223,8 +224,8 @@ def test_16bit_s(self): self.assertEqual(im.getpixel((0, 1)), 0) def test_12bit_rawmode(self): - """ Are we generating the same interpretation - of the image as Imagemagick is? """ + """Are we generating the same interpretation + of the image as Imagemagick is?""" im = Image.open("Tests/images/12bit.cropped.tif") @@ -616,6 +617,17 @@ def test_fd_leak(self): tmpfile = self.tempfile("temp.tif") import os + @pytest.mark.parametrize( + "test_file", + [ + "Tests/images/oom-225817ca0f8c663be7ab4b9e717b02c661e66834.tif", + ], + ) + def test_oom(self, test_file): + with pytest.raises(UnidentifiedImageError): + with Image.open(test_file) as im: + pass + # this is an mmaped file. with Image.open("Tests/images/uint16_1_4660.tif") as im: im.save(tmpfile) diff --git a/docs/releasenotes/6.2.2.5.rst b/docs/releasenotes/6.2.2.5.rst index 4130c871073..a18c5c37bd5 100644 --- a/docs/releasenotes/6.2.2.5.rst +++ b/docs/releasenotes/6.2.2.5.rst @@ -37,3 +37,5 @@ This release addresses several critical CVEs. Pillow in the open phase, before an image was accepted for opening. +:cve: `CVE-2022-45199`: Pillow before 9.3.0 allows denial of service via SAMPLESPERPIXEL. + diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 0661c2ffb19..5d5df3abc53 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -261,6 +261,8 @@ (MM, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), } +MAX_SAMPLESPERPIXEL = max(len(key_tp[4]) for key_tp in OPEN_INFO.keys()) + PREFIXES = [ b"MM\x00\x2A", # Valid TIFF header with big-endian byte order b"II\x2A\x00", # Valid TIFF header with little-endian byte order @@ -1264,10 +1266,25 @@ def _setup(self): else: bps_count = 1 bps_count += len(extra_tuple) - # Some files have only one value in bps_tuple, - # while should have more. Fix it - if bps_count > len(bps_tuple) and len(bps_tuple) == 1: - bps_tuple = bps_tuple * bps_count + bps_actual_count = len(bps_tuple) + samples_per_pixel = self.tag_v2.get( + SAMPLESPERPIXEL, + 3 if self._compression == "tiff_jpeg" and photo in (2, 6) else 1, + ) + + if samples_per_pixel > MAX_SAMPLESPERPIXEL: + # DOS check, samples_per_pixel can be a Long, and we extend the tuple below + logger.error("More samples per pixel than can be decoded: %s", samples_per_pixel) + raise SyntaxError("Invalid value for samples per pixel") + + if samples_per_pixel < bps_actual_count: + # If a file has more values in bps_tuple than expected, + # remove the excess. + bps_tuple = bps_tuple[:samples_per_pixel] + elif samples_per_pixel > bps_actual_count and bps_actual_count == 1: + # If a file has only one value in bps_tuple, when it should have more, + # presume it is the same number of bits for all of the samples. + bps_tuple = bps_tuple * samples_per_pixel # mode: check photometric interpretation and bits per pixel key = (