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.
| Package | Affected versions | Patched versions |
|---|---|---|
pypdfPyPI | < 6.10.2 | 6.10.2 |
Affected products
1Patches
1c50a0104cf08SEC: Do not rely on possibly invalid /Size for incremental cloning (#3735)
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- github.com/py-pdf/pypdf/commit/c50a0104cf083356f7c7f5d61410466a57f5c88anvdPatchWEB
- github.com/py-pdf/pypdf/pull/3735nvdIssue TrackingPatchWEB
- github.com/py-pdf/pypdf/security/advisories/GHSA-4pxv-j86v-mhcwnvdPatchVendor AdvisoryWEB
- github.com/advisories/GHSA-4pxv-j86v-mhcwghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-41313ghsaADVISORY
- github.com/py-pdf/pypdf/releases/tag/6.10.2nvdRelease NotesWEB
News mentions
0No linked articles in our index yet.