Skip to content

Commit fc8192b

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 29c657a commit fc8192b

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
@@ -903,8 +903,7 @@ def test_dump_naive_datetime_with_aware_datetime_option(self):
903903

904904
class TestBinaryPlistlib(unittest.TestCase):
905905

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

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

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