VYPR
Medium severity6.5NVD Advisory· Published Apr 22, 2026· Updated Apr 27, 2026

CVE-2026-41313

CVE-2026-41313

Description

pypdf is a free and open-source pure-python PDF library. An attacker who uses a vulnerability present in versions prior to 6.10.2 can craft a PDF which leads to long runtimes. This requires loading a PDF with a large trailer /Size value in incremental mode. This has been fixed in pypdf 6.10.2. As a workaround, one may apply the changes from the patch manually.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
pypdfPyPI
< 6.10.26.10.2

Affected products

1

Patches

1
c50a0104cf08

SEC: Do not rely on possibly invalid /Size for incremental cloning (#3735)

https://github.com/py-pdf/pypdfStefanApr 15, 2026via ghsa
3 files changed · +137 9
  • docs/user/security.md+7 0 modified
    @@ -43,6 +43,13 @@ If you want to employ custom limits for the *PdfWriter* as well, the currently p
     is to initialize it from the reader, id est something like
     `PdfWriter(clone_from=PdfReader("file.pdf", root_object_recovery_limit=42))`.
     
    +For *PdfWriter* instances, the following limits are employed for incremental reading:
    +
    +* `incremental_clone_object_count_limit` limits the number of objects to read during cloning. It defaults to
    +  500 000. Setting it to `None` will fully disable this limit.
    +* `incremental_clone_object_id_limit` limits the maximum object ID to read during cloning. It defaults to
    +  1 000 000. Setting it to `None` will fully disable this limit.
    +
     ## Reporting possible vulnerabilities
     
     Please refer to our [security policy](https://github.com/py-pdf/pypdf/security/policy).
    
  • pypdf/_writer.py+42 6 modified
    @@ -84,7 +84,7 @@
     from .constants import FieldDictionaryAttributes as FA
     from .constants import PageAttributes as PG
     from .constants import TrailerKeys as TK
    -from .errors import PdfReadError, PyPdfError
    +from .errors import LimitReachedError, PdfReadError, PyPdfError
     from .generic import (
         PAGE_FIT,
         ArrayObject,
    @@ -178,6 +178,9 @@ def __init__(
             incremental: bool = False,
             full: bool = False,
             strict: bool = False,
    +        *,
    +        incremental_clone_object_count_limit: Optional[int] = 500_000,
    +        incremental_clone_object_id_limit: Optional[int] = 1_000_000,
         ) -> None:
             self.strict = strict
             """
    @@ -227,6 +230,16 @@ def __init__(
             self._merged_in_pages: dict[Optional[IndirectObject], Optional[IndirectObject]] = {}
             "Tracks pages added to the writer and what page they turned into."
     
    +        # Security parameters.
    +        self._incremental_clone_object_count_limit = (
    +            incremental_clone_object_count_limit
    +            if isinstance(incremental_clone_object_count_limit, int)
    +            else sys.maxsize
    +        )
    +        self._incremental_clone_object_id_limit = (
    +            incremental_clone_object_id_limit if isinstance(incremental_clone_object_id_limit, int) else sys.maxsize
    +        )
    +
             if self.incremental:
                 if isinstance(fileobj, (str, Path)):
                     with open(fileobj, "rb") as f:
    @@ -1129,6 +1142,28 @@ def reattach_fields(
                     lst.append(annotation)
             return lst
     
    +    def _collect_incremental_clone_object_ids(self, reader: PdfReader) -> list[int]:
    +        object_ids: set[int] = set()
    +        for xref_entry in reader.xref.values():
    +            object_ids.update(filter(None, xref_entry))
    +        object_ids.update(filter(None, reader.xref_objStm))
    +
    +        object_count = len(object_ids)
    +        if object_count > self._incremental_clone_object_count_limit:
    +            raise LimitReachedError(
    +                f"Incremental clone object count {object_count} exceeds "
    +                f"maximum allowed count {self._incremental_clone_object_count_limit}."
    +            )
    +
    +        max_object_id = max(object_ids, default=0)
    +        if max_object_id > self._incremental_clone_object_id_limit:
    +            raise LimitReachedError(
    +                f"Incremental clone object ID {max_object_id} exceeds "
    +                f"maximum allowed ID {self._incremental_clone_object_id_limit}."
    +            )
    +
    +        return sorted(object_ids)
    +
         def clone_reader_document_root(self, reader: PdfReader) -> None:
             """
             Copy the reader document root to the writer and all sub-elements,
    @@ -1141,11 +1176,12 @@ def clone_reader_document_root(self, reader: PdfReader) -> None:
             """
             self._info_obj = None
             if self.incremental:
    -            self._objects = [None] * (cast(int, reader.trailer["/Size"]) - 1)
    -            for i in range(len(self._objects)):
    -                o = reader.get_object(i + 1)
    -                if o is not None:
    -                    self._objects[i] = o.replicate(self)
    +            object_ids = self._collect_incremental_clone_object_ids(reader)
    +            self._objects = [None] * (object_ids[-1] if object_ids else 0)
    +            for object_id in object_ids:
    +                reader_object = reader.get_object(object_id)
    +                if reader_object is not None:
    +                    self._objects[object_id - 1] = reader_object.replicate(self)
             else:
                 self._objects.clear()
             self._root_object = reader.root_object.clone(self)
    
  • tests/test_writer.py+88 3 modified
    @@ -20,7 +20,7 @@
         Transformation,
     )
     from pypdf.annotations import Link
    -from pypdf.errors import DeprecationError, PageSizeNotDefinedError, PdfReadError, PyPdfError
    +from pypdf.errors import DeprecationError, LimitReachedError, PageSizeNotDefinedError, PdfReadError, PyPdfError
     from pypdf.generic import (
         ArrayObject,
         ByteStringObject,
    @@ -3016,8 +3016,7 @@ def test_wrong_size_in_incremental_pdf(caplog):
         with pytest.raises(expected_exception=PdfReadError, match=r"^Object count 19 exceeds defined trailer size 2$"):
             writer.clone_reader_document_root(reader=PdfReader(BytesIO(modified_data)))
     
    -    with pytest.raises(expected_exception=PdfReadError, match=r"^Got index error while flattening\.$"):
    -        PdfWriter(BytesIO(modified_data), incremental=True)
    +    PdfWriter(BytesIO(modified_data), incremental=True)
     
     
     @pytest.mark.enable_socket
    @@ -3088,3 +3087,89 @@ def test_flatten_form_field_with_signature():
         writer.write(b)
     
         _ = PdfReader(b)
    +
    +
    +@pytest.mark.timeout(10)
    +def test_clone_reader_document_root__incremental__large_size():
    +    parts: list[bytes] = [b"%PDF-1.4\n"]
    +    offsets: dict[int, int] = {}
    +
    +    for object_number, body in (
    +            (1, b"<< /Type /Catalog /Pages 2 0 R >>"),
    +            (2, b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>"),
    +            (3, b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 1 1] >>"),
    +    ):
    +        offsets[object_number] = sum(len(p) for p in parts)
    +        parts.append(f"{object_number} 0 obj\n".encode())
    +        parts.append(body + b"\n")
    +        parts.append(b"endobj\n")
    +
    +    xref_offset = sum(len(p) for p in parts)
    +    parts.append(b"xref\n")
    +    parts.append(b"0 4\n")
    +    parts.append(b"0000000000 65535 f \n")
    +    parts.append(f"{offsets[1]:010d} 00000 n \n".encode())
    +    parts.append(f"{offsets[2]:010d} 00000 n \n".encode())
    +    parts.append(f"{offsets[3]:010d} 00000 n \n".encode())
    +    parts.append(b"trailer\n<< /Root 1 0 R /Size 5000000 >>\n")
    +    parts.append(f"startxref\n{xref_offset}\n%%EOF\n".encode())
    +    data = b"".join(parts)
    +
    +    writer = PdfWriter(BytesIO(data), incremental=True)
    +    assert writer._objects == [
    +        DictionaryObject({
    +            NameObject("/Pages"): IndirectObject(2, 0, writer),
    +            NameObject("/Type"): NameObject("/Catalog")
    +        }),
    +        DictionaryObject({
    +            NameObject("/Count"): NumberObject(1),
    +            NameObject("/Kids"): ArrayObject([
    +                IndirectObject(3, 0, writer)
    +            ]),
    +            NameObject("/Type"): NameObject("/Pages")
    +        }),
    +        DictionaryObject({
    +            NameObject("/MediaBox"): ArrayObject([
    +                NumberObject(0), NumberObject(0), NumberObject(1), NumberObject(1)
    +            ]),
    +            NameObject("/Parent"): IndirectObject(2, 0, writer),
    +            NameObject("/Type"): NameObject("/Page")
    +        })
    +    ]
    +
    +
    +def test_collect_incremental_clone_object_ids():
    +    reader = PdfReader(RESOURCE_ROOT / "crazyones.pdf")
    +
    +    # No limit.
    +    writer = PdfWriter()
    +    assert writer._collect_incremental_clone_object_ids(reader) == list(range(1, 23))
    +
    +    # Size limit.
    +    writer = PdfWriter(incremental_clone_object_count_limit=13)
    +    with pytest.raises(
    +            expected_exception=LimitReachedError,
    +            match=r"^Incremental clone object count 22 exceeds maximum allowed count 13\.$"
    +    ):
    +        writer._collect_incremental_clone_object_ids(reader)
    +
    +    # Number limit.
    +    writer = PdfWriter(incremental_clone_object_id_limit=17)
    +    with pytest.raises(
    +            expected_exception=LimitReachedError,
    +            match=r"^Incremental clone object ID 22 exceeds maximum allowed ID 17\.$"
    +    ):
    +        writer._collect_incremental_clone_object_ids(reader)
    +
    +
    +def test_clone_reader_document_root__incremental__unknown_object():
    +    writer = PdfWriter()
    +    writer.add_blank_page(width=72, height=72)
    +    data = BytesIO()
    +    writer.write(data)
    +    data.flush()
    +
    +    writer = PdfWriter(data, incremental=True)
    +    reader = PdfReader(RESOURCE_ROOT / "crazyones.pdf")
    +    with mock.patch.object(writer, "_collect_incremental_clone_object_ids", return_value=[*list(range(1, 23)), 42]):
    +        writer.clone_reader_document_root(reader)
    

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.