Moderate severityNVD Advisory· Published Feb 20, 2026· Updated Feb 24, 2026
pypdf possibly has long runtimes for malformed FlateDecode streams
CVE-2026-27026
Description
pypdf is a free and open-source pure-python PDF library. Prior to 6.7.1, an attacker who uses this vulnerability can craft a PDF which leads to long runtimes. This requires a malformed /FlateDecode stream, where the byte-by-byte decompression is used. This vulnerability is fixed in 6.7.1.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
pypdfPyPI | < 6.7.1 | 6.7.1 |
Affected products
1Patches
17905842d833fSEC: Limit FlateDecode recovery attempts (#3644)
3 files changed · +25 −7
docs/user/security.md+3 −0 modified@@ -17,6 +17,9 @@ aware of the possible side effects, you can modify the following constants which For JBIG2 images, there is a similar parameter to limit the memory usage during decoding: `pypdf.filters.JBIG2_MAX_OUTPUT_LENGTH` It defaults to 75 MB as well. +For the *FlateDecode* filter, the number of bytes to attempt recovery with can be set by `pypdf.filters.ZLIB_MAX_RECOVERY_INPUT_LENGTH`. +It defaults to 5 MB due to the much more complex recovery approach. + ### Reading *pypdf* currently employs the following reading limits on *PdfReader* instances:
pypdf/filters.py+12 −4 modified@@ -74,7 +74,10 @@ JBIG2_MAX_OUTPUT_LENGTH = 75_000_000 LZW_MAX_OUTPUT_LENGTH = 75_000_000 ZLIB_MAX_OUTPUT_LENGTH = 75_000_000 +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_with_limit(data: bytes) -> bytes: @@ -133,18 +136,23 @@ def decompress(data: bytes) -> bytes: decompressor = zlib.decompressobj(zlib.MAX_WBITS | 32) result_str = b"" remaining_limit = ZLIB_MAX_OUTPUT_LENGTH - data_single_bytes = [data[i : i + 1] for i in range(len(data))] + data_length = len(data) known_errors = set() - for index, b in enumerate(data_single_bytes): + for index in range(data_length): + chunk = _SINGLE_BYTES[data[index]] try: - decompressed = decompressor.decompress(b, max_length=remaining_limit) + decompressed = decompressor.decompress(chunk, max_length=remaining_limit) result_str += decompressed remaining_limit -= len(decompressed) if remaining_limit <= 0: raise LimitReachedError( - f"Limit reached while decompressing. {len(data_single_bytes) - index} bytes remaining." + f"Limit reached while decompressing. {data_length - index} bytes remaining." ) except zlib.error as error: + if index > ZLIB_MAX_RECOVERY_INPUT_LENGTH: + raise LimitReachedError( + f"Recovery limit reached while decompressing. {data_length - index} bytes remaining." + ) error_str = str(error) if error_str in known_errors: continue
tests/test_filters.py+10 −3 modified@@ -876,11 +876,11 @@ def test_decompress(): data = string.printable.encode("utf-8") + string.printable[::-1].encode("utf-8") compressed = FlateDecode.encode(data) - # # Decompress regularly. + # Decompress regularly. decompressed = decompress(compressed) assert decompressed == data - # # Decompress byte-wise. + # Decompress byte-wise. with mock.patch("pypdf.filters._decompress_with_limit", side_effect=zlib.error): decompressed = decompress(compressed) assert decompressed == data @@ -889,10 +889,17 @@ def test_decompress(): with mock.patch("pypdf.filters._decompress_with_limit", side_effect=zlib.error), \ mock.patch("pypdf.filters.ZLIB_MAX_OUTPUT_LENGTH", len(compressed) - 13), \ pytest.raises( - LimitReachedError, match=r"^Limit reached while decompressing\. 12 bytes remaining\." + LimitReachedError, match=r"^Limit reached while decompressing\. 12 bytes remaining\.$" ): decompress(compressed) + # Decompress byte-wise with input limit. + with mock.patch("pypdf.filters.ZLIB_MAX_RECOVERY_INPUT_LENGTH", 1000), \ + pytest.raises( + LimitReachedError, match=r"^Recovery limit reached while decompressing\. 336 bytes remaining\.$" + ): + decompress(b"A" * 1337) + def test_decompress__logging_on_invalid_data(caplog): """We do not like suddenly getting empty outputs for non-empty inputs without a warning."""
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/advisories/GHSA-9mvc-8737-8j8hghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-27026ghsaADVISORY
- github.com/py-pdf/pypdf/commit/7905842d833f899f1d3228af7e7467ad80277016ghsax_refsource_MISCWEB
- github.com/py-pdf/pypdf/pull/3644ghsax_refsource_MISCWEB
- github.com/py-pdf/pypdf/releases/tag/6.7.1ghsax_refsource_MISCWEB
- github.com/py-pdf/pypdf/security/advisories/GHSA-9mvc-8737-8j8hghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.