Skip to content

DoS from nested BEGIN/END blocks #134

Description

@da4089

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:

  1. 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.)

  2. 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.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions