Skip to content

Commit e99059d

Browse files
[3.10] 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 1173f80 commit e99059d

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
@@ -838,8 +838,7 @@ def test_xml_plist_with_entity_decl(self):
838838

839839
class TestBinaryPlistlib(unittest.TestCase):
840840

841-
@staticmethod
842-
def decode(*objects, offset_size=1, ref_size=1):
841+
def build(self, *objects, offset_size=1, ref_size=1):
843842
data = [b'bplist00']
844843
offset = 8
845844
offsets = []
@@ -851,7 +850,11 @@ def decode(*objects, offset_size=1, ref_size=1):
851850
len(objects), 0, offset)
852851
data.extend(offsets)
853852
data.append(tail)
854-
return plistlib.loads(b''.join(data), fmt=plistlib.FMT_BINARY)
853+
return b''.join(data)
854+
855+
def decode(self, *objects, offset_size=1, ref_size=1):
856+
data = self.build(*objects, offset_size=offset_size, ref_size=ref_size)
857+
return plistlib.loads(data, fmt=plistlib.FMT_BINARY)
855858

856859
def test_nonstandard_refs_size(self):
857860
# Issue #21538: Refs and offsets are 24-bit integers
@@ -959,6 +962,34 @@ def test_invalid_binary(self):
959962
with self.assertRaises(plistlib.InvalidFileException):
960963
plistlib.loads(b'bplist00' + data, fmt=plistlib.FMT_BINARY)
961964

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

963994
class TestKeyedArchive(unittest.TestCase):
964995
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)