mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-08-13 16:54:45 +03:00
BE-157-CVE-2021-28675
Fix DOS in PSDImagePlugin -- CVE-2021-28675
* PSDImagePlugin did not sanity check the number of input layers and
vs the size of the data block, this could lead to a DOS on
Image.open prior to Image.load.
* This issue dates to the PIL fork
Apply fix 8febdad8dd
This commit is contained in:
parent
6699954baa
commit
28ef1d46ad
|
@ -8,7 +8,7 @@ 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]
|
||||
|
||||
|
@ -16,6 +16,9 @@ Changelog (Pillow)
|
|||
Fixed ImagePath.Path array handling
|
||||
[rickprice]
|
||||
|
||||
- Fix CVE-2021-28675: Fix DOS in PsdImagePlugin
|
||||
[rickprice]
|
||||
|
||||
6.2.2.4 (2023-03-29)
|
||||
------------------
|
||||
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,3 +1,5 @@
|
|||
import pytest
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from .helper import PillowTestCase, hopper
|
||||
|
@ -42,6 +44,7 @@ class TestDecompressionBomb(PillowTestCase):
|
|||
|
||||
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")
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
from PIL import Image
|
||||
|
||||
import pytest
|
||||
|
||||
from .helper import PillowTestCase
|
||||
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@ import sys
|
|||
import zlib
|
||||
from io import BytesIO
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, ImageFile, PngImagePlugin
|
||||
from PIL._util import py3
|
||||
|
||||
|
@ -107,7 +109,9 @@ class TestFilePng(PillowTestCase):
|
|||
# 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)
|
||||
|
@ -477,7 +481,9 @@ class TestFilePng(PillowTestCase):
|
|||
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.
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import pytest
|
||||
|
||||
from PIL import Image, PsdImagePlugin
|
||||
|
||||
from .helper import PillowTestCase, hopper
|
||||
|
@ -88,11 +90,29 @@ class TestImagePsd(PillowTestCase):
|
|||
|
||||
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
|
||||
|
|
|
@ -64,14 +64,16 @@ class TestFileTiff(PillowTestCase):
|
|||
|
||||
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"
|
||||
|
@ -91,8 +93,10 @@ class TestFileTiff(PillowTestCase):
|
|||
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))
|
||||
|
||||
|
@ -101,8 +105,10 @@ class TestFileTiff(PillowTestCase):
|
|||
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.
|
||||
|
@ -157,10 +163,12 @@ class TestFileTiff(PillowTestCase):
|
|||
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):
|
||||
|
@ -235,7 +243,8 @@ class TestFileTiff(PillowTestCase):
|
|||
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(
|
||||
|
@ -445,7 +454,8 @@ class TestFileTiff(PillowTestCase):
|
|||
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)
|
||||
|
@ -476,7 +486,8 @@ class TestFileTiff(PillowTestCase):
|
|||
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 \
|
||||
|
@ -484,14 +495,16 @@ class TestFileTiff(PillowTestCase):
|
|||
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 \
|
||||
|
@ -500,7 +513,8 @@ class TestFileTiff(PillowTestCase):
|
|||
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"]:
|
||||
|
@ -527,7 +541,8 @@ class TestFileTiff(PillowTestCase):
|
|||
# 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)
|
||||
|
@ -540,7 +555,8 @@ class TestFileTiff(PillowTestCase):
|
|||
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)
|
||||
|
@ -589,8 +605,9 @@ class TestFileTiff(PillowTestCase):
|
|||
|
||||
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")
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
@ -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")
|
||||
|
|
|
@ -37,7 +37,7 @@ import warnings
|
|||
# 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
|
||||
|
||||
|
@ -2815,7 +2815,9 @@ def open(fp, mode="r"):
|
|||
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)
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
|
|
|
@ -543,12 +543,18 @@ 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))
|
||||
|
@ -556,10 +562,12 @@ 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)
|
||||
|
||||
|
||||
class PyCodecState(object):
|
||||
class PyCodecState:
|
||||
def __init__(self):
|
||||
self.xsize = 0
|
||||
self.ysize = 0
|
||||
|
|
|
@ -121,7 +121,8 @@ class PsdImageFile(ImageFile.ImageFile):
|
|||
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)
|
||||
|
||||
#
|
||||
|
@ -178,12 +179,20 @@ class PsdImageFile(ImageFile.ImageFile):
|
|||
finally:
|
||||
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))
|
||||
|
@ -194,7 +203,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
|
||||
|
||||
|
@ -227,16 +237,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))
|
||||
|
@ -246,7 +256,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
|
||||
|
@ -254,7 +264,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
|
||||
|
|
|
@ -71,3 +71,11 @@ _plugins = [
|
|||
"XpmImagePlugin",
|
||||
"XVThumbImagePlugin",
|
||||
]
|
||||
|
||||
|
||||
class UnidentifiedImageError(OSError):
|
||||
"""
|
||||
Raised in :py:meth:`PIL.Image.open` if an image cannot be opened and identified.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
|
Loading…
Reference in New Issue
Block a user