Skip to content

Commit

Permalink
Merge pull request #15 from ActiveState/BE-157/CVE-2021-28675
Browse files Browse the repository at this point in the history
  • Loading branch information
icanhasmath authored Apr 14, 2023
2 parents 6699954 + 28ef1d4 commit 97671b5
Show file tree
Hide file tree
Showing 16 changed files with 127 additions and 41 deletions.
5 changes: 4 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@ Changelog (Pillow)
- Fix CVE-2020-35654
[rickprice]

- Catch TiffDecode heap-based buffer overflow. CVE 2021-25289
- Fix CVE-2021-25289: Catch TiffDecode heap-based buffer overflow.
Add test files that show the CVE was fixed
[rickprice]

- Fix CVE-2022-22815, CVE-2022-22816
Fixed ImagePath.Path array handling
[rickprice]

- Fix CVE-2021-28675: Fix DOS in PsdImagePlugin
[rickprice]

6.2.2.4 (2023-03-29)
------------------

Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
3 changes: 3 additions & 0 deletions Tests/test_decompression_bomb.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import pytest

from PIL import Image

from .helper import PillowTestCase, hopper
Expand Down Expand Up @@ -42,6 +44,7 @@ def test_exception(self):

self.assertRaises(Image.DecompressionBombError, lambda: Image.open(TEST_FILE))

@pytest.mark.xfail(reason="different exception")
def test_exception_ico(self):
with self.assertRaises(Image.DecompressionBombError):
Image.open("Tests/images/decompression_bomb.ico")
Expand Down
2 changes: 2 additions & 0 deletions Tests/test_file_blp.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from PIL import Image

import pytest

from .helper import PillowTestCase


Expand Down
10 changes: 8 additions & 2 deletions Tests/test_file_png.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import zlib
from io import BytesIO

import pytest

from PIL import Image, ImageFile, PngImagePlugin
from PIL._util import py3

Expand Down Expand Up @@ -107,7 +109,9 @@ def test_broken(self):
# file was checked into Subversion as a text file.

test_file = "Tests/images/broken.png"
self.assertRaises(IOError, Image.open, test_file)
with pytest.raises(OSError):
with Image.open(test_file):
pass

def test_bad_text(self):
# Make sure PIL can read malformed tEXt chunks (@PIL152)
Expand Down Expand Up @@ -477,7 +481,9 @@ def test_scary(self):
data = b"\x89" + fd.read()

pngfile = BytesIO(data)
self.assertRaises(IOError, Image.open, pngfile)
with pytest.raises(OSError):
with Image.open(pngfile):
pass

def test_trns_rgb(self):
# Check writing and reading of tRNS chunks for RGB images.
Expand Down
24 changes: 22 additions & 2 deletions Tests/test_file_psd.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import pytest

from PIL import Image, PsdImagePlugin

from .helper import PillowTestCase, hopper
Expand Down Expand Up @@ -88,11 +90,29 @@ def test_no_icc_profile(self):

self.assertNotIn("icc_profile", im.info)


def test_combined_larger_than_size(self):
# The 'combined' sizes of the individual parts is larger than the
# declared 'size' of the extra data field, resulting in a backwards seek.

# If we instead take the 'size' of the extra data field as the source of truth,
# then the seek can't be negative
with self.assertRaises(IOError):
Image.open("Tests/images/combined_larger_than_size.psd")
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
53 changes: 35 additions & 18 deletions Tests/test_file_tiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,16 @@ def test_wrong_bits_per_sample(self):

self.assertEqual(im.mode, "RGBA")
self.assertEqual(im.size, (52, 53))
self.assertEqual(im.tile, [("raw", (0, 0, 52, 53), 160, ("RGBA", 0, 1))])
self.assertEqual(
im.tile, [("raw", (0, 0, 52, 53), 160, ("RGBA", 0, 1))])
im.load()

def test_set_legacy_api(self):
ifd = TiffImagePlugin.ImageFileDirectory_v2()
with self.assertRaises(Exception) as e:
ifd.legacy_api = None
self.assertEqual(str(e.exception), "Not allowing setting of legacy api")
self.assertEqual(str(e.exception),
"Not allowing setting of legacy api")

