Skip to content

Commit 6969658

Browse files
gh-119342: Fix OOM vulnerability in plistlib
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.
1 parent e870c85 commit 6969658

File tree

3 files changed

+60
-14
lines changed

3 files changed

+60
-14
lines changed

Lib/plistlib.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -508,12 +508,31 @@ def _get_size(self, tokenL):
508508

509509
return tokenL
510510

511+
def _read(self, size):
512+
if 0:
513+
data = self._fp.read(size)
514+
if len(data) != size:
515+
raise InvalidFileException
516+
return data
517+
cursize = min(size, 1 << 20)
518+
data = self._fp.read(cursize)
519+
while True:
520+
if len(data) != cursize:
521+
raise InvalidFileException
522+
if cursize == size:
523+
return data
524+
delta = min(cursize, size - cursize)
525+
data += self._fp.read(delta)
526+
cursize += delta
527+
511528
def _read_ints(self, n, size):
512-
data = self._fp.read(size * n)
529+
data = self._read(size * n)
530+
#print('XXX', n, size)
531+
#assert n < 100
513532
if size in _BINARY_FORMAT:
514533
return struct.unpack(f'>{n}{_BINARY_FORMAT[size]}', data)
515534
else:
516-
if not size or len(data) != size * n:
535+
if not size:
517536
raise InvalidFileException()
518537
return tuple(int.from_bytes(data[i: i + size], 'big')
519538
for i in range(0, size * n, size))
@@ -573,22 +592,16 @@ def _read_object(self, ref):
573592

574593
elif tokenH == 0x40: # data
575594
s = self._get_size(tokenL)
576-
result = self._fp.read(s)
577-
if len(result) != s:
578-
raise InvalidFileException()
595+
result = self._read(s)
579596

580597
elif tokenH == 0x50: # ascii string
581598
s = self._get_size(tokenL)
582-
data = self._fp.read(s)
583-
if len(data) != s:
584-
raise InvalidFileException()
599+
data = self._read(s)
585600
result = data.decode('ascii')
586601

587602
elif tokenH == 0x60: # unicode string
588603
s = self._get_size(tokenL) * 2
589-
data = self._fp.read(s)
590-
if len(data) != s:
591-
raise InvalidFileException()
604+
data = self._read(s)
592605
result = data.decode('utf-16be')
593606

594607
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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix OOM vulnerability in :mod:`plistlib`, when reading a specially prepared
2+
small Plist file could cause consuming an arbitrary amount of memory.

0 commit comments

Comments
 (0)