From 2b7f6d78ad91bd83fb0977e6b535350e5500dc8b Mon Sep 17 00:00:00 2001 From: Hartmut Goebel Date: Mon, 29 Nov 2021 23:16:32 +0100 Subject: [PATCH 1/3] Improve log message for unknown timezone Closes #174. --- vobject/icalendar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vobject/icalendar.py b/vobject/icalendar.py index 325bae7..5640a77 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 From 265020d097f8c35630bdf488a42d3242ed0998b1 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Wed, 10 Sep 2025 01:35:25 +1000 Subject: [PATCH 2/3] Add support for zoneinfo tzinfo sub-classes. zoneinfo was added to the stdlib in 3.9, and is perhaps the most likely source of tzinfo objects now. Use its 'key' attribute to get a unique timezone identifier, rather than falling back to the tzname, which isn't unique (eg. IST = Ireland, Israel, and India). Fixes #117. --- vobject/icalendar.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/vobject/icalendar.py b/vobject/icalendar.py index 5640a77..52ff94b 100644 --- a/vobject/icalendar.py +++ b/vobject/icalendar.py @@ -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) From fa2332c7d791666f1e9a8a0669d2ca936d1ee0cf Mon Sep 17 00:00:00 2001 From: David Arnold Date: Mon, 3 Aug 2026 14:51:44 +1000 Subject: [PATCH 3/3] Limit allowed nesting of BEGIN/END blocks to avoid DoS. --- tests.py | 28 ++++++++++++++++++++++++++++ vobject/base.py | 15 ++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) 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():