Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-PyPDF2 for openSUSE:Factory checked in at 2026-02-23 16:13:22 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-PyPDF2 (Old) and /work/SRC/openSUSE:Factory/.python-PyPDF2.new.1977 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-PyPDF2" Mon Feb 23 16:13:22 2026 rev:13 rq:1334483 version:2.11.1 Changes: -------- --- /work/SRC/openSUSE:Factory/python-PyPDF2/python-PyPDF2.changes 2025-05-12 16:55:51.346218714 +0200 +++ /work/SRC/openSUSE:Factory/.python-PyPDF2.new.1977/python-PyPDF2.changes 2026-02-23 16:15:21.268273115 +0100 @@ -1,0 +2,9 @@ +Mon Feb 23 10:42:53 UTC 2026 - Markéta Machová <[email protected]> + +- Add security patches: + * CVE-2025-55197.patch (bsc#1248089) + * CVE-2026-27024.patch (bsc#1258691) + * CVE-2026-27025.patch (bsc#1258692) + * CVE-2026-27026.patch (bsc#1258693) + +------------------------------------------------------------------- New: ---- CVE-2025-55197.patch CVE-2026-27024.patch CVE-2026-27025.patch CVE-2026-27026.patch ----------(New B)---------- New:- Add security patches: * CVE-2025-55197.patch (bsc#1248089) * CVE-2026-27024.patch (bsc#1258691) New: * CVE-2025-55197.patch (bsc#1248089) * CVE-2026-27024.patch (bsc#1258691) * CVE-2026-27025.patch (bsc#1258692) New: * CVE-2026-27024.patch (bsc#1258691) * CVE-2026-27025.patch (bsc#1258692) * CVE-2026-27026.patch (bsc#1258693) New: * CVE-2026-27025.patch (bsc#1258692) * CVE-2026-27026.patch (bsc#1258693) ----------(New E)---------- ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-PyPDF2.spec ++++++ --- /var/tmp/diff_new_pack.0qmZPm/_old 2026-02-23 16:15:21.776294061 +0100 +++ /var/tmp/diff_new_pack.0qmZPm/_new 2026-02-23 16:15:21.780294226 +0100 @@ -1,7 +1,7 @@ # # spec file for package python-PyPDF2 # -# Copyright (c) 2025 SUSE LLC +# Copyright (c) 2026 SUSE LLC and contributors # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -25,6 +25,14 @@ Group: Development/Languages/Python URL: https://github.com/py-pdf/PyPDF2 Source: https://github.com/py-pdf/PyPDF2/archive/refs/tags/%{version}.tar.gz +# PATCH-FIX-UPSTREAM CVE-2025-55197.patch bsc#1248089 +Patch0: CVE-2025-55197.patch +# PATCH-FIX-UPSTREAM CVE-2026-27024.patch bsc#1258691 +Patch1: CVE-2026-27024.patch +# PATCH-FIX-UPSTREAM CVE-2026-27025.patch bsc#1258692 +Patch2: CVE-2026-27025.patch +# PATCH-FIX-UPSTREAM CVE-2026-27026.patch bsc#1258693 +Patch3: CVE-2026-27026.patch BuildRequires: %{python_module pip} BuildRequires: %{python_module setuptools} BuildRequires: %{python_module wheel} @@ -49,7 +57,7 @@ It is therefore a useful tool for websites that manage or manipulate PDFs. %prep -%setup -q -n PyPDF2-%{version} +%autosetup -p1 -n PyPDF2-%{version} #remove unwanted shebang sed -i '/^#!/ d' PyPDF2/pagerange.py @@ -61,6 +69,8 @@ %python_expand %fdupes %{buildroot}%{$python_sitelib} chmod a-x CHANGELOG.md LICENSE README.md +# many tests need internet connection, cannot be run on OBS + %files %{python_files} %license LICENSE %doc CHANGELOG.md README.md ++++++ CVE-2025-55197.patch ++++++ >From bb3a69030fde7da545229438ff327b8c971cef49 Mon Sep 17 00:00:00 2001 From: Stefan <[email protected]> Date: Mon, 11 Aug 2025 16:10:25 +0200 Subject: [PATCH] SEC: Limit decompressed size for FlateDecode filter (#3430) Closes #3429. --- PyPDF2/_reader.py | 10 ++++- PyPDF2/errors.py | 4 ++ PyPDF2/filters.py | 89 +++++++++++++++++++++++++--------------- tests/example_files.yaml | 2 + tests/test_filters.py | 41 ++++++++++++++---- tests/test_reader.py | 15 +++++++ 6 files changed, 119 insertions(+), 42 deletions(-) Index: PyPDF2-2.11.1/PyPDF2/_reader.py =================================================================== --- PyPDF2-2.11.1.orig/PyPDF2/_reader.py +++ PyPDF2-2.11.1/PyPDF2/_reader.py @@ -1681,15 +1681,20 @@ class PdfReader: xrefstream = cast(ContentStream, read_object(stream, self)) assert cast(str, xrefstream["/Type"]) == "/XRef" self.cache_indirect_object(generation, idnum, xrefstream) - stream_data = BytesIO(b_(xrefstream.get_data())) + # Index pairs specify the subsections in the dictionary. If # none create one subsection that spans everything. - idx_pairs = xrefstream.get("/Index", [0, xrefstream.get("/Size")]) + if "/Size" not in xrefstream: + # According to table 17 of the PDF 2.0 specification, this key is required. + raise PdfReadError(f"Size missing from XRef stream {xrefstream!r}!") + idx_pairs = xrefstream.get("/Index", [0, xrefstream["/Size"]]) entry_sizes = cast(Dict[Any, Any], xrefstream.get("/W")) assert len(entry_sizes) >= 3 if self.strict and len(entry_sizes) > 3: raise PdfReadError(f"Too many entry sizes: {entry_sizes}") + stream_data = BytesIO(xrefstream.get_data()) + def get_entry(i: int) -> Union[int, Tuple[int, ...]]: # Reads the correct number of bytes for each entry. See the # discussion of the W parameter in PDF spec table 17. Index: PyPDF2-2.11.1/PyPDF2/errors.py =================================================================== --- PyPDF2-2.11.1.orig/PyPDF2/errors.py +++ PyPDF2-2.11.1/PyPDF2/errors.py @@ -46,3 +46,7 @@ class EmptyFileError(PdfReadError): STREAM_TRUNCATED_PREMATURELY = "Stream has ended unexpectedly" + + +class LimitReachedError(PyPdfError): + """Raised when a limit is reached.""" Index: PyPDF2-2.11.1/PyPDF2/filters.py =================================================================== --- PyPDF2-2.11.1.orig/PyPDF2/filters.py +++ PyPDF2-2.11.1/PyPDF2/filters.py @@ -58,12 +58,12 @@ from .constants import GraphicsStatePara from .constants import ImageAttributes as IA from .constants import LzwFilterParameters as LZW from .constants import StreamAttributes as SA -from .errors import PdfReadError, PdfStreamError +from .errors import LimitReachedError, PdfReadError, PdfStreamError def decompress(data: bytes) -> bytes: try: - return zlib.decompress(data) + return _decompress_with_limit(data) except zlib.error: d = zlib.decompressobj(zlib.MAX_WBITS | 32) result_str = b"" @@ -74,6 +74,18 @@ def decompress(data: bytes) -> bytes: pass return result_str +ZLIB_MAX_OUTPUT_LENGTH = 75_000_000 + + +def _decompress_with_limit(data: bytes) -> bytes: + decompressor = zlib.decompressobj() + result = decompressor.decompress(data, max_length=ZLIB_MAX_OUTPUT_LENGTH) + if decompressor.unconsumed_tail: + raise LimitReachedError( + f"Limit reached while decompressing. {len(decompressor.unconsumed_tail)} bytes remaining." + ) + return result + class FlateDecode: @staticmethod Index: PyPDF2-2.11.1/tests/test_filters.py =================================================================== --- PyPDF2-2.11.1.orig/tests/test_filters.py +++ PyPDF2-2.11.1/tests/test_filters.py @@ -1,4 +1,5 @@ import string +import zlib from io import BytesIO from itertools import product as cartesian_product from unittest.mock import patch @@ -6,13 +7,14 @@ from unittest.mock import patch import pytest from PyPDF2 import PdfReader -from PyPDF2.errors import PdfReadError, PdfStreamError +from PyPDF2.errors import LimitReachedError, PdfReadError, PdfStreamError from PyPDF2.filters import ( ASCII85Decode, ASCIIHexDecode, CCITParameters, CCITTFaxDecode, FlateDecode, + decompress, ) from PyPDF2.generic import ArrayObject, DictionaryObject, NumberObject ++++++ CVE-2026-27024.patch ++++++ >From bd2f6d052fe5941e85e37082c2a43453d48d1295 Mon Sep 17 00:00:00 2001 From: Stefan <[email protected]> Date: Tue, 17 Feb 2026 17:51:28 +0100 Subject: [PATCH] SEC: Detect cyclic references when accessing TreeObject.children (#3645) --- PyPDF2/generic/_data_structures.py | 11 ++++++++++- tests/generic/test_data_structures.py | 18 ++++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) Index: PyPDF2-2.11.1/PyPDF2/generic/_data_structures.py =================================================================== --- PyPDF2-2.11.1.orig/PyPDF2/generic/_data_structures.py +++ PyPDF2-2.11.1/PyPDF2/generic/_data_structures.py @@ -372,10 +372,19 @@ class TreeObject(DictionaryObject): return child_ref = self[NameObject("/First")] + last = self[NameObject("/Last")] child = child_ref.get_object() + visited: set[int] = set() while True: + child_id = id(child) + if child_id in visited: + logger_warning(f"Detected cycle in outline structure for {child}", __name__) + return + visited.add(child_id) + yield child - if child == self[NameObject("/Last")]: + + if child == last: return child_ref = child.get(NameObject("/Next")) # type: ignore if child_ref is None: Index: PyPDF2-2.11.1/tests/test_generic.py =================================================================== --- PyPDF2-2.11.1.orig/tests/test_generic.py +++ PyPDF2-2.11.1/tests/test_generic.py @@ -918,3 +918,17 @@ def test_create_string_object_force(): ) def test_float_object_decimal_to_string(value, expected): assert repr(FloatObject(value)) == expected + + +def test_tree_object__cyclic_reference(caplog): + writer = PdfWriter() + child1 = writer._add_object(DictionaryObject()) + child2 = writer._add_object(DictionaryObject({NameObject("/Next"): child1})) + child3 = writer._add_object(DictionaryObject({NameObject("/Next"): child2})) + child1.get_object()[NameObject("/Next")] = child3 + tree = TreeObject() + tree[NameObject("/First")] = child2 + tree[NameObject("/Last")] = writer._add_object(DictionaryObject()) + + assert list(tree.children()) == [child2.get_object(), child1.get_object(), child3.get_object()] + assert "Detected cycle in outline structure for " in caplog.text ++++++ CVE-2026-27025.patch ++++++ >From 77d7b8d7cfbe8dd179858dfa42666f73fc6e57a2 Mon Sep 17 00:00:00 2001 From: Stefan <[email protected]> Date: Tue, 17 Feb 2026 17:46:56 +0100 Subject: [PATCH] SEC: Limit size of `/ToUnicode` entries (#3646) --- PyPDF2/_cmap.py | 20 ++++++++++ tests/test_cmap.py | 91 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 109 insertions(+), 2 deletions(-) Index: PyPDF2-2.11.1/PyPDF2/_cmap.py =================================================================== --- PyPDF2-2.11.1.orig/PyPDF2/_cmap.py +++ PyPDF2-2.11.1/PyPDF2/_cmap.py @@ -5,7 +5,7 @@ from typing import Any, Dict, List, Tupl from ._codecs import adobe_glyphs, charset_encoding from ._utils import logger_warning -from .errors import PdfReadWarning +from .errors import PdfReadWarning, LimitReachedError from .generic import DecodedStreamObject, DictionaryObject @@ -262,6 +262,15 @@ def process_cm_line( return process_rg, process_char, multiline_rg +# Usual values should be up to 65_536. +MAPPING_DICTIONARY_SIZE_LIMIT = 100_000 + + +def _check_mapping_size(size: int) -> None: + if size > MAPPING_DICTIONARY_SIZE_LIMIT: + raise LimitReachedError(f"Maximum /ToUnicode size limit reached: {size} > {MAPPING_DICTIONARY_SIZE_LIMIT}.") + + def parse_bfrange( l: bytes, map_dict: Dict[Any, Any], @@ -273,6 +282,8 @@ def parse_bfrange( nbi = max(len(lst[0]), len(lst[1])) map_dict[-1] = ceil(nbi / 2) fmt = b"%%0%dX" % (map_dict[-1] * 2) + entry_count = len(int_entry) + _check_mapping_size(entry_count) if multiline_rg is not None: a = multiline_rg[0] # a, b not in the current line b = multiline_rg[1] @@ -280,6 +291,8 @@ def parse_bfrange( if sq == b"]": closure_found = True break + entry_count += 1 + _check_mapping_size(entry_count) map_dict[ unhexlify(fmt % a).decode( "charmap" if map_dict[-1] == 1 else "utf-16-be", @@ -296,6 +309,8 @@ def parse_bfrange( if sq == b"]": closure_found = True break + entry_count += 1 + _check_mapping_size(entry_count) map_dict[ unhexlify(fmt % a).decode( "charmap" if map_dict[-1] == 1 else "utf-16-be", @@ -308,6 +323,8 @@ def parse_bfrange( c = int(lst[2], 16) fmt2 = b"%%0%dX" % max(4, len(lst[2])) closure_found = True + range_size = max(0, b - a + 1) + _check_mapping_size(entry_count + range_size) # This can be checked beforehand. while a <= b: map_dict[ unhexlify(fmt % a).decode( @@ -323,6 +340,8 @@ def parse_bfrange( def parse_bfchar(l: bytes, map_dict: Dict[Any, Any], int_entry: List[int]) -> None: lst = [x for x in l.split(b" ") if x] + new_count = len(lst) // 2 + _check_mapping_size(len(int_entry) + new_count) # This can be checked beforehand. map_dict[-1] = len(lst[0]) // 2 while len(lst) > 1: map_to = "" Index: PyPDF2-2.11.1/tests/test_cmap.py =================================================================== --- PyPDF2-2.11.1.orig/tests/test_cmap.py +++ PyPDF2-2.11.1/tests/test_cmap.py @@ -3,7 +3,9 @@ from io import BytesIO import pytest from PyPDF2 import PdfReader -from PyPDF2.errors import PdfReadWarning +from PyPDF2._cmap import parse_bfchar, parse_bfrange +from PyPDF2.errors import PdfReadWarning, LimitReachedError +from PyPDF2.generic import StreamObject from . import get_pdf_from_url @@ -91,3 +93,89 @@ def test_iss1379(): name = "02voc.pdf" reader = PdfReader(BytesIO(get_pdf_from_url(url, name=name))) reader.pages[2].extract_text() + + +def test_parse_bfrange__iteration_limit(): + writer = PdfWriter() + + to_unicode = StreamObject() + to_unicode.set_data( + b"beginbfrange\n" + b"<00000000> <001FFFFF> <00000000>\n" + b"endbfrange\n" + ) + font = writer._add_object(DictionaryObject({ + NameObject("/Type"): NameObject("/Font"), + NameObject("/Subtype"): NameObject("/Type1"), + NameObject("/BaseFont"): NameObject("/Helvetica"), + NameObject("/ToUnicode"): to_unicode, + })) + + page = writer.add_blank_page(width=100, height=100) + page[NameObject("/Resources")] = DictionaryObject({ + NameObject("/Font"): DictionaryObject({ + NameObject("/F1"): font.indirect_reference, + }) + }) + + # Case without list, exceeding list directly. + with pytest.raises( + expected_exception=LimitReachedError, match=r"^Maximum /ToUnicode size limit reached: 2097152 > 100000\.$" + ): + _ = page.extract_text() + + # Use a pre-filled dummy list to simulate multiple calls where the upper bound does + # not overflow, but the overall size does. Case without list. + int_entry = [0] * 99_999 + map_dict = {} + with pytest.raises( + expected_exception=LimitReachedError, match=r"^Maximum /ToUnicode size limit reached: 165535 > 100000\.$" + ): + _ = parse_bfrange(line=b"0000 FFFF 0000", map_dict=map_dict, int_entry=int_entry, multiline_rg=None) + assert map_dict == {-1: 2} + + # Exceeding from previous call. + int_entry.append(1) + map_dict = {} + with pytest.raises( + expected_exception=LimitReachedError, match=r"^Maximum /ToUnicode size limit reached: 100001 > 100000\.$" + ): + _ = parse_bfrange(line=b"00000000 00000000 00000000", map_dict=map_dict, int_entry=int_entry, multiline_rg=None) + assert map_dict == {-1: 4} + + # multiline_rg + int_entry = [0] * 99_995 + map_dict = {-1: 1} + with pytest.raises( + expected_exception=LimitReachedError, match=r"^Maximum /ToUnicode size limit reached: 100001 > 100000\.$" + ): + _ = parse_bfrange( + line=b"0020 0021 0022 0023 0024 0025 0026 2019", + map_dict=map_dict, int_entry=int_entry, multiline_rg=(32, 251) + ) + assert map_dict == {-1: 1, " ": " ", "!": "!", '"': '"', "#": "#", "$": "$"} + + # No multiline_rg, but list. + int_entry = [0] * 99_995 + map_dict = {} + with pytest.raises( + expected_exception=LimitReachedError, match=r"^Maximum /ToUnicode size limit reached: 100001 > 100000\.$" + ): + _ = parse_bfrange( + line=b"01 8A [ FFFD FFFD FFFD FFFF FFAB AAAA BBBB", + map_dict=map_dict, int_entry=int_entry, multiline_rg=None + ) + assert map_dict == {-1: 1, "\x01": "�", "\x02": "�", "\x03": "�", "\x04": "\uffff", "\x05": "ᆱ"} + + +def test_parse_bfchar__iteration_limit(): + int_entry = [0] * 99_995 + map_dict = {} + with pytest.raises( + expected_exception=LimitReachedError, match=r"^Maximum /ToUnicode size limit reached: 100002 > 100000\.$" + ): + parse_bfchar( + line=b"0003 0020 0008 0025 0009 0026 000A 0027 000B 0028 000C 0029 000D 002A", + map_dict=map_dict, int_entry=int_entry, + ) + assert map_dict == {} ++++++ CVE-2026-27026.patch ++++++ >From 7905842d833f899f1d3228af7e7467ad80277016 Mon Sep 17 00:00:00 2001 From: Stefan <[email protected]> Date: Tue, 17 Feb 2026 17:38:44 +0100 Subject: [PATCH] SEC: Limit FlateDecode recovery attempts (#3644) --- docs/user/security.md | 3 +++ PyPDF2/filters.py | 16 ++++++++++++---- tests/test_filters.py | 13 ++++++++++--- 3 files changed, 25 insertions(+), 7 deletions(-) Index: PyPDF2-2.11.1/PyPDF2/filters.py =================================================================== --- PyPDF2-2.11.1.orig/PyPDF2/filters.py +++ PyPDF2-2.11.1/PyPDF2/filters.py @@ -60,6 +60,11 @@ from .constants import LzwFilterParamete from .constants import StreamAttributes as SA from .errors import LimitReachedError, PdfReadError, PdfStreamError +ZLIB_MAX_RECOVERY_INPUT_LENGTH = 5_000_000 + +# Reuse cached 1-byte values in the fallback loop to avoid per-byte allocations. +_SINGLE_BYTES = tuple(bytes((i,)) for i in range(256)) + def decompress(data: bytes) -> bytes: try: @@ -67,10 +72,22 @@ def decompress(data: bytes) -> bytes: except zlib.error: d = zlib.decompressobj(zlib.MAX_WBITS | 32) result_str = b"" - for b in [data[i : i + 1] for i in range(len(data))]: + remaining_limit = ZLIB_MAX_OUTPUT_LENGTH + for index in range(len(data)): + chunk = _SINGLE_BYTES[data[index]] try: - result_str += d.decompress(b) + decompressed = d.decompress(chunk) + result_str += decompressed + remaining_limit -= len(decompressed) + if remaining_limit <= 0: + raise LimitReachedError( + f"Limit reached while decompressing. {len(data) - index} bytes remaining." + ) except zlib.error: + if index > ZLIB_MAX_RECOVERY_INPUT_LENGTH: + raise LimitReachedError( + f"Recovery limit reached while decompressing. {data_length - index} bytes remaining." + ) pass return result_str
