forked from pool/python39
160 lines
6.6 KiB
Diff
160 lines
6.6 KiB
Diff
From e99059d800b741504ef18693803927a0dc062be4 Mon Sep 17 00:00:00 2001
|
|
From: Serhiy Storchaka <storchaka@gmail.com>
|
|
Date: Mon, 1 Dec 2025 17:28:15 +0200
|
|
Subject: [PATCH] [3.10] gh-119342: Fix a potential denial of service in
|
|
plistlib (GH-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 694922cf40aa3a28f898b5f5ee08b71b4922df70)
|
|
|
|
Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
|
|
---
|
|
Lib/plistlib.py | 31 +++++---
|
|
Lib/test/test_plistlib.py | 37 +++++++++-
|
|
Misc/NEWS.d/next/Security/2024-05-21-22-11-31.gh-issue-119342.BTFj4Z.rst | 5 +
|
|
3 files changed, 59 insertions(+), 14 deletions(-)
|
|
create mode 100644 Misc/NEWS.d/next/Security/2024-05-21-22-11-31.gh-issue-119342.BTFj4Z.rst
|
|
|
|
Index: Python-3.9.25/Lib/plistlib.py
|
|
===================================================================
|
|
--- Python-3.9.25.orig/Lib/plistlib.py 2025-12-23 23:47:30.450823742 +0100
|
|
+++ Python-3.9.25/Lib/plistlib.py 2025-12-23 23:49:03.726727983 +0100
|
|
@@ -64,6 +64,9 @@
|
|
PlistFormat = enum.Enum('PlistFormat', 'FMT_XML FMT_BINARY', module=__name__)
|
|
globals().update(PlistFormat.__members__)
|
|
|
|
+# Data larger than this will be read in chunks, to prevent extreme
|
|
+# overallocation.
|
|
+_MIN_READ_BUF_SIZE = 1 << 20
|
|
|
|
class UID:
|
|
def __init__(self, data):
|
|
@@ -490,12 +493,24 @@
|
|
|
|
return tokenL
|
|
|
|
+ def _read(self, size):
|
|
+ cursize = min(size, _MIN_READ_BUF_SIZE)
|
|
+ data = self._fp.read(cursize)
|
|
+ while True:
|
|
+ if len(data) != cursize:
|
|
+ raise InvalidFileException
|
|
+ if cursize == size:
|
|
+ return data
|
|
+ delta = min(cursize, size - cursize)
|
|
+ data += self._fp.read(delta)
|
|
+ cursize += delta
|
|
+
|
|
def _read_ints(self, n, size):
|
|
- data = self._fp.read(size * n)
|
|
+ data = self._read(size * n)
|
|
if size in _BINARY_FORMAT:
|
|
return struct.unpack(f'>{n}{_BINARY_FORMAT[size]}', data)
|
|
else:
|
|
- if not size or len(data) != size * n:
|
|
+ if not size:
|
|
raise InvalidFileException()
|
|
return tuple(int.from_bytes(data[i: i + size], 'big')
|
|
for i in range(0, size * n, size))
|
|
@@ -552,22 +567,16 @@
|
|
|
|
elif tokenH == 0x40: # data
|
|
s = self._get_size(tokenL)
|
|
- result = self._fp.read(s)
|
|
- if len(result) != s:
|
|
- raise InvalidFileException()
|
|
+ result = self._read(s)
|
|
|
|
elif tokenH == 0x50: # ascii string
|
|
s = self._get_size(tokenL)
|
|
- data = self._fp.read(s)
|
|
- if len(data) != s:
|
|
- raise InvalidFileException()
|
|
+ data = self._read(s)
|
|
result = data.decode('ascii')
|
|
|
|
elif tokenH == 0x60: # unicode string
|
|
s = self._get_size(tokenL) * 2
|
|
- data = self._fp.read(s)
|
|
- if len(data) != s:
|
|
- raise InvalidFileException()
|
|
+ data = self._read(s)
|
|
result = data.decode('utf-16be')
|
|
|
|
elif tokenH == 0x80: # UID
|
|
Index: Python-3.9.25/Lib/test/test_plistlib.py
|
|
===================================================================
|
|
--- Python-3.9.25.orig/Lib/test/test_plistlib.py 2025-12-23 23:47:31.633839488 +0100
|
|
+++ Python-3.9.25/Lib/test/test_plistlib.py 2025-12-23 23:50:05.844028198 +0100
|
|
@@ -837,8 +837,7 @@
|
|
|
|
class TestBinaryPlistlib(unittest.TestCase):
|
|
|
|
- @staticmethod
|
|
- def decode(*objects, offset_size=1, ref_size=1):
|
|
+ def build(self, *objects, offset_size=1, ref_size=1):
|
|
data = [b'bplist00']
|
|
offset = 8
|
|
offsets = []
|
|
@@ -850,7 +849,11 @@
|
|
len(objects), 0, offset)
|
|
data.extend(offsets)
|
|
data.append(tail)
|
|
- return plistlib.loads(b''.join(data), fmt=plistlib.FMT_BINARY)
|
|
+ return b''.join(data)
|
|
+
|
|
+ def decode(self, *objects, offset_size=1, ref_size=1):
|
|
+ data = self.build(*objects, offset_size=offset_size, ref_size=ref_size)
|
|
+ return plistlib.loads(data, fmt=plistlib.FMT_BINARY)
|
|
|
|
def test_nonstandard_refs_size(self):
|
|
# Issue #21538: Refs and offsets are 24-bit integers
|
|
@@ -958,6 +961,34 @@
|
|
with self.assertRaises(plistlib.InvalidFileException):
|
|
plistlib.loads(b'bplist00' + data, fmt=plistlib.FMT_BINARY)
|
|
|
|
+ def test_truncated_large_data(self):
|
|
+ self.addCleanup(support.unlink, support.TESTFN)
|
|
+ def check(data):
|
|
+ with open(support.TESTFN, 'wb') as f:
|
|
+ f.write(data)
|
|
+ # buffered file
|
|
+ with open(support.TESTFN, 'rb') as f:
|
|
+ with self.assertRaises(plistlib.InvalidFileException):
|
|
+ plistlib.load(f, fmt=plistlib.FMT_BINARY)
|
|
+ # unbuffered file
|
|
+ with open(support.TESTFN, 'rb', buffering=0) as f:
|
|
+ with self.assertRaises(plistlib.InvalidFileException):
|
|
+ plistlib.load(f, fmt=plistlib.FMT_BINARY)
|
|
+ for w in range(20, 64):
|
|
+ s = 1 << w
|
|
+ # data
|
|
+ check(self.build(b'\x4f\x13' + s.to_bytes(8, 'big')))
|
|
+ # ascii string
|
|
+ check(self.build(b'\x5f\x13' + s.to_bytes(8, 'big')))
|
|
+ # unicode string
|
|
+ check(self.build(b'\x6f\x13' + s.to_bytes(8, 'big')))
|
|
+ # array
|
|
+ check(self.build(b'\xaf\x13' + s.to_bytes(8, 'big')))
|
|
+ # dict
|
|
+ check(self.build(b'\xdf\x13' + s.to_bytes(8, 'big')))
|
|
+ # number of objects
|
|
+ check(b'bplist00' + struct.pack('>6xBBQQQ', 1, 1, s, 0, 8))
|
|
+
|
|
|
|
class TestKeyedArchive(unittest.TestCase):
|
|
def test_keyed_archive_data(self):
|
|
Index: Python-3.9.25/Misc/NEWS.d/next/Security/2024-05-21-22-11-31.gh-issue-119342.BTFj4Z.rst
|
|
===================================================================
|
|
--- /dev/null 1970-01-01 00:00:00.000000000 +0000
|
|
+++ Python-3.9.25/Misc/NEWS.d/next/Security/2024-05-21-22-11-31.gh-issue-119342.BTFj4Z.rst 2025-12-23 23:49:03.727528792 +0100
|
|
@@ -0,0 +1,5 @@
|
|
+Fix a potential memory denial of service in the :mod:`plistlib` module.
|
|
+When reading a Plist file received from untrusted source, it could cause
|
|
+an arbitrary amount of memory to be allocated.
|
|
+This could have led to symptoms including a :exc:`MemoryError`, swapping, out
|
|
+of memory (OOM) killed processes or containers, or even system crashes.
|