diff --git a/PIL/PngImagePlugin.py b/PIL/PngImagePlugin.py index 8403461be..7a9becd3b 100644 --- a/PIL/PngImagePlugin.py +++ b/PIL/PngImagePlugin.py @@ -72,6 +72,19 @@ _MODES = { _simple_palette = re.compile(b'^\xff+\x00\xff*$') +# Maximum decompressed size for a iTXt or zTXt chunk. +# Eliminates decompression bombs where compressed chunks can expand 1000x +MAX_TEXT_CHUNK = ImageFile.SAFEBLOCK +# Set the maximum total text chunk size. +MAX_TEXT_MEMORY = 64 * MAX_TEXT_CHUNK + +def _safe_zlib_decompress(s): + dobj = zlib.decompressobj() + plaintext = dobj.decompress(s, MAX_TEXT_CHUNK) + if dobj.unconsumed_tail: + raise ValueError("Decompressed Data Too Large") + return plaintext + # -------------------------------------------------------------------- # Support classes. Suitable for PNG and related formats like MNG etc. @@ -260,6 +273,14 @@ class PngStream(ChunkStream): self.im_tile = None self.im_palette = None + self.text_memory = 0 + + def check_text_memory(self, chunklen): + self.text_memory += chunklen + if self.text_memory > MAX_TEXT_MEMORY: + raise ValueError("Too much memory used in text chunks: %s>MAX_TEXT_MEMORY" % + self.text_memory) + def chunk_iCCP(self, pos, length): # ICC profile @@ -278,7 +299,7 @@ class PngStream(ChunkStream): raise SyntaxError("Unknown compression method %s in iCCP chunk" % comp_method) try: - icc_profile = zlib.decompress(s[i+2:]) + icc_profile = _safe_zlib_decompress(s[i+2:]) except zlib.error: icc_profile = None # FIXME self.im_info["icc_profile"] = icc_profile @@ -372,6 +393,8 @@ class PngStream(ChunkStream): v = v.decode('latin-1', 'replace') self.im_info[k] = self.im_text[k] = v + self.check_text_memory(len(v)) + return s def chunk_zTXt(self, pos, length): @@ -391,7 +414,7 @@ class PngStream(ChunkStream): raise SyntaxError("Unknown compression method %s in zTXt chunk" % comp_method) try: - v = zlib.decompress(v[1:]) + v = _safe_zlib_decompress(v[1:]) except zlib.error: v = b"" @@ -401,6 +424,8 @@ class PngStream(ChunkStream): v = v.decode('latin-1', 'replace') self.im_info[k] = self.im_text[k] = v + self.check_text_memory(len(v)) + return s def chunk_iTXt(self, pos, length): @@ -421,7 +446,7 @@ class PngStream(ChunkStream): if cf != 0: if cm == 0: try: - v = zlib.decompress(v) + v = _safe_zlib_decompress(v) except zlib.error: return s else: @@ -436,7 +461,8 @@ class PngStream(ChunkStream): return s self.im_info[k] = self.im_text[k] = iTXt(v, lang, tk) - + self.check_text_memory(len(v)) + return s diff --git a/Tests/check_png_dos.py b/Tests/check_png_dos.py new file mode 100644 index 000000000..c74990a8c --- /dev/null +++ b/Tests/check_png_dos.py @@ -0,0 +1,47 @@ +from helper import unittest, PillowTestCase +from PIL import Image, PngImagePlugin +from io import BytesIO +import zlib + +TEST_FILE = "Tests/images/png_decompression_dos.png" + +class TestPngDos(PillowTestCase): + def test_dos_text(self): + + try: + im = Image.open(TEST_FILE) + im.load() + except ValueError as msg: + self.assertTrue(msg, "Decompressed Data Too Large") + return + + for s in im.text.values(): + self.assertLess(len(s), 1024*1024, "Text chunk larger than 1M") + + def test_dos_total_memory(self): + im = Image.new('L',(1,1)) + compressed_data = zlib.compress('a'*1024*1023) + + info = PngImagePlugin.PngInfo() + + for x in range(64): + info.add_text('t%s'%x, compressed_data, 1) + info.add_itxt('i%s'%x, compressed_data, zip=True) + + b = BytesIO() + im.save(b, 'PNG', pnginfo=info) + b.seek(0) + + try: + im2 = Image.open(b) + except ValueError as msg: + self.assertIn("Too much memory", msg) + return + + total_len = 0 + for txt in im2.text.values(): + total_len += len(txt) + self.assertLess(total_len, 64*1024*1024, "Total text chunks greater than 64M") + +if __name__ == '__main__': + unittest.main() diff --git a/Tests/images/png_decompression_dos.png b/Tests/images/png_decompression_dos.png new file mode 100644 index 000000000..986561b2e Binary files /dev/null and b/Tests/images/png_decompression_dos.png differ diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 7a43414eb..b556199f5 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -153,7 +153,7 @@ class TestFilePng(PillowTestCase): im = load(HEAD + chunk(b'iTXt', b'spam\0\1\0en\0Spam\0' + zlib.compress(b"egg")[:1]) + TAIL) - self.assertEqual(im.info, {}) + self.assertEqual(im.info, {'spam':''}) im = load(HEAD + chunk(b'iTXt', b'spam\0\1\1en\0Spam\0' + zlib.compress(b"egg")) + TAIL) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index c9172f85f..b440f7ac9 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -333,7 +333,12 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following transparent palette image. ``Open`` also sets ``Image.text`` to a list of the values of the -``tEXt``, ``zTXt``, and ``iTXt`` chunks of the PNG image. +``tEXt``, ``zTXt``, and ``iTXt`` chunks of the PNG image. Individual +compressed chunks are limited to a decompressed size of +``PngImagePlugin.MAX_TEXT_CHUNK``, by default 1MB, to prevent +decompression bombs. Additionally, the total size of all of the text +chunks is limited to ``PngImagePlugin.MAX_TEXT_MEMORY``, defaulting to +64MB. The :py:meth:`~PIL.Image.Image.save` method supports the following options: