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

PackageAffected versionsPatched versions
pypdfPyPI
< 6.7.16.7.1

Affected products

1

Patches

1
7905842d833f

SEC: Limit FlateDecode recovery attempts (#3644)

https://github.com/py-pdf/pypdfStefanFeb 17, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.