Summary
Deeply-nested vCard/iCal input (nested BEGIN:/END: blocks) causes an uncaught
RecursionError. vobject documents ParseError / VObjectError (vobject.base) as the
errors raised for bad input, so callers that guard those documented exceptions
around parsing will instead see an unhandled RecursionError and crash. Because
vObject/iCalendar data commonly comes from untrusted sources (calendar invites,
contact/ICS imports, webcal feeds), a small crafted payload can DoS the caller.
Details
Parsing itself is safe -- readComponents() is iterative (uses an explicit Stack).
However, two recursive tree-walks over the component tree that the parser builds
are unbounded:
-
DEFAULT parse path: vobject.readOne(src) / readComponents() call
Component.transformChildrenToNative() (transform=True is the default), which
recurses via child.transformChildrenToNative(). This overflows at roughly
995 levels of nesting (~16 KB of input) at Python's default recursion limit.
(With transform=False the parse stays iterative and does not crash --
confirming the recursion is in the transform step.)
-
serialize(): defaultSerialize() recurses via child.serialize(), overflowing
at ~498 levels (~8 KB).
Both raise a bare RecursionError, which is not a VObjectError/ParseError.
Proof of concept
import sys, vobject
from vobject.base import VObjectError, ParseError
def nested(depth):
return ("".join("BEGIN:X\r\n" for _ in range(depth))
+ "FN:test\r\n"
+ "".join("END:X\r\n" for _ in range(depth)))
# Vector 1: default readOne (transformToNative) -- crashes ~depth 1200
try:
[vobject.readOne](http://vobject.readone/)(nested(1200))
except (VObjectError, ParseError):
print("documented error - ok")
# -> RecursionError (uncaught)
# Vector 2: serialize -- crashes ~depth 700
obj = vobject.readOne(nested(700)) # parses (transform crash is higher)
obj.serialize() # -> RecursionError (uncaught)
I could not find any prior recursion/RecursionError/DoS report on
py-vobject/vobject, so this looks novel.
Suggested fix (attached patch: vobject_max_depth.patch)
The parser's BEGIN handler in readComponents() is the single natural chokepoint:
the Stack depth there is exactly the component-nesting depth. Enforcing a maximum
nesting depth at that point bounds the tree depth and therefore protects all
downstream recursive operations (transformChildrenToNative, serialize, validate),
while raising the documented ParseError:
MAX_NESTING_DEPTH = 100 # far above any legitimate vCard/iCal nesting
elif vline.name == "BEGIN":
if len(stack) >= MAX_NESTING_DEPTH:
raise ParseError(
"Component nesting depth exceeds the maximum of "
"{0}".format(MAX_NESTING_DEPTH), n)
stack.push(Component(vline.value, group=vline.group))
I verified: deep inputs (700/1200/5000) now raise ParseError for both readOne and
serialize; legitimate VCALENDAR/VEVENT documents still parse and serialize
correctly. The patch applies cleanly to the 0.9.9 sdist. If a higher limit is
preferred, MAX_NESTING_DEPTH is a single constant.
Summary
Deeply-nested vCard/iCal input (nested BEGIN:/END: blocks) causes an uncaught
RecursionError. vobject documents ParseError / VObjectError (vobject.base) as the
errors raised for bad input, so callers that guard those documented exceptions
around parsing will instead see an unhandled RecursionError and crash. Because
vObject/iCalendar data commonly comes from untrusted sources (calendar invites,
contact/ICS imports, webcal feeds), a small crafted payload can DoS the caller.
Details
Parsing itself is safe -- readComponents() is iterative (uses an explicit Stack).
However, two recursive tree-walks over the component tree that the parser builds
are unbounded:
DEFAULT parse path: vobject.readOne(src) / readComponents() call
Component.transformChildrenToNative() (transform=True is the default), which
recurses via child.transformChildrenToNative(). This overflows at roughly
995 levels of nesting (~16 KB of input) at Python's default recursion limit.
(With transform=False the parse stays iterative and does not crash --
confirming the recursion is in the transform step.)
serialize(): defaultSerialize() recurses via child.serialize(), overflowing
at ~498 levels (~8 KB).
Both raise a bare RecursionError, which is not a VObjectError/ParseError.
Proof of concept
I could not find any prior recursion/RecursionError/DoS report on
py-vobject/vobject, so this looks novel.
Suggested fix (attached patch: vobject_max_depth.patch)
The parser's BEGIN handler in readComponents() is the single natural chokepoint:
the Stack depth there is exactly the component-nesting depth. Enforcing a maximum
nesting depth at that point bounds the tree depth and therefore protects all
downstream recursive operations (transformChildrenToNative, serialize, validate),
while raising the documented ParseError:
I verified: deep inputs (700/1200/5000) now raise ParseError for both readOne and
serialize; legitimate VCALENDAR/VEVENT documents still parse and serialize
correctly. The patch applies cleanly to the 0.9.9 sdist. If a higher limit is
preferred, MAX_NESTING_DEPTH is a single constant.