CVE-2023-46250: Loop with Unreachable Exit Condition ('Infinite Loop') (CWE-835)
value: pypdf is a free and open-source pure-python PDF library. An attacker who uses a vulnerability present in versions 3.7.0 through 3.16.4 can craft a PDF which leads to an infinite loop. This infinite loop blocks the current process and can utilize a single core of the CPU by 100%. It does not affect memory usage. That is, for example, the case when the pypdf-user manipulates an incoming malicious PDF e.g. by merging it with another PDF or by adding annotations. The issue was fixed in version 3.17.0. As a workaround, apply the patch manually by modifying `pypdf/generic/_data_structures.py`.
CWE description: The product contains an iteration or loop with an exit condition that cannot be reached, i.e., an infinite loop.
CVSS v3 base score: 5.5
Published: 2023-10-31T16:15Z
Repository: https://github.com/py-pdf/pypdf
Language: Python
Files changed: pypdf/generic/_data_structures.py
Commit date: 2023-10-29 05:14:40 +0100
Commit message: SEC: Infinite recursion when using PdfWriter(clone_from=reader) (#2264)

Use a visited memo to check if the current object in the clone operation has already been visited, and if so, do not add it to the list of objects.

This avoids infinite recursion in case there are links to the same object inside a PDF.
Diff: diff --git a/pypdf/generic/_data_structures.py b/pypdf/generic/_data_structures.py
index 94f1f24..ff1dfb6 100644
--- a/pypdf/generic/_data_structures.py
+++ b/pypdf/generic/_data_structures.py
@@ -38,10 +38,11 @@ from typing import (
     Dict,
     Iterable,
     List,
     Optional,
     Sequence,
+    Set,
     Tuple,
     Union,
     cast,
 )
 
@@ -185,26 +186,28 @@ class DictionaryObject(dict, PdfObject):
             if self.indirect_reference.pdf == pdf_dest and not force_duplicate:  # type: ignore
                 return self
         except Exception:
             pass
 
+        visited: Set[Tuple[int, int]] = set()
         d__ = cast(
             "DictionaryObject",
             self._reference_clone(self.__class__(), pdf_dest, force_duplicate),
         )
         if ignore_fields is None:
             ignore_fields = []
         if len(d__.keys()) == 0:
-            d__._clone(self, pdf_dest, force_duplicate, ignore_fields)
+            d__._clone(self, pdf_dest, force_duplicate, ignore_fields, visited)
         return d__
 
     def _clone(
         self,
         src: "DictionaryObject",
         pdf_dest: PdfWriterProtocol,
         force_duplicate: bool,
         ignore_fields: Optional[Sequence[Union[str, int]]],
+        visited: Set[Tuple[int, int]],
     ) -> None:
         """
         Update the object from src.
 
         Args:
@@ -268,10 +271,18 @@ class DictionaryObject(dict, PdfObject):
                                 "DictionaryObject",
                                 cur_obj._reference_clone(
                                     cur_obj.__class__(), pdf_dest, force_duplicate
                                 ),
                             )
+                            # check to see if we've previously processed our item
+                            if clon.indirect_reference is not None:
+                                idnum = clon.indirect_reference.idnum
+                                generation = clon.indirect_reference.generation
+                                if (idnum, generation) in visited:
+                                    cur_obj = None
+                                    break
+                                visited.add((idnum, generation))
                             objs.append((cur_obj, clon))
                             assert prev_obj is not None
                             prev_obj[NameObject(k)] = clon.indirect_reference
                             prev_obj = clon
                             try:
@@ -280,11 +291,11 @@ class DictionaryObject(dict, PdfObject):
                                 else:
                                     cur_obj = cast("DictionaryObject", cur_obj[k])
                             except Exception:
                                 cur_obj = None
                         for s, c in objs:
-                            c._clone(s, pdf_dest, force_duplicate, ignore_fields)
+                            c._clone(s, pdf_dest, force_duplicate, ignore_fields, visited)
 
         for k, v in src.items():
             if k not in ignore_fields:
                 if isinstance(v, StreamObject):
                     if not hasattr(v, "indirect_reference"):
@@ -796,10 +807,11 @@ class StreamObject(DictionaryObject):
         self,
         src: DictionaryObject,
         pdf_dest: PdfWriterProtocol,
         force_duplicate: bool,
         ignore_fields: Optional[Sequence[Union[str, int]]],
+        visited: Set[Tuple[int, int]],
     ) -> None:
         """
         Update the object from src.
 
         Args:
@@ -818,11 +830,11 @@ class StreamObject(DictionaryObject):
                     "DecodedStreamObject",
                     decoded_self.clone(pdf_dest, force_duplicate, ignore_fields),
                 )
         except Exception:
             pass
