From 4a6aac605aa388b9b1d39d120db055fe73a5f669 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Sun, 30 Nov 2014 23:31:29 -0800 Subject: [PATCH 1/6] Fix for zlib.decompression bomb in iTXt,zTXt, and iCCP chunks --- PIL/PngImagePlugin.py | 17 ++++++++++------- Tests/check_png_dos.py | 24 ++++++++++++++++++++++++ Tests/images/png_decompression_dos.png | Bin 0 -> 6289 bytes 3 files changed, 34 insertions(+), 7 deletions(-) create mode 100644 Tests/check_png_dos.py create mode 100644 Tests/images/png_decompression_dos.png diff --git a/PIL/PngImagePlugin.py b/PIL/PngImagePlugin.py index 2110aa637..8bfdc2ac7 100644 --- a/PIL/PngImagePlugin.py +++ b/PIL/PngImagePlugin.py @@ -72,6 +72,13 @@ _MODES = { _simple_palette = re.compile(b'^\xff+\x00\xff*$') +def _safe_zlib_decompress(s): + dobj = zlib.decompressobj() + plaintext = dobj.decompress(s, ImageFile.SAFEBLOCK) + if dobj.unconsumed_tail: + raise ValueError("Decompressed Data Too Large") + return plaintext + # -------------------------------------------------------------------- # Support classes. Suitable for PNG and related formats like MNG etc. @@ -184,7 +191,6 @@ class PngInfo: tkey = tkey.encode("utf-8", "strict") if zip: - import zlib self.add(b"iTXt", key + b"\0\x01\0" + lang + b"\0" + tkey + b"\0" + zlib.compress(value)) else: @@ -206,7 +212,6 @@ class PngInfo: key = key.encode('latin-1', 'strict') if zip: - import zlib self.add(b"zTXt", key + b"\0\0" + zlib.compress(value)) else: self.add(b"tEXt", key + b"\0" + value) @@ -247,7 +252,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 @@ -359,9 +364,8 @@ class PngStream(ChunkStream): if comp_method != 0: raise SyntaxError("Unknown compression method %s in zTXt chunk" % comp_method) - import zlib try: - v = zlib.decompress(v[1:]) + v = _safe_zlib_decompress(v[1:]) except zlib.error: v = b"" @@ -390,9 +394,8 @@ class PngStream(ChunkStream): return s if cf != 0: if cm == 0: - import zlib try: - v = zlib.decompress(v) + v = _safe_zlib_decompress(v) except zlib.error: return s else: diff --git a/Tests/check_png_dos.py b/Tests/check_png_dos.py new file mode 100644 index 000000000..4e9b76537 --- /dev/null +++ b/Tests/check_png_dos.py @@ -0,0 +1,24 @@ +from helper import unittest, PillowTestCase +import sys +from PIL import Image +from io import BytesIO + +test_file = "Tests/images/png_decompression_dos.png" + +@unittest.skipIf(sys.platform.startswith('win32'), "requires Unix or MacOS") +class TestPngDos(PillowTestCase): + + def test_dos_text(self): + + try: + im = Image.open(test_file) + im.load() + except ValueError as msg: + self.assert_(msg, "Decompressed Data Too Large") + return + + for s in im.text.values(): + self.assert_(len(s) < 1024*1024, "Text chunk larger than 1M") + +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 0000000000000000000000000000000000000000..986561b2e78e76e92e0a8b07886dc26f6444257b GIT binary patch literal 6289 zcmeAS@N?(olHy`uVBq!ia0vp^j3CSbBp9sfW`{B`aPU=yM3f|DrW-OaRLpsM&=92H z&;qa5^Uq1xIe<8$U^E0qLtr!nMnhmU1V%$(c!xj@@8mO4jE~Ov%|>b&7>%|JhIg!w z`ffA?MnhmU1V%$(Gz4&k0HkHWQYFiY)G{z0Z5iMSuTk;Q5Eu=C(GVC7fzc2c-XQ>K p8N9c+xC7`9Q7kcv6U2|zXz1Ea_KC51p1gQu&X%Q~loCIE&GIWYhL literal 0 HcmV?d00001 From 4738506ecbf809352494f94b551e105357b2b672 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Sun, 28 Dec 2014 22:34:13 -0800 Subject: [PATCH 2/6] Test change -- different representation for invalid compressed object --- Tests/test_file_png.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) From c5c648a6a33372b90e9fbf0c0a7bc4e75bdd238c Mon Sep 17 00:00:00 2001 From: wiredfool Date: Mon, 29 Dec 2014 17:10:27 -0800 Subject: [PATCH 3/6] Limit total text chunk size to 64k --- PIL/PngImagePlugin.py | 23 +++++++++++++++++++++-- Tests/check_png_dos.py | 30 +++++++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/PIL/PngImagePlugin.py b/PIL/PngImagePlugin.py index 8bfdc2ac7..598395c0a 100644 --- a/PIL/PngImagePlugin.py +++ b/PIL/PngImagePlugin.py @@ -72,9 +72,15 @@ _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, ImageFile.SAFEBLOCK) + plaintext = dobj.decompress(s, MAX_TEXT_CHUNK) if dobj.unconsumed_tail: raise ValueError("Decompressed Data Too Large") return plaintext @@ -234,6 +240,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 @@ -346,6 +360,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): @@ -375,6 +391,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): @@ -410,7 +428,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 index 4e9b76537..8f974d293 100644 --- a/Tests/check_png_dos.py +++ b/Tests/check_png_dos.py @@ -1,13 +1,12 @@ from helper import unittest, PillowTestCase import sys -from PIL import Image +from PIL import Image, PngImagePlugin from io import BytesIO +import zlib test_file = "Tests/images/png_decompression_dos.png" -@unittest.skipIf(sys.platform.startswith('win32'), "requires Unix or MacOS") class TestPngDos(PillowTestCase): - def test_dos_text(self): try: @@ -20,5 +19,30 @@ class TestPngDos(PillowTestCase): for s in im.text.values(): self.assert_(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.assert_("Too much memory" in msg) + return + + total_len = 0 + for txt in im2.text.values(): + total_len += len(txt) + self.assert_(total_len < 64*1024*1024) + if __name__ == '__main__': unittest.main() From 43f6291cfd22d3cbcc39711f0ee3f9efdc257539 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Tue, 30 Dec 2014 16:57:24 -0800 Subject: [PATCH 4/6] Test style cleanup --- Tests/check_png_dos.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Tests/check_png_dos.py b/Tests/check_png_dos.py index 8f974d293..c74990a8c 100644 --- a/Tests/check_png_dos.py +++ b/Tests/check_png_dos.py @@ -1,23 +1,22 @@ from helper import unittest, PillowTestCase -import sys from PIL import Image, PngImagePlugin from io import BytesIO import zlib -test_file = "Tests/images/png_decompression_dos.png" +TEST_FILE = "Tests/images/png_decompression_dos.png" class TestPngDos(PillowTestCase): def test_dos_text(self): try: - im = Image.open(test_file) + im = Image.open(TEST_FILE) im.load() except ValueError as msg: - self.assert_(msg, "Decompressed Data Too Large") + self.assertTrue(msg, "Decompressed Data Too Large") return for s in im.text.values(): - self.assert_(len(s) < 1024*1024, "Text chunk larger than 1M") + self.assertLess(len(s), 1024*1024, "Text chunk larger than 1M") def test_dos_total_memory(self): im = Image.new('L',(1,1)) @@ -36,13 +35,13 @@ class TestPngDos(PillowTestCase): try: im2 = Image.open(b) except ValueError as msg: - self.assert_("Too much memory" in msg) + self.assertIn("Too much memory", msg) return total_len = 0 for txt in im2.text.values(): total_len += len(txt) - self.assert_(total_len < 64*1024*1024) + self.assertLess(total_len, 64*1024*1024, "Total text chunks greater than 64M") if __name__ == '__main__': unittest.main() From 61ee1d9ccc9551e668fc00540720b7169194605b Mon Sep 17 00:00:00 2001 From: wiredfool Date: Tue, 30 Dec 2014 17:06:38 -0800 Subject: [PATCH 5/6] Documentation Update for PNG zlib DOS --- docs/handbook/image-file-formats.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 31adcb142..34839caaa 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -332,6 +332,14 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following Transparency color index. This key is omitted if the image is not a 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. 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: **optimize** From cf880329a755f5b5c81df661990105593adf4a37 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Wed, 31 Dec 2014 15:33:06 -0800 Subject: [PATCH 6/6] Version Bump -- 2.6.2 --- CHANGES.rst | 6 ++++++ PIL/__init__.py | 2 +- _imaging.c | 2 +- setup.py | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index fec47bf5b..e1abb0d7d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,12 @@ Changelog (Pillow) ================== +2.6.2 (2015-01-01) +------------------ + +- Fix potential PNG decompression DOS #1060 + [wiredfool] + 2.6.1 (2014-10-13) ------------------ diff --git a/PIL/__init__.py b/PIL/__init__.py index c7a102d08..6de3caef0 100644 --- a/PIL/__init__.py +++ b/PIL/__init__.py @@ -12,7 +12,7 @@ # ;-) VERSION = '1.1.7' # PIL version -PILLOW_VERSION = '2.6.1' # Pillow +PILLOW_VERSION = '2.6.2' # Pillow _plugins = ['BmpImagePlugin', 'BufrStubImagePlugin', diff --git a/_imaging.c b/_imaging.c index a7605b1ee..e963e8c2f 100644 --- a/_imaging.c +++ b/_imaging.c @@ -71,7 +71,7 @@ * See the README file for information on usage and redistribution. */ -#define PILLOW_VERSION "2.6.1" +#define PILLOW_VERSION "2.6.2" #include "Python.h" diff --git a/setup.py b/setup.py index 3b282f1c0..ac41c51ab 100644 --- a/setup.py +++ b/setup.py @@ -90,7 +90,7 @@ except (ImportError, OSError): NAME = 'Pillow' -PILLOW_VERSION = '2.6.1' +PILLOW_VERSION = '2.6.2' TCL_ROOT = None JPEG_ROOT = None JPEG2K_ROOT = None