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.
| Package | Affected versions | Patched versions |
|---|---|---|
pypdfPyPI | < 6.6.2 | 6.6.2 |
Affected products
1Patches
1b1282f8dcdc1SEC: Detect cyclic references when retrieving outlines (#3610)
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- github.com/advisories/GHSA-2q4j-m29v-hq73ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-24688ghsaADVISORY
- github.com/py-pdf/pypdf/commit/b1282f8dcdc1a7b41ceab6740ffddfdf31b1fec1ghsax_refsource_MISCWEB
- github.com/py-pdf/pypdf/pull/3610ghsax_refsource_MISCWEB
- github.com/py-pdf/pypdf/releases/tag/6.6.2ghsax_refsource_MISCWEB
- github.com/py-pdf/pypdf/security/advisories/GHSA-2q4j-m29v-hq73ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.