Skip to content

Commit c02c7df

Browse files
serhiy-storchakamiss-islington
authored andcommitted
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 6c922bb commit c02c7df

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):
@@ -508,12 +511,24 @@ def _get_size(self, tokenL):
508511

509512
return tokenL
510513

514+
def _read(self, size):
515+
cursize = min(size, _MIN_READ_BUF_SIZE)
516+
data = self._fp.read(cursize)
517+
while True:
518+
if len(data) != cursize:
519+
raise InvalidFileException
520+
if cursize == size:
521+
return data
522+
delta = min(cursize, size - cursize)
523+
data += self._fp.read(delta)
524+
cursize += delta
525+
511526
def _read_ints(self, n, size):
512-
data = self._fp.read(size * n)
527+
data = self._read(size * n)
513528
if size in _BINARY_FORMAT:
514529
return struct.unpack(f'>{n}{_BINARY_FORMAT[size]}', data)
515530
else:
516-
if not size or len(data) != size * n:
531+
if not size:
517532
raise InvalidFileException()
518533
return tuple(int.from_bytes(data[i: i + size], 'big')
519534
for i in range(0, size * n, size))
@@ -573,22 +588,16 @@ def _read_object(self, ref):
573588

574589
elif tokenH == 0x40: # data
575590
s = self._get_size(tokenL)
576-
result = self._fp.read(s)
577-
if len(result) != s:
578-
raise InvalidFileException()
591+
result = self._read(s)
579592

580593
elif tokenH == 0x50: # ascii string
581594
s = self._get_size(tokenL)
582-
data = self._fp.read(s)
583-
if len(data) != s:
584-
raise InvalidFileException()
595+
data = self._read(s)
585596
result = data.decode('ascii')
586597

587598
elif tokenH == 0x60: # unicode string
588599
s = self._get_size(tokenL) * 2
589-
data = self._fp.read(s)
590-
if len(data) != s:
591-
raise InvalidFileException()
600+
data = self._read(s)
592601
result = data.decode('utf-16be')
593602

594603
elif tokenH == 0x80: # UID

Lib/test/test_plistlib.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -904,8 +904,7 @@ def test_dump_naive_datetime_with_aware_datetime_option(self):
904904

905905
class TestBinaryPlistlib(unittest.TestCase):
906906

907-
@staticmethod
908-
def decode(*objects, offset_size=1, ref_size=1):
907+
def build(self, *objects, offset_size=1, ref_size=1):
909908
data = [b'bplist00']
910909
offset = 8
911910
offsets = []
@@ -917,7 +916,11 @@ def decode(*objects, offset_size=1, ref_size=1):
917916
len(objects), 0, offset)
918917
data.extend(offsets)
919918
data.append(tail)
920-
return plistlib.loads(b''.join(data), fmt=plistlib.FMT_BINARY)
919+
return b''.join(data)
920+
921+
def decode(self, *objects, offset_size=1, ref_size=1):
922+
data = self.build(*objects, offset_size=offset_size, ref_size=ref_size)
923+
return plistlib.loads(data, fmt=plistlib.FMT_BINARY)
921924

922925
def test_nonstandard_refs_size(self):
923926
# Issue #21538: Refs and offsets are 24-bit integers
@@ -1025,6 +1028,34 @@ def test_invalid_binary(self):
10251028
with self.assertRaises(plistlib.InvalidFileException):
10261029
plistlib.loads(b'bplist00' + data, fmt=plistlib.FMT_BINARY)
10271030

1031+
def test_truncated_large_data(self):
1032+
self.addCleanup(os_helper.unlink, os_helper.TESTFN)
1033+
def check(data):
1034+
with open(os_helper.TESTFN, 'wb') as f:
1035+
f.write(data)
1036+
# buffered file
1037+
with open(os_helper.TESTFN, 'rb') as f:
1038+
with self.assertRaises(plistlib.InvalidFileException):
1039+
plistlib.load(f, fmt=plistlib.FMT_BINARY)
1040+
# unbuffered file
1041+
with open(os_helper.TESTFN, 'rb', buffering=0) as f:
1042+
with self.assertRaises(plistlib.InvalidFileException):
1043+
plistlib.load(f, fmt=plistlib.FMT_BINARY)
1044+
for w in range(20, 64):
1045+
s = 1 << w
1046+
# data
1047+
check(self.build(b'\x4f\x13' + s.to_bytes(8, 'big')))
1048+
# ascii string
1049+
check(self.build(b'\x5f\x13' + s.to_bytes(8, 'big')))
1050+
# unicode string
1051+
check(self.build(b'\x6f\x13' + s.to_bytes(8, 'big')))
1052+
# array
1053+
check(self.build(b'\xaf\x13' + s.to_bytes(8, 'big')))
1054+
# dict
1055+
check(self.build(b'\xdf\x13' + s.to_bytes(8, 'big')))
1056+
# number of objects
1057+
check(b'bplist00' + struct.pack('>6xBBQQQ', 1, 1, s, 0, 8))
1058+
10281059
def test_load_aware_datetime(self):
10291060
data = (b'bplist003B\x04>\xd0d\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00'
10301061
b'\x01\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00'
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)