VYPR
Moderate severityOSV Advisory· Published Jan 27, 2026· Updated Jan 27, 2026

pypdf has possible Infinite Loop when processing outlines/bookmarks

CVE-2026-24688

Description

pypdf is a free and open-source pure-python PDF library. An attacker who uses an infinite loop vulnerability that is present in versions prior to 6.6.2 can craft a PDF which leads to an infinite loop. This requires accessing the outlines/bookmarks. This has been fixed in pypdf 6.6.2. If projects cannot upgrade yet, consider applying the changes from PR #3610 manually.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
pypdfPyPI
< 6.6.26.6.2

Affected products

1

Patches

1
b1282f8dcdc1

SEC: Detect cyclic references when retrieving outlines (#3610)

https://github.com/py-pdf/pypdfStefanJan 26, 2026via ghsa
2 files changed · +109 3
  • pypdf/_doc_common.py+19 2 modified
    @@ -833,7 +833,10 @@ def outline(self) -> OutlineType:
             return self._get_outline()
     
         def _get_outline(
    -        self, node: Optional[DictionaryObject] = None, outline: Optional[Any] = None
    +        self,
    +        node: Optional[DictionaryObject] = None,
    +        outline: Optional[Any] = None,
    +        visited: Optional[set[int]] = None,
         ) -> OutlineType:
             if outline is None:
                 outline = []
    @@ -855,15 +858,29 @@ def _get_outline(
                 return outline
     
             # see if there are any more outline items
    +        if visited is None:
    +            visited = set()
             while True:
    +            node_id = id(node)
    +            if node_id in visited:
    +                logger_warning(f"Detected cycle in outline structure for {node}", __name__)
    +                break
    +            visited.add(node_id)
    +
                 outline_obj = self._build_outline_item(node)
                 if outline_obj:
                     outline.append(outline_obj)
     
                 # check for sub-outline
                 if "/First" in node:
                     sub_outline: list[Any] = []
    -                self._get_outline(cast(DictionaryObject, node["/First"]), sub_outline)
    +                # Pass a copy to allow multiple outer entries to reference the same inner one.
    +                inner_visited = visited.copy()
    +                self._get_outline(
    +                    node=cast(DictionaryObject, node["/First"]),
    +                    outline=sub_outline,
    +                    visited=inner_visited,
    +                )
                     if sub_outline:
                         outline.append(sub_outline)
     
    
  • tests/test_doc_common.py+90 1 modified
    @@ -12,7 +12,15 @@
     
     from pypdf import PdfReader, PdfWriter
     from pypdf.errors import PdfReadError
    -from pypdf.generic import EmbeddedFile, NameObject, NullObject, TextStringObject, ViewerPreferences
    +from pypdf.generic import (
    +    ArrayObject,
    +    DictionaryObject,
    +    EmbeddedFile,
    +    NameObject,
    +    NullObject,
    +    TextStringObject,
    +    ViewerPreferences,
    +)
     from tests import get_data_from_url
     
     TESTS_ROOT = Path(__file__).parent.resolve()
    @@ -466,3 +474,84 @@ def test_flatten__cyclic_references():
     
         with pytest.raises(expected_exception=PdfReadError, match=r"^Detected cyclic page references\.$"):
             reader._flatten()
    +
    +
    +@pytest.mark.enable_socket
    +@pytest.mark.timeout(10)
    +def test_get_outline__cyclic_references(caplog):
    +    url = "https://github.com/user-attachments/files/24859044/circular_outline.pdf"
    +    name = "circular_outline.pdf"
    +    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))
    +
    +    assert reader.outline == [
    +        {
    +            "/%is_open%": True,
    +            "/Page": reader.pages[0].indirect_reference,
    +            "/Title": "Bookmark A",
    +            "/Type": "/Fit"
    +        },
    +        {
    +            "/%is_open%": True,
    +            "/Page": reader.pages[0].indirect_reference,
    +            "/Title": "Bookmark B",
    +            "/Type": "/Fit"
    +        }
    +    ]
    +    assert caplog.messages[0].startswith("Detected cycle in outline structure for {")
    +
    +
    +@pytest.mark.enable_socket
    +@pytest.mark.timeout(10)
    +def test_get_outline__cyclic_references__nested_handling(caplog):
    +    url = "https://github.com/user-attachments/files/24859044/circular_outline.pdf"
    +    name = "circular_outline.pdf"
    +    writer = PdfWriter(clone_from=BytesIO(get_data_from_url(url=url, name=name)))
    +
    +    nested_outline = DictionaryObject()
    +    writer._add_object(nested_outline)
    +    nested_outline.update({
    +        NameObject("/Title"): TextStringObject("Nested entry"),
    +        NameObject("/Parent"): writer.get_object(5),
    +        NameObject("/Dest"): ArrayObject([writer.pages[0].indirect_reference, NameObject("/Fit")]),
    +        NameObject("/Next"): writer.get_object(6),
    +    })
    +    writer.get_object(5)[NameObject("/First")] = nested_outline.indirect_reference
    +    writer.get_object(6)[NameObject("/First")] = nested_outline.indirect_reference
    +
    +    assert writer.outline == [
    +        {
    +            "/%is_open%": True,
    +            "/Page": writer.pages[0].indirect_reference,
    +            "/Title": "Bookmark A",
    +            "/Type": "/Fit"
    +        },
    +        [
    +            {
    +                "/%is_open%": True,
    +                "/Page": writer.pages[0].indirect_reference,
    +                "/Title": "Nested entry",
    +                "/Type": "/Fit"
    +            },
    +            {
    +                "/%is_open%": True,
    +                "/Page": writer.pages[0].indirect_reference,
    +                "/Title": "Bookmark B",
    +                "/Type": "/Fit"
    +            }
    +        ],
    +        {
    +            "/%is_open%": True,
    +            "/Page": writer.pages[0].indirect_reference,
    +            "/Title": "Bookmark B",
    +            "/Type": "/Fit"
    +        },
    +        [
    +            {
    +                "/%is_open%": True,
    +                "/Page": writer.pages[0].indirect_reference,
    +                "/Title": "Nested entry",
    +                "/Type": "/Fit"
    +            }
    +        ]
    +    ]
    +    assert caplog.messages[0].startswith("Detected cycle in outline structure for {")
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.