Merge pull request #1060 from wiredfool/png-dos

Fix potential PNG decompression DOS
This commit is contained in:
Hugo 2014-12-31 20:32:41 +02:00
commit b3e09122e5
5 changed files with 84 additions and 6 deletions

View File

@ -72,6 +72,19 @@ _MODES = {
_simple_palette = re.compile(b'^\xff+\x00\xff*$') _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. # Support classes. Suitable for PNG and related formats like MNG etc.
@ -260,6 +273,14 @@ class PngStream(ChunkStream):
self.im_tile = None self.im_tile = None
self.im_palette = 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): def chunk_iCCP(self, pos, length):
# ICC profile # ICC profile
@ -278,7 +299,7 @@ class PngStream(ChunkStream):
raise SyntaxError("Unknown compression method %s in iCCP chunk" % raise SyntaxError("Unknown compression method %s in iCCP chunk" %
comp_method) comp_method)
try: try:
icc_profile = zlib.decompress(s[i+2:]) icc_profile = _safe_zlib_decompress(s[i+2:])
except zlib.error: except zlib.error:
icc_profile = None # FIXME icc_profile = None # FIXME
self.im_info["icc_profile"] = icc_profile self.im_info["icc_profile"] = icc_profile
@ -372,6 +393,8 @@ class PngStream(ChunkStream):
v = v.decode('latin-1', 'replace') v = v.decode('latin-1', 'replace')
self.im_info[k] = self.im_text[k] = v self.im_info[k] = self.im_text[k] = v
self.check_text_memory(len(v))
return s return s
def chunk_zTXt(self, pos, length): def chunk_zTXt(self, pos, length):
@ -391,7 +414,7 @@ class PngStream(ChunkStream):
raise SyntaxError("Unknown compression method %s in zTXt chunk" % raise SyntaxError("Unknown compression method %s in zTXt chunk" %
comp_method) comp_method)
try: try:
v = zlib.decompress(v[1:]) v = _safe_zlib_decompress(v[1:])
except zlib.error: except zlib.error:
v = b"" v = b""
@ -401,6 +424,8 @@ class PngStream(ChunkStream):
v = v.decode('latin-1', 'replace') v = v.decode('latin-1', 'replace')
self.im_info[k] = self.im_text[k] = v self.im_info[k] = self.im_text[k] = v
self.check_text_memory(len(v))
return s return s
def chunk_iTXt(self, pos, length): def chunk_iTXt(self, pos, length):
@ -421,7 +446,7 @@ class PngStream(ChunkStream):
if cf != 0: if cf != 0:
if cm == 0: if cm == 0:
try: try:
v = zlib.decompress(v) v = _safe_zlib_decompress(v)
except zlib.error: except zlib.error:
return s return s
else: else:
@ -436,6 +461,7 @@ class PngStream(ChunkStream):
return s return s
self.im_info[k] = self.im_text[k] = iTXt(v, lang, tk) self.im_info[k] = self.im_text[k] = iTXt(v, lang, tk)
self.check_text_memory(len(v))
return s return s

47
Tests/check_png_dos.py Normal file
View File

@ -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()

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@ -153,7 +153,7 @@ class TestFilePng(PillowTestCase):
im = load(HEAD + chunk(b'iTXt', b'spam\0\1\0en\0Spam\0' + im = load(HEAD + chunk(b'iTXt', b'spam\0\1\0en\0Spam\0' +
zlib.compress(b"egg")[:1]) + TAIL) 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' + im = load(HEAD + chunk(b'iTXt', b'spam\0\1\1en\0Spam\0' +
zlib.compress(b"egg")) + TAIL) zlib.compress(b"egg")) + TAIL)

View File

@ -333,7 +333,12 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following
transparent palette image. transparent palette image.
``Open`` also sets ``Image.text`` to a list of the values of the ``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: The :py:meth:`~PIL.Image.Image.save` method supports the following options: