diff --git a/pypdf/generic/_data_structures.py b/pypdf/generic/_data_structures.py index a3e66c01fe..98b9fc3540 100644 --- a/pypdf/generic/_data_structures.py +++ b/pypdf/generic/_data_structures.py @@ -300,7 +300,18 @@ def read_from_stream( stream.seek(-len(rest), 1) stream.seek(-1, 1) # read and append object - arr.append(read_object(stream, pdf, forced_encoding, parsing_budget=parsing_budget)) + try: + obj = read_object(stream, pdf, forced_encoding, parsing_budget=parsing_budget) + except LimitReachedError: + # Always propagate security issues. + raise + except (MemoryError, RecursionError) as exc: + # A deeply nested array (e.g. "[[[[...]]]]") blows the interpreter's + # recursion limit before the element-count budget above ever triggers, + # since nesting depth, not element count, drives the recursion here. + # Mirrors the guard DictionaryObject.read_from_stream already applies. + raise PdfReadError("Error reading PDF file.") from exc + arr.append(obj) return arr diff --git a/tests/generic/test_data_structures.py b/tests/generic/test_data_structures.py index f84fec3d68..d929364fda 100644 --- a/tests/generic/test_data_structures.py +++ b/tests/generic/test_data_structures.py @@ -807,6 +807,17 @@ def test_array_object__read_from_stream__maximum_element_count() -> None: ArrayObject.read_from_stream(BytesIO(data), pdf=pdf) # type: ignore[arg-type] +def test_array_object__read_from_stream__deeply_nested_array_raises_pdfreaderror() -> None: + # A single array nested thousands of levels deep drives recursion depth, + # not element count, so array_maximum_element_count never engages. + # Previously this raised an uncaught RecursionError instead of PdfReadError. + pdf = ReaderDummy() + depth = 5000 + data = b"[" * depth + b"]" * depth + with pytest.raises(PdfReadError, match=r"^Error reading PDF file\.$"): + ArrayObject.read_from_stream(BytesIO(data), pdf=pdf) # type: ignore[arg-type] + + def test_dictionary_object__read_from_stream__token_limit() -> None: pdf = ReaderDummy() data = b"<< /Key1 << /Key2 << /Key3 << /Key4 << /SomeName 42 >> >> >> >> /Key5 13 >>"