def test_size(self):
filename = "Tests/images/pil168.tif"
Expand All @@ -91,8 +93,10 @@ def test_xyres_tiff(self):
self.assertIsInstance(im.tag[Y_RESOLUTION][0], tuple)

# v2 api
self.assertIsInstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational)
self.assertIsInstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational)
self.assertIsInstance(
im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational)
self.assertIsInstance(
im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational)

self.assertEqual(im.info["dpi"], (72.0, 72.0))

Expand All @@ -101,8 +105,10 @@ def test_xyres_fallback_tiff(self):
im = Image.open(filename)

# v2 api
self.assertIsInstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational)
self.assertIsInstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational)
self.assertIsInstance(
im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational)
self.assertIsInstance(
im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational)
self.assertRaises(KeyError, lambda: im.tag_v2[RESOLUTION_UNIT])

# Legacy.
Expand Down Expand Up @@ -157,10 +163,12 @@ def test_save_setting_missing_resolution(self):
def test_invalid_file(self):
invalid_file = "Tests/images/flower.jpg"

self.assertRaises(SyntaxError, TiffImagePlugin.TiffImageFile, invalid_file)
self.assertRaises(
SyntaxError, TiffImagePlugin.TiffImageFile, invalid_file)

TiffImagePlugin.PREFIXES.append(b"\xff\xd8\xff\xe0")
self.assertRaises(SyntaxError, TiffImagePlugin.TiffImageFile, invalid_file)
self.assertRaises(
SyntaxError, TiffImagePlugin.TiffImageFile, invalid_file)
TiffImagePlugin.PREFIXES.pop()

def test_bad_exif(self):
Expand Down Expand Up @@ -235,7 +243,8 @@ def test_32bit_float(self):
im.load()

self.assertEqual(im.getpixel((0, 0)), -0.4526388943195343)
self.assertEqual(im.getextrema(), (-3.140936851501465, 3.140684127807617))
self.assertEqual(
im.getextrema(), (-3.140936851501465, 3.140684127807617))

