diff --git a/tests.py b/tests.py index 8b6deb4..0c1a6e6 100644 --- a/tests.py +++ b/tests.py @@ -864,6 +864,34 @@ def test_issue50(self): cal = base.readOne(test_file) self.assertEqual(datetime.datetime(2024, 8, 12, 22, 30, tzinfo=tzutc()), cal.vevent.dtend.value) + def test_nested_begin_depth(self): + """ + Catch deeply nested BEGIN and raise ParseError rather than + exceeding interpreter recursion limit. + + Reported by: oc-8a8a9d + """ + depth = 200 + deep_ical = ("".join("BEGIN:X\r\n" for _ in range(depth)) + + "FN:test\r\n" + + "".join("END:X\r\n" for _ in range(depth))) + obj = None + + default_recursion_limit = sys.getrecursionlimit() + sys.setrecursionlimit(150) + try: + # Vector 1: default readOne (transform=True) + with self.assertRaises(ParseError): + obj = base.readOne(deep_ical) + + # Vector 2: serialize() + if obj: + with self.assertRaises(ParseError): + obj.serialize() + + finally: + sys.setrecursionlimit(default_recursion_limit) + class TestChangeTZ(unittest.TestCase): """ diff --git a/vobject/base.py b/vobject/base.py index 6765608..77010ac 100644 --- a/vobject/base.py +++ b/vobject/base.py @@ -87,6 +87,15 @@ def to_basestring(s): TAB = '\t' SPACEORTAB = SPACE + TAB +# Maximum allowed nesting depth of components (nested BEGIN:/END: +# blocks). Deeply-nested input would otherwise build a component tree +# that overflows the interpreter stack during the recursive +# transformChildrenToNative()/serialize() walks, raising an uncaught +# RecursionError (a denial of service). Instead we raise the +# documented ParseError once this depth is exceeded. The limit is far +# above any legitimate vCard/iCal nesting. +DEFAULT_MAX_NESTING = 100 + # --------------------------------- Main classes ------------------------------- @@ -1077,7 +1086,8 @@ def pop(self): def readComponents(streamOrString, validate=False, transform=True, - ignoreUnreadable=False, allowQP=False): + ignoreUnreadable=False, allowQP=False, + max_nesting=DEFAULT_MAX_NESTING): """ Generate one Component at a time from a stream. """ @@ -1107,6 +1117,9 @@ def readComponents(streamOrString, validate=False, transform=True, versionLine = vline stack.modifyTop(vline) elif vline.name == "BEGIN": + if len(stack) >= max_nesting: + raise ParseError("Component nesting depth exceeds requested " + "maximum of {0} levels".format(max_nesting), n) stack.push(Component(vline.value, group=vline.group)) elif vline.name == "PROFILE": if not stack.top(): diff --git a/vobject/icalendar.py b/vobject/icalendar.py index 325bae7..52ff94b 100644 --- a/vobject/icalendar.py +++ b/vobject/icalendar.py @@ -80,7 +80,7 @@ def getTzid(tzid, smart=True): tz = timezone(tzid) registerTzid(toUnicode(tzid), tz) except UnknownTimeZoneError as e: - logging.error(e) + logging.error("Unknown Timezone: %r", e.args[0]) except ImportError as e: logging.error(e) return tz @@ -335,6 +335,10 @@ def pickTzid(tzinfo, allowUTC=False): if tzinfo is None or (not allowUTC and tzinfo_eq(tzinfo, utc)): return None + # Try a zoneinfo (CPython 3.9+) first. + if hasattr(tzinfo, 'key'): + return toUnicode(tzinfo.key) + # Try pytz tzid key if hasattr(tzinfo, 'tzid'): return toUnicode(tzinfo.tzid)