CVE-2023-46250 (published 2023-10-31T16:15Z)

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

CVSS v3 base score: 5.5, MEDIUM (CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H)

Weakness information:
cwe_id: CWE-835
cwe_name: Loop with Unreachable Exit Condition ('Infinite Loop')
description: The product contains an iteration or loop with an exit condition that cannot be reached, i.e., an infinite loop.
extended_description: If the loop can be influenced by an attacker, this weakness could allow attackers to consume excessive resources such as CPU or memory.
         

Fix information:
repo_url: https://github.com/py-pdf/pypdf
commit_details: repo_url: https://github.com/py-pdf/pypdf
author: Alexander Huszagh
author_date: 2023-10-28 23:14:40-05:00
msg: 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.
repository: repo_url: https://github.com/py-pdf/pypdf
repo_name: py-pdf/pypdf
description: A pure-python PDF library capable of splitting, merging, cropping, and transforming the pages of PDF files
repo_language: Python
owner: py-pdf
file_changes: filename: _data_structures.py
change_type: ModificationType.MODIFY
diff: @@ -40,6 +40,7 @@ from typing import (
     List,
     Optional,
     Sequence,
+    Set,
     Tuple,
     Union,
     cast,
@@ -187,6 +188,7 @@ class DictionaryObject(dict, PdfObject):
         except Exception:
             pass
 
+        visited: Set[Tuple[int, int]] = set()
         d__ = cast(
             "DictionaryObject",
             self._reference_clone(self.__class__(), pdf_dest, force_duplicate),
@@ -194,7 +196,7 @@ class DictionaryObject(dict, PdfObject):
         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(
@@ -203,6 +205,7 @@ class DictionaryObject(dict, PdfObject):
         pdf_dest: PdfWriterProtocol,
         force_duplicate: bool,
         ignore_fields: Optional[Sequence[Union[str, int]]],
+        visited: Set[Tuple[int, int]],
     ) -> None:
         """
         Update the object from src.
@@ -270,6 +273,14 @@ class DictionaryObject(dict, PdfObject):
                                     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
@@ -282,7 +293,7 @@ class DictionaryObject(dict, PdfObject):
                             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:
@@ -798,6 +809,7 @@ class StreamObject(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.
@@ -820,7 +832,7 @@ class StreamObject(DictionaryObject):
                 )
         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
@@ -1048,6 +1060,7 @@ class ContentStream(DecodedStreamObject):
         except Exception:
             pass
 
+        visited: Set[Tuple[int, int]] = set()
         d__ = cast(
             "ContentStream",
             self._reference_clone(
@@ -1056,7 +1069,7 @@ class ContentStream(DecodedStreamObject):
         )
         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(
@@ -1065,6 +1078,7 @@ class ContentStream(DecodedStreamObject):
         pdf_dest: PdfWriterProtocol,
         force_duplicate: bool,
         ignore_fields: Optional[Sequence[Union[str, int]]],
+        visited: Set[Tuple[int, int]],
     ) -> None:
         """
         Update the object from src.
@@ -1081,7 +1095,7 @@ class ContentStream(DecodedStreamObject):
         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

programming_language: Python