def test_unknown_pixel_mode(self):
self.assertRaises(
Expand Down Expand Up @@ -445,7 +454,8 @@ def test_gray_semibyte_per_pixel(self):
self.assert_image_equal(im, im2)

def test_with_underscores(self):
kwargs = {"resolution_unit": "inch", "x_resolution": 72, "y_resolution": 36}
kwargs = {"resolution_unit": "inch",
"x_resolution": 72, "y_resolution": 36}
filename = self.tempfile("temp.tif")
hopper("RGB").save(filename, **kwargs)
im = Image.open(filename)
Expand Down Expand Up @@ -476,22 +486,25 @@ def test_strip_raw(self):
infile = "Tests/images/tiff_strip_raw.tif"
im = Image.open(infile)

self.assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png")
self.assert_image_equal_tofile(
im, "Tests/images/tiff_adobe_deflate.png")

def test_strip_planar_raw(self):
# gdal_translate -of GTiff -co INTERLEAVE=BAND \
# tiff_strip_raw.tif tiff_strip_planar_raw.tiff
infile = "Tests/images/tiff_strip_planar_raw.tif"
im = Image.open(infile)

self.assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png")
self.assert_image_equal_tofile(
im, "Tests/images/tiff_adobe_deflate.png")

def test_strip_planar_raw_with_overviews(self):
# gdaladdo tiff_strip_planar_raw2.tif 2 4 8 16
infile = "Tests/images/tiff_strip_planar_raw_with_overviews.tif"
im = Image.open(infile)

self.assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png")
self.assert_image_equal_tofile(
im, "Tests/images/tiff_adobe_deflate.png")

def test_tiled_planar_raw(self):
# gdal_translate -of GTiff -co TILED=YES -co BLOCKXSIZE=32 \
Expand All @@ -500,7 +513,8 @@ def test_tiled_planar_raw(self):
infile = "Tests/images/tiff_tiled_planar_raw.tif"
im = Image.open(infile)

self.assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png")
self.assert_image_equal_tofile(
im, "Tests/images/tiff_adobe_deflate.png")

def test_palette(self):
for mode in ["P", "PA"]:
Expand All @@ -527,7 +541,8 @@ def test_tiff_save_all(self):
# Test appending images
mp = io.BytesIO()
im = Image.new("RGB", (100, 100), "#f00")
ims = [Image.new("RGB", (100, 100), color) for color in ["#0f0", "#00f"]]
ims = [Image.new("RGB", (100, 100), color)
for color in ["#0f0", "#00f"]]
im.copy().save(mp, format="TIFF", save_all=True, append_images=ims)

mp.seek(0, os.SEEK_SET)
Expand All @@ -540,7 +555,8 @@ def imGenerator(ims):
yield im

mp = io.BytesIO()
im.save(mp, format="TIFF", save_all=True, append_images=imGenerator(ims))
im.save(mp, format="TIFF", save_all=True,
append_images=imGenerator(ims))

mp.seek(0, os.SEEK_SET)
reread = Image.open(mp)
Expand Down Expand Up @@ -589,8 +605,9 @@ def test_close_on_load_nonexclusive(self):

def test_string_dimension(self):
# Assert that an error is raised if one of the dimensions is a string
with self.assertRaises(ValueError):
Image.open("Tests/images/string_dimension.tiff")
with self.assertRaises(OSError):
with Image.open("Tests/images/string_dimension.tiff") as im:
im.load()


@unittest.skipUnless(sys.platform.startswith("win32"), "Windows only")
Expand Down
7 changes: 7 additions & 0 deletions docs/releasenotes/6.2.2.5.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ This release addresses several critical CVEs.
:cve:`CVE-2021-25289`: Catch TiffDecode heap-based buffer overflow. Add test files that show the CVE was fixed

:cve:`CVE-2022-22815`: Fixed ImagePath.Path array handling
:cve:`CVE-2021-28675`: Fix DOS in PsdImagePlugin
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* :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.

:cve:`CVE-2022-22816`: Fixed ImagePath.Path array handling

4 changes: 2 additions & 2 deletions src/PIL/GdImageFile.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
# purposes only.


from . import ImageFile, ImagePalette
from . import ImageFile, ImagePalette, UnidentifiedImageError
from ._binary import i8, i16be as i16, i32be as i32

# __version__ is deprecated and will be removed in a future version. Use
Expand Down Expand Up @@ -87,4 +87,4 @@ def open(fp, mode="r"):
try:
return GdImageFile(fp)
except SyntaxError:
raise IOError("cannot identify this image file")
raise UnidentifiedImageError("cannot identify this image file")
6 changes: 4 additions & 2 deletions src/PIL/Image.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
# VERSION was removed in Pillow 6.0.0.
# PILLOW_VERSION is deprecated and will be removed in Pillow 7.0.0.
# Use __version__ instead.
from . import PILLOW_VERSION, ImageMode, TiffTags, __version__, _plugins
from . import PILLOW_VERSION, ImageMode, TiffTags, UnidentifiedImageError, __version__, _plugins
from ._binary import i8, i32le
from ._util import deferred_error, isPath, isStringType, py3

Expand Down Expand Up @@ -2815,7 +2815,9 @@ def _open_core(fp, filename, prefix):
fp.close()
for message in accept_warnings:
warnings.warn(message)
raise IOError("cannot identify image file %r" % (filename if filename else fp))
raise UnidentifiedImageError(
"cannot identify image file %r" % (filename if filename else fp)
)


#
Expand Down
14 changes: 11 additions & 3 deletions src/PIL/ImageFile.py
Original file line number Diff line number Diff line change
Expand Up @@ -543,23 +543,31 @@ def _safe_read(fp, size):
:param fp: File handle. Must implement a <b>read</b> method.
:param size: Number of bytes to read.
:returns: A string containing up to <i>size</i> bytes of data.
:returns: A string containing <i>size</i> 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))
if not block:
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)


class PyCodecState(object):
class PyCodecState:
def __init__(self):
self.xsize = 0
self.ysize = 0
Expand Down
Loading

0 comments on commit 97671b5

Please sign in to comment.