Skip to content

Commit

Permalink
Merge pull request #5377 from hugovk/security-and-release-notes
Browse files Browse the repository at this point in the history
Security fixes for 8.2.0
  • Loading branch information
hugovk authored Apr 1, 2021
2 parents ef5f294 + 694c84f commit ee635be
Show file tree
Hide file tree
Showing 36 changed files with 285 additions and 90 deletions.
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading

0 comments on commit ee635be

Please sign in to comment.