-        super()._clone(src, pdf_dest, force_duplicate, ignore_fields)
+        super()._clone(src, pdf_dest, force_duplicate, ignore_fields, visited)
 
     def get_data(self) -> Union[bytes, str]:
         return self._data
 
     def set_data(self, data: bytes) -> None:
@@ -1046,27 +1058,29 @@ class ContentStream(DecodedStreamObject):
             if self.indirect_reference.pdf == pdf_dest and not force_duplicate:  # type: ignore
                 return self
         except Exception:
             pass
 
+        visited: Set[Tuple[int, int]] = set()
         d__ = cast(
             "ContentStream",
             self._reference_clone(
                 self.__class__(None, None), pdf_dest, force_duplicate
             ),
         )
         if ignore_fields is None:
             ignore_fields = []
-        d__._clone(self, pdf_dest, force_duplicate, ignore_fields)
+        d__._clone(self, pdf_dest, force_duplicate, ignore_fields, visited)
         return d__
 
     def _clone(
         self,
         src: DictionaryObject,
         pdf_dest: PdfWriterProtocol,
         force_duplicate: bool,
         ignore_fields: Optional[Sequence[Union[str, int]]],
+        visited: Set[Tuple[int, int]],
     ) -> None:
         """
         Update the object from src.
 
         Args:
@@ -1079,11 +1093,11 @@ class ContentStream(DecodedStreamObject):
         super().set_data(b_(src_cs._data))
         self.pdf = pdf_dest
         self._operations = list(src_cs._operations)
         self.forced_encoding = src_cs.forced_encoding
         # no need to call DictionaryObjection or anything
-        # like super(DictionaryObject,self)._clone(src, pdf_dest, force_duplicate, ignore_fields)
+        # like super(DictionaryObject,self)._clone(src, pdf_dest, force_duplicate, ignore_fields, visited)
 
     def _parse_content_stream(self, stream: StreamType) -> None:
         # 7.8.2 Content Streams
         stream.seek(0, 0)
         operands: List[Union[int, str, PdfObject]] = []

Vulnerable code:             d__._clone(self, pdf_dest, force_duplicate, ignore_fields)
                            c._clone(s, pdf_dest, force_duplicate, ignore_fields)
        super()._clone(src, pdf_dest, force_duplicate, ignore_fields)
        d__._clone(self, pdf_dest, force_duplicate, ignore_fields)
        # like super(DictionaryObject,self)._clone(src, pdf_dest, force_duplicate, ignore_fields)
Fixed code:     Set,
        visited: Set[Tuple[int, int]] = set()
            d__._clone(self, pdf_dest, force_duplicate, ignore_fields, visited)
        visited: Set[Tuple[int, int]],
                            # check to see if we've previously processed our item
                            if clon.indirect_reference is not None:
                                idnum = clon.indirect_reference.idnum
                                generation = clon.indirect_reference.generation
                                if (idnum, generation) in visited:
                                    cur_obj = None
                                    break
                                visited.add((idnum, generation))
                            c._clone(s, pdf_dest, force_duplicate, ignore_fields, visited)
        visited: Set[Tuple[int, int]],
        super()._clone(src, pdf_dest, force_duplicate, ignore_fields, visited)
        visited: Set[Tuple[int, int]] = set()
        d__._clone(self, pdf_dest, force_duplicate, ignore_fields, visited)
        visited: Set[Tuple[int, int]],
        # like super(DictionaryObject,self)._clone(src, pdf_dest, force_duplicate, ignore_fields, visited)