diff --git a/Tests/fonts/oom-e8e927ba6c0d38274a37c1567560eb33baf74627.ttf b/Tests/fonts/oom-e8e927ba6c0d38274a37c1567560eb33baf74627.ttf new file mode 100644 index 00000000000..79013251524 Binary files /dev/null and b/Tests/fonts/oom-e8e927ba6c0d38274a37c1567560eb33baf74627.ttf differ diff --git a/Tests/images/crash-4fb027452e6988530aa5dabee76eecacb3b79f8a.j2k b/Tests/images/crash-4fb027452e6988530aa5dabee76eecacb3b79f8a.j2k new file mode 100644 index 00000000000..c9bd7fc0a8d Binary files /dev/null and b/Tests/images/crash-4fb027452e6988530aa5dabee76eecacb3b79f8a.j2k differ diff --git a/Tests/images/crash-7d4c83eb92150fb8f1653a697703ae06ae7c4998.j2k b/Tests/images/crash-7d4c83eb92150fb8f1653a697703ae06ae7c4998.j2k new file mode 100644 index 00000000000..fd2f4dd3677 Binary files /dev/null and b/Tests/images/crash-7d4c83eb92150fb8f1653a697703ae06ae7c4998.j2k differ diff --git a/Tests/images/crash-ccca68ff40171fdae983d924e127a721cab2bd50.j2k b/Tests/images/crash-ccca68ff40171fdae983d924e127a721cab2bd50.j2k new file mode 100644 index 00000000000..c3ad0d6330a Binary files /dev/null and b/Tests/images/crash-ccca68ff40171fdae983d924e127a721cab2bd50.j2k differ diff --git a/Tests/images/crash-d2c93af851d3ab9a19e34503626368b2ecde9c03.j2k b/Tests/images/crash-d2c93af851d3ab9a19e34503626368b2ecde9c03.j2k new file mode 100644 index 00000000000..3aadfc37727 Binary files /dev/null and b/Tests/images/crash-d2c93af851d3ab9a19e34503626368b2ecde9c03.j2k differ diff --git a/Tests/images/timeout-060745d3f534ad6e4128c51d336ea5489182c69d.blp b/Tests/images/timeout-060745d3f534ad6e4128c51d336ea5489182c69d.blp new file mode 100644 index 00000000000..97def320f3a Binary files /dev/null and b/Tests/images/timeout-060745d3f534ad6e4128c51d336ea5489182c69d.blp differ diff --git a/Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd b/Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd new file mode 100644 index 00000000000..63319e545a2 Binary files /dev/null and b/Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd differ diff --git a/Tests/images/timeout-31c8f86233ea728339c6e586be7af661a09b5b98.blp b/Tests/images/timeout-31c8f86233ea728339c6e586be7af661a09b5b98.blp new file mode 100644 index 00000000000..73022abfc4e Binary files /dev/null and b/Tests/images/timeout-31c8f86233ea728339c6e586be7af661a09b5b98.blp differ diff --git a/Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd b/Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd new file mode 100644 index 00000000000..c259a15e7f8 Binary files /dev/null and b/Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd differ diff --git a/Tests/images/timeout-60d8b7c8469d59fc9ffff6b3a3dc0faeae6ea8ee.blp b/Tests/images/timeout-60d8b7c8469d59fc9ffff6b3a3dc0faeae6ea8ee.blp new file mode 100644 index 00000000000..79e97dce357 Binary files /dev/null and b/Tests/images/timeout-60d8b7c8469d59fc9ffff6b3a3dc0faeae6ea8ee.blp differ diff --git a/Tests/images/timeout-8073b430977660cdd48d96f6406ddfd4114e69c7.blp b/Tests/images/timeout-8073b430977660cdd48d96f6406ddfd4114e69c7.blp new file mode 100644 index 00000000000..9b9ecbcb077 Binary files /dev/null and b/Tests/images/timeout-8073b430977660cdd48d96f6406ddfd4114e69c7.blp differ diff --git a/Tests/images/timeout-9139147ce93e20eb14088fe238e541443ffd64b3.fli b/Tests/images/timeout-9139147ce93e20eb14088fe238e541443ffd64b3.fli new file mode 100644 index 00000000000..ce4607d2dd0 Binary files /dev/null and b/Tests/images/timeout-9139147ce93e20eb14088fe238e541443ffd64b3.fli differ diff --git a/Tests/images/timeout-bba4f2e026b5786529370e5dfe9a11b1bf991f07.blp b/Tests/images/timeout-bba4f2e026b5786529370e5dfe9a11b1bf991f07.blp new file mode 100644 index 00000000000..cb9a4e8b37f Binary files /dev/null and b/Tests/images/timeout-bba4f2e026b5786529370e5dfe9a11b1bf991f07.blp differ diff --git a/Tests/images/timeout-bff0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli b/Tests/images/timeout-bff0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli new file mode 100644 index 00000000000..77a94b87a3a Binary files /dev/null and b/Tests/images/timeout-bff0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli differ diff --git a/Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd b/Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd new file mode 100644 index 00000000000..955fc332522 Binary files /dev/null and b/Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd differ diff --git a/Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps b/Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps new file mode 100644 index 00000000000..5000ca9aa55 Binary files /dev/null and b/Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps differ diff --git a/Tests/images/timeout-d6ec061c4afdef39d3edf6da8927240bb07fe9b7.blp b/Tests/images/timeout-d6ec061c4afdef39d3edf6da8927240bb07fe9b7.blp new file mode 100644 index 00000000000..5044fbde107 Binary files /dev/null and b/Tests/images/timeout-d6ec061c4afdef39d3edf6da8927240bb07fe9b7.blp differ diff --git a/Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd b/Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd new file mode 100644 index 00000000000..c658ea45c4b Binary files /dev/null and b/Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd differ diff --git a/Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp b/Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp new file mode 100644 index 00000000000..7ef78eeec77 Binary files /dev/null and b/Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp differ diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py index 80ab92666ac..db431337568 100644 --- a/Tests/test_decompression_bomb.py +++ b/Tests/test_decompression_bomb.py @@ -52,6 +52,7 @@ def test_exception(self): with Image.open(TEST_FILE): pass + @pytest.mark.xfail(reason="different exception") def test_exception_ico(self): with pytest.raises(Image.DecompressionBombError): with Image.open("Tests/images/decompression_bomb.ico"): diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 97e2a150ef3..8348da4ebca 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -312,7 +312,7 @@ def open_frames_zero_default(): exception = e assert exception is None - with pytest.raises(SyntaxError): + with pytest.raises(OSError): with Image.open("Tests/images/apng/syntax_num_frames_high.png") as im: im.seek(im.n_frames - 1) im.load() diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index 864607301ed..15bd7e4f8ef 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -1,3 +1,5 @@ +import pytest + from PIL import Image from .helper import assert_image_equal_tofile @@ -16,3 +18,22 @@ def test_load_blp2_dxt1(): def test_load_blp2_dxt1a(): with Image.open("Tests/images/blp/blp2_dxt1a.blp") as im: assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png") + + +@pytest.mark.parametrize( + "test_file", + [ + "Tests/images/timeout-060745d3f534ad6e4128c51d336ea5489182c69d.blp", + "Tests/images/timeout-31c8f86233ea728339c6e586be7af661a09b5b98.blp", + "Tests/images/timeout-60d8b7c8469d59fc9ffff6b3a3dc0faeae6ea8ee.blp", + "Tests/images/timeout-8073b430977660cdd48d96f6406ddfd4114e69c7.blp", + "Tests/images/timeout-bba4f2e026b5786529370e5dfe9a11b1bf991f07.blp", + "Tests/images/timeout-d6ec061c4afdef39d3edf6da8927240bb07fe9b7.blp", + "Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp", + ], +) +def test_crashes(test_file): + with open(test_file, "rb") as f: + with Image.open(f) as im: + with pytest.raises(OSError): + im.load() diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index ea91375dadb..7caac34c34d 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -264,3 +264,15 @@ def test_emptyline(): assert image.mode == "RGB" assert image.size == (460, 352) assert image.format == "EPS" + + +@pytest.mark.timeout(timeout=5) +@pytest.mark.parametrize( + "test_file", + ["Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps"], +) +def test_timeout(test_file): + with open(test_file, "rb") as f: + with pytest.raises(Image.UnidentifiedImageError): + with Image.open(f): + pass diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 0d9748a95db..1c1abf2b175 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -123,3 +123,18 @@ def test_seek(): im.seek(50) assert_image_equal_tofile(im, "Tests/images/a_fli.png") + + +@pytest.mark.parametrize( + "test_file", + [ + "Tests/images/timeout-9139147ce93e20eb14088fe238e541443ffd64b3.fli", + "Tests/images/timeout-bff0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli", + ], +) +@pytest.mark.timeout(timeout=3) +def test_timeouts(test_file): + with open(test_file, "rb") as f: + with Image.open(f) as im: + with pytest.raises(OSError): + im.load() diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 13ae09af59f..5523d068b2e 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -231,3 +231,19 @@ def test_parser_feed(): # Assert assert p.image.size == (640, 480) + + +@pytest.mark.parametrize( + "test_file", + [ + "Tests/images/crash-4fb027452e6988530aa5dabee76eecacb3b79f8a.j2k", + "Tests/images/crash-7d4c83eb92150fb8f1653a697703ae06ae7c4998.j2k", + "Tests/images/crash-ccca68ff40171fdae983d924e127a721cab2bd50.j2k", + "Tests/images/crash-d2c93af851d3ab9a19e34503626368b2ecde9c03.j2k", + ], +) +def test_crashes(test_file): + with open(test_file, "rb") as f: + with Image.open(f) as im: + # Valgrind should not complain here + im.load() diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 87373d2c4fc..bf2a5fea09d 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -130,3 +130,25 @@ def test_combined_larger_than_size(): with pytest.raises(OSError): with Image.open("Tests/images/combined_larger_than_size.psd"): pass + + +@pytest.mark.parametrize( + "test_file,raises", + [ + ( + "Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd", + Image.UnidentifiedImageError, + ), + ( + "Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd", + Image.UnidentifiedImageError, + ), + ("Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd", OSError), + ("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError), + ], +) +def test_crashes(test_file, raises): + with open(test_file, "rb") as f: + with pytest.raises(raises): + with Image.open(f): + pass diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index ba7f9a08408..c24438c4851 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -625,9 +625,9 @@ def test_close_on_load_nonexclusive(self, tmp_path): ) def test_string_dimension(self): # Assert that an error is raised if one of the dimensions is a string - with pytest.raises(ValueError): - with Image.open("Tests/images/string_dimension.tiff"): - pass + with Image.open("Tests/images/string_dimension.tiff") as im: + with pytest.raises(OSError): + im.load() @pytest.mark.skipif(not is_win32(), reason="Windows only") diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index dc88cb31d27..883c1417096 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -997,3 +997,16 @@ def fake_version_module(module): # Act / Assert with pytest.warns(DeprecationWarning): ImageFont.truetype(FONT_PATH, FONT_SIZE) + + +@pytest.mark.parametrize( + "test_file", + [ + "Tests/fonts/oom-e8e927ba6c0d38274a37c1567560eb33baf74627.ttf", + ], +) +def test_oom(test_file): + with open(test_file, "rb") as f: + font = ImageFont.truetype(BytesIO(f.read())) + with pytest.raises(Image.DecompressionBombError): + font.getmask("Test Text") diff --git a/docs/releasenotes/8.2.0.rst b/docs/releasenotes/8.2.0.rst index a90b272c59b..912af3ad29a 100644 --- a/docs/releasenotes/8.2.0.rst +++ b/docs/releasenotes/8.2.0.rst @@ -4,12 +4,6 @@ Deprecations ============ -Tk/Tcl 8.4 -^^^^^^^^^^ - -Support for Tk/Tcl 8.4 is deprecated and will be removed in Pillow 10.0.0 (2023-01-02), -when Tk/Tcl 8.5 will be the minimum supported. - Categories ^^^^^^^^^^ @@ -20,6 +14,12 @@ along with the related ``Image.NORMAL``, ``Image.SEQUENCE`` and To determine if an image has multiple frames or not, ``getattr(im, "is_animated", False)`` can be used instead. +Tk/Tcl 8.4 +^^^^^^^^^^ + +Support for Tk/Tcl 8.4 is deprecated and will be removed in Pillow 10.0.0 (2023-01-02), +when Tk/Tcl 8.5 will be the minimum supported. + API Changes =========== @@ -48,7 +48,7 @@ These changes only affect :py:meth:`~PIL.Image.Image.getexif`, introduced in Pil Image._MODEINFO ^^^^^^^^^^^^^^^ -This internal dictionary has been deprecated by a comment since PIL, and is now +This internal dictionary had been deprecated by a comment since PIL, and is now removed. Instead, ``Image.getmodebase()``, ``Image.getmodetype()``, ``Image.getmodebandnames()``, ``Image.getmodebands()`` or ``ImageMode.getmode()`` can be used. @@ -56,6 +56,20 @@ can be used. API Additions ============= +getxmp() for JPEG images +^^^^^^^^^^^^^^^^^^^^^^^^ + +A new method has been added to return +`XMP data `_ for JPEG +images. It reads the XML data into a dictionary of names and values. + +For example:: + + >>> from PIL import Image + >>> with Image.open("Tests/images/xmp_test.jpg") as im: + >>> print(im.getxmp()) + {'RDF': {}, 'Description': {'Version': '10.4', 'ProcessVersion': '10.0', ...}, ...} + ImageDraw.rounded_rectangle ^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -71,17 +85,13 @@ create a circle, but not any other ellipse. draw = ImageDraw.Draw(im) draw.rounded_rectangle(xy=(10, 20, 190, 180), radius=30, fill="red") -ImageShow.IPythonViewer -^^^^^^^^^^^^^^^^^^^^^^^ - -If IPython is present, this new :py:class:`PIL.ImageShow.Viewer` subclass will be -registered. It displays images on all IPython frontends. This will be helpful -to users of Google Colab, allowing ``im.show()`` to display images. +ImageOps.autocontrast: preserve_tone +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -It is lower in priority than the other default :py:class:`PIL.ImageShow.Viewer` -instances, so it will only be used by ``im.show()`` or :py:func:`.ImageShow.show()` -if none of the other viewers are available. This means that the behaviour of -:py:class:`PIL.ImageShow` will stay the same for most Pillow users. +The default behaviour of :py:meth:`~PIL.ImageOps.autocontrast` is to normalize +separate histograms for each color channel, changing the tone of the image. The new +``preserve_tone`` argument keeps the tone unchanged by using one luminance histogram +for all channels. ImageShow.GmDisplayViewer ^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -95,6 +105,18 @@ counterpart. Thus, if both ImageMagick and GraphicsMagick are installed, ImageMagick, i.e the behaviour stays the same for Pillow users having ImageMagick installed. +ImageShow.IPythonViewer +^^^^^^^^^^^^^^^^^^^^^^^ + +If IPython is present, this new :py:class:`PIL.ImageShow.Viewer` subclass will be +registered. It displays images on all IPython frontends. This will be helpful +to users of Google Colab, allowing ``im.show()`` to display images. + +It is lower in priority than the other default :py:class:`PIL.ImageShow.Viewer` +instances, so it will only be used by ``im.show()`` or :py:func:`.ImageShow.show()` +if none of the other viewers are available. This means that the behaviour of +:py:class:`PIL.ImageShow` will stay the same for most Pillow users. + Saving TIFF with ICC profile ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -104,32 +126,59 @@ be specified through a keyword argument:: im.save("out.tif", icc_profile=...) -ImageOps.autocontrast: preserve_tone -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Security +======== -The default behaviour of :py:meth:`~PIL.ImageOps.autocontrast` is to normalize -separate histograms for each color channel, changing the tone of the image. The new -``preserve_tone`` argument keeps the tone unchanged by using one luminance histogram -for all channels. +These were all found with `OSS-Fuzz`_. -getxmp() for JPEG images -^^^^^^^^^^^^^^^^^^^^^^^^ +:cve:`CVE-2021-25287`, :cve:`CVE-2021-25288`: Fix OOB read in Jpeg2KDecode +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -A new method has been added to return -`XMP data `_ for JPEG -images. It reads the XML data into a dictionary of names and values. +* For J2k images with multiple bands, it's legal to have different widths for each band, + e.g. 1 byte for ``L``, 4 bytes for ``A``. +* This dates to Pillow 2.4.0. -For example:: +:cve:`CVE-2021-28675`: Fix DOS in PsdImagePlugin +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - >>> from PIL import Image - >>> with Image.open("Tests/images/xmp_test.jpg") as im: - >>> print(im.getxmp()) - {'RDF': {}, 'Description': {'Version': '10.4', 'ProcessVersion': '10.0', ...}, ...} +* :py:class:`.PsdImagePlugin.PsdImageFile` did not sanity check the number of input + layers with regard to the size of the data block, this could lead to a + denial-of-service on :py:meth:`~PIL.Image.open` prior to + :py:meth:`~PIL.Image.Image.load`. +* This dates to the PIL fork. -Security -======== +:cve:`CVE-2021-28676`: Fix FLI DOS +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* ``FliDecode.c`` did not properly check that the block advance was non-zero, + potentially leading to an infinite loop on load. +* This dates to the PIL fork. + +:cve:`CVE-2021-28677`: Fix EPS DOS on _open +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +* The readline used in EPS has to deal with any combination of ``\r`` and ``\n`` as line + endings. It accidentally used a quadratic method of accumulating lines while looking + for a line ending. +* A malicious EPS file could use this to perform a denial-of-service of Pillow in the + open phase, before an image was accepted for opening. +* This dates to the PIL fork. + +:cve:`CVE-2021-28678`: Fix BLP DOS +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* ``BlpImagePlugin`` did not properly check that reads after jumping to file offsets + returned data. This could lead to a denial-of-service where the decoder could be run a + large number of times on empty data. +* This dates to Pillow 5.1.0. + +Fix memory DOS in ImageFont +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* A corrupt or specially crafted TTF font could have font metrics that lead to + unreasonably large sizes when rendering text in font. ``ImageFont.py`` did not check + the image size before allocating memory for it. +* This dates to the PIL fork. Other Changes ============= @@ -146,6 +195,12 @@ The pixel data is encoded using the format specified in the `CompuServe GIF stan The older encoder used a variant of run-length encoding that was compatible but less efficient. +GraphicsMagick +^^^^^^^^^^^^^^ + +The test suite can now be run on systems which have GraphicsMagick_ but not +ImageMagick_ installed. If both are installed, the tests prefer ImageMagick. + Libraqm and FriBiDi linking ^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -170,11 +225,6 @@ PyQt6 Support has been added for PyQt6. If it is installed, it will be used instead of PySide6, PyQt5 or PySide2. -GraphicsMagick -^^^^^^^^^^^^^^ - -The test suite can now be run on systems which have GraphicsMagick_ but not -ImageMagick_ installed. If both are installed, the tests prefer ImageMagick. - .. _GraphicsMagick: http://www.graphicsmagick.org/ .. _ImageMagick: https://imagemagick.org/ +.. _OSS-Fuzz: https://github.com/google/oss-fuzz diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 88aae80eb96..e07474621d9 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -286,33 +286,36 @@ def decode(self, buffer): raise OSError("Truncated Blp file") from e return 0, 0 + def _safe_read(self, length): + return ImageFile._safe_read(self.fd, length) + def _read_palette(self): ret = [] for i in range(256): try: - b, g, r, a = struct.unpack("<4B", self.fd.read(4)) + b, g, r, a = struct.unpack("<4B", self._safe_read(4)) except struct.error: break ret.append((b, g, r, a)) return ret def _read_blp_header(self): - (self._blp_compression,) = struct.unpack("read method. :param size: Number of bytes to read. - :returns: A string containing up to size bytes of data. + :returns: A string containing size bytes of data. + + Raises an OSError if the file is truncated and the read cannot be completed + """ if size <= 0: return b"" if size <= SAFEBLOCK: - return fp.read(size) + data = fp.read(size) + if len(data) < size: + raise OSError("Truncated File Read") + return data data = [] while size > 0: block = fp.read(min(size, SAFEBLOCK)) @@ -558,6 +564,8 @@ def _safe_read(fp, size): break data.append(block) size -= len(block) + if sum(len(d) for d in data) < size: + raise OSError("Truncated File Read") return b"".join(data) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index c48d8983565..2f63ddae6fc 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -669,6 +669,7 @@ def getmask2( ) size = size[0] + stroke_width * 2, size[1] + stroke_width * 2 offset = offset[0] - stroke_width, offset[1] - stroke_width + Image._decompression_bomb_check(size) im = fill("RGBA" if mode == "RGBA" else "L", size, 0) self.font.render( text, im.id, mode, direction, features, language, stroke_width, ink diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index d3799edc3d9..e7b8846741a 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -119,7 +119,8 @@ def _open(self): end = self.fp.tell() + size size = i32(read(4)) if size: - self.layers = _layerinfo(self.fp) + _layer_data = io.BytesIO(ImageFile._safe_read(self.fp, size)) + self.layers = _layerinfo(_layer_data, size) self.fp.seek(end) self.n_frames = len(self.layers) self.is_animated = self.n_frames > 1 @@ -171,11 +172,20 @@ def _close__fp(self): self.__fp = None -def _layerinfo(file): +def _layerinfo(fp, ct_bytes): # read layerinfo block layers = [] - read = file.read - for i in range(abs(i16(read(2)))): + + def read(size): + return ImageFile._safe_read(fp, size) + + ct = i16(read(2)) + + # sanity check + if ct_bytes < (abs(ct) * 20): + raise SyntaxError("Layer block too short for number of layers requested") + + for i in range(abs(ct)): # bounding box y0 = i32(read(4)) @@ -186,7 +196,8 @@ def _layerinfo(file): # image info info = [] mode = [] - types = list(range(i16(read(2)))) + ct_types = i16(read(2)) + types = list(range(ct_types)) if len(types) > 4: continue @@ -219,16 +230,16 @@ def _layerinfo(file): size = i32(read(4)) # length of the extra data field combined = 0 if size: - data_end = file.tell() + size + data_end = fp.tell() + size length = i32(read(4)) if length: - file.seek(length - 16, io.SEEK_CUR) + fp.seek(length - 16, io.SEEK_CUR) combined += length + 4 length = i32(read(4)) if length: - file.seek(length, io.SEEK_CUR) + fp.seek(length, io.SEEK_CUR) combined += length + 4 length = i8(read(1)) @@ -238,7 +249,7 @@ def _layerinfo(file): name = read(length).decode("latin-1", "replace") combined += length + 1 - file.seek(data_end) + fp.seek(data_end) layers.append((name, mode, (x0, y0, x1, y1))) # get tiles @@ -246,7 +257,7 @@ def _layerinfo(file): for name, mode, bbox in layers: tile = [] for m in mode: - t = _maketile(file, m, bbox, 1) + t = _maketile(fp, m, bbox, 1) if t: tile.extend(t) layers[i] = name, mode, bbox, tile diff --git a/src/libImaging/FliDecode.c b/src/libImaging/FliDecode.c index e9000fc99e1..3a6030703ec 100644 --- a/src/libImaging/FliDecode.c +++ b/src/libImaging/FliDecode.c @@ -243,6 +243,11 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt return -1; } advance = I32(ptr); + if (advance == 0 ) { + // If there's no advance, we're in an infinite loop + state->errcode = IMAGING_CODEC_BROKEN; + return -1; + } if (advance < 0 || advance > bytes) { state->errcode = IMAGING_CODEC_OVERRUN; return -1; diff --git a/src/libImaging/Jpeg2KDecode.c b/src/libImaging/Jpeg2KDecode.c index a2a7354dbe0..f086848e9b7 100644 --- a/src/libImaging/Jpeg2KDecode.c +++ b/src/libImaging/Jpeg2KDecode.c @@ -644,7 +644,7 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { j2k_unpacker_t unpack = NULL; size_t buffer_size = 0, tile_bytes = 0; unsigned n, tile_height, tile_width; - int components; + int total_component_width = 0; stream = opj_stream_create(BUFFER_SIZE, OPJ_TRUE); @@ -814,23 +814,40 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { goto quick_exit; } + if (tile_info.nb_comps != image->numcomps) { + state->errcode = IMAGING_CODEC_BROKEN; + state->state = J2K_STATE_FAILED; + goto quick_exit; + } + /* Sometimes the tile_info.datasize we get back from openjpeg - is less than numcomps*w*h, and we overflow in the + is less than sum(comp_bytes)*w*h, and we overflow in the shuffle stage */ tile_width = tile_info.x1 - tile_info.x0; tile_height = tile_info.y1 - tile_info.y0; - components = tile_info.nb_comps == 3 ? 4 : tile_info.nb_comps; - if ((tile_width > UINT_MAX / components) || - (tile_height > UINT_MAX / components) || - (tile_width > UINT_MAX / (tile_height * components)) || - (tile_height > UINT_MAX / (tile_width * components))) { + + /* Total component width = sum (component_width) e.g, it's + legal for an la file to have a 1 byte width for l, and 4 for + a, and then a malicious file could have a smaller tile_bytes + */ + + for (n=0; n < tile_info.nb_comps; n++) { + // see csize /acsize calcs + int csize = (image->comps[n].prec + 7) >> 3; + csize = (csize == 3) ? 4 : csize; + total_component_width += csize; + } + if ((tile_width > UINT_MAX / total_component_width) || + (tile_height > UINT_MAX / total_component_width) || + (tile_width > UINT_MAX / (tile_height * total_component_width)) || + (tile_height > UINT_MAX / (tile_width * total_component_width))) { state->errcode = IMAGING_CODEC_BROKEN; state->state = J2K_STATE_FAILED; goto quick_exit; } - tile_bytes = tile_width * tile_height * components; + tile_bytes = tile_width * tile_height * total_component_width; if (tile_bytes > tile_info.data_size) { tile_info.data_size = tile_bytes;