Skip to content

Commit fa6d8d0

Browse files
[3.12] pythongh-119342: Fix a potential denial of service in plistlib (pythonGH-119343)
Reading a specially prepared small Plist file could cause OOM because file's read(n) preallocates a bytes object for reading the specified amount of data. Now plistlib reads large data by chunks, therefore the upper limit of consumed memory is proportional to the size of the input file. (cherry picked from commit 694922c) Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
1 parent a183a11 commit fa6d8d0

File tree

3 files changed

+59
-14
lines changed

3 files changed

+59
-14
lines changed

Lib/plistlib.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@
7373
PlistFormat = enum.Enum('PlistFormat', 'FMT_XML FMT_BINARY', module=__name__)
7474
globals().update(PlistFormat.__members__)
7575

76+
# Data larger than this will be read in chunks, to prevent extreme
77+
# overallocation.
78+
_MIN_READ_BUF_SIZE = 1 << 20
7679

7780
class UID:
7881
def __init__(self, data):
@@ -499,12 +502,24 @@ def _get_size(self, tokenL):
499502

500503
return tokenL
501504

505+
def _read(self, size):
506+
cursize = min(size, _MIN_READ_BUF_SIZE)
507+
data = self._fp.read(cursize)
508+
while True:
509+
if len(data) != cursize:
510+
raise InvalidFileException
511+
if cursize == size:
512+
return data
513+
delta = min(cursize, size - cursize)
514+
data += self._fp.read(delta)
515+
cursize += delta
516+
502517
def _read_ints(self, n, size):
503-
data = self._fp.read(size * n)
518+
data = self._read(size * n)
504519
if size in _BINARY_FORMAT:
505520
return struct.unpack(f'>{n}{_BINARY_FORMAT[size]}', data)
506521
else:
507-
if not size or len(data) != size * n:
522+
if not size:
508523
raise InvalidFileException()
509524
return tuple(int.from_bytes(data[i: i + size], 'big')
510525
for i in range(0, size * n, size))
@@ -561,22 +576,16 @@ def _read_object(self, ref):
561576

562577
elif tokenH == 0x40: # data
563578
s = self._get_size(tokenL)
564-
result = self._fp.read(s)
565-
if len(result) != s:
566-
raise InvalidFileException()
579+
result = self._read(s)
567580

568581
elif tokenH == 0x50: # ascii string
569582
s = self._get_size(tokenL)
570-
data = self._fp.read(s)
571-
if len(data) != s:
572-
raise InvalidFileException()
583+
data = self._read(s)
573584
result = data.decode('ascii')
574585

575586
elif tokenH == 0x60: # unicode string
576587
s = self._get_size(tokenL) * 2
577-
data = self._fp.read(s)
578-
if len(data) != s:
579-
raise InvalidFileException()
588+
data = self._read(s)
580589
result = data.decode('utf-16be')
581590

582591
elif tokenH == 0x80: # UID

Lib/test/test_plistlib.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -841,8 +841,7 @@ def test_xml_plist_with_entity_decl(self):
841841

842842
class TestBinaryPlistlib(unittest.TestCase):
843843

844-
@staticmethod
845-
def decode(*objects, offset_size=1, ref_size=1):
844+
def build(self, *objects, offset_size=1, ref_size=1):
846845
data = [b'bplist00']
847846
offset = 8
848847
offsets = []
@@ -854,7 +853,11 @@ def decode(*objects, offset_size=1, ref_size=1):
854853
len(objects), 0, offset)
855854
data.extend(offsets)
856855
data.append(tail)
857-
return plistlib.loads(b''.join(data), fmt=plistlib.FMT_BINARY)
856+
return b''.join(data)
857+
858+
def decode(self, *objects, offset_size=1, ref_size=1):
859+
data = self.build(*objects, offset_size=offset_size, ref_size=ref_size)
860+
return plistlib.loads(data, fmt=plistlib.FMT_BINARY)
858861

859862
def test_nonstandard_refs_size(self):
860863
# Issue #21538: Refs and offsets are 24-bit integers
@@ -963,6 +966,34 @@ def test_invalid_binary(self):
963966
with self.assertRaises(plistlib.InvalidFileException):
964967
plistlib.loads(b'bplist00' + data, fmt=plistlib.FMT_BINARY)
965968

969+
def test_truncated_large_data(self):
970+
self.addCleanup(os_helper.unlink, os_helper.TESTFN)
971+
def check(data):
972+
with open(os_helper.TESTFN, 'wb') as f:
973+
f.write(data)
974+
# buffered file
975+
with open(os_helper.TESTFN, 'rb') as f:
976+
with self.assertRaises(plistlib.InvalidFileException):
977+
plistlib.load(f, fmt=plistlib.FMT_BINARY)
978+
# unbuffered file
979+
with open(os_helper.TESTFN, 'rb', buffering=0) as f:
980+
with self.assertRaises(plistlib.InvalidFileException):
981+
plistlib.load(f, fmt=plistlib.FMT_BINARY)
982+
for w in range(20, 64):
983+
s = 1 << w
984+
# data
985+
check(self.build(b'\x4f\x13' + s.to_bytes(8, 'big')))
986+
# ascii string
987+
check(self.build(b'\x5f\x13' + s.to_bytes(8, 'big')))
988+
# unicode string
989+
check(self.build(b'\x6f\x13' + s.to_bytes(8, 'big')))
990+
# array
991+
check(self.build(b'\xaf\x13' + s.to_bytes(8, 'big')))
992+
# dict
993+
check(self.build(b'\xdf\x13' + s.to_bytes(8, 'big')))
994+
# number of objects
995+
check(b'bplist00' + struct.pack('>6xBBQQQ', 1, 1, s, 0, 8))
996+
966997

967998
class TestKeyedArchive(unittest.TestCase):
968999
def test_keyed_archive_data(self):
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fix a potential memory denial of service in the :mod:`plistlib` module.
2+
When reading a Plist file received from untrusted source, it could cause
3+
an arbitrary amount of memory to be allocated.
4+
This could have led to symptoms including a :exc:`MemoryError`, swapping, out
5+
of memory (OOM) killed processes or containers, or even system crashes.

0 commit comments

Comments
 (0)