Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Security fixes for 8.2.0 #5377

Merged
merged 10 commits into from Apr 1, 2021
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1 change: 1 addition & 0 deletions Tests/test_decompression_bomb.py
Expand Up @@ -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"):
Expand Down
2 changes: 1 addition & 1 deletion Tests/test_file_apng.py
Expand Up @@ -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()
Expand Down
21 changes: 21 additions & 0 deletions Tests/test_file_blp.py
@@ -1,3 +1,5 @@
import pytest

from PIL import Image

from .helper import assert_image_equal_tofile
Expand All @@ -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()
12 changes: 12 additions & 0 deletions Tests/test_file_eps.py
Expand Up @@ -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
15 changes: 15 additions & 0 deletions Tests/test_file_fli.py
Expand Up @@ -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()
16 changes: 16 additions & 0 deletions Tests/test_file_jpeg2k.py
Expand Up @@ -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()
22 changes: 22 additions & 0 deletions Tests/test_file_psd.py
Expand Up @@ -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
6 changes: 3 additions & 3 deletions Tests/test_file_tiff.py
Expand Up @@ -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")
Expand Down
13 changes: 13 additions & 0 deletions Tests/test_imagefont.py
Expand Up @@ -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")
134 changes: 92 additions & 42 deletions docs/releasenotes/8.2.0.rst
Expand Up @@ -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
^^^^^^^^^^

Expand All @@ -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
===========

Expand Down Expand Up @@ -48,14 +48,28 @@ 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.

API Additions
=============

getxmp() for JPEG images
^^^^^^^^^^^^^^^^^^^^^^^^

A new method has been added to return
`XMP data <https://en.wikipedia.org/wiki/Extensible_Metadata_Platform>`_ 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
^^^^^^^^^^^^^^^^^^^^^^^^^^^

Expand All @@ -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
^^^^^^^^^^^^^^^^^^^^^^^^^
Expand All @@ -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
^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Expand All @@ -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 <https://en.wikipedia.org/wiki/Extensible_Metadata_Platform>`_ 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
=============
Expand All @@ -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
^^^^^^^^^^^^^^^^^^^^^^^^^^^

Expand All @@ -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