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-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-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-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/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..f21e4edc512 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -1,4 +1,5 @@ from PIL import Image +import pytest from .helper import assert_image_equal_tofile diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 87373d2c4fc..8c58310bdb7 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -130,3 +130,18 @@ 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..1bc46ee308c 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -625,9 +625,10 @@ 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 pytest.raises(OSError): + with Image.open("Tests/images/string_dimension.tiff") as im: + im.load() + @pytest.mark.skipif(not is_win32(), reason="Windows only") diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index f58de95bd68..2ed1520fd1a 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -545,12 +545,18 @@ def _safe_read(fp, size): :param fp: File handle. Must implement a 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 can not 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,9 +564,13 @@ 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: def __init__(self): self.xsize = 0 diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index d3799edc3d9..96de58fe7a3 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 @@ -170,12 +171,20 @@ def _close__fp(self): 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)) @@ -186,7 +195,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 +229,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 +248,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 +256,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