VYPR
Moderate severityNVD Advisory· Published Feb 27, 2026· Updated Mar 3, 2026

Manipulated RunLengthDecode streams can exhaust RAM

CVE-2026-28351

Description

pypdf is a free and open-source pure-python PDF library. Prior to version 6.7.4, an attacker who uses this vulnerability can craft a PDF which leads to large memory usage. This requires parsing the content stream using the RunLengthDecode filter. This has been fixed in pypdf 6.7.4. As a workaround, consider applying the changes from PR #3664.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
pypdfPyPI
< 6.7.46.7.4

Affected products

1

Patches

1
f309c6003746

SEC: Allow limiting output length for RunLengthDecode filter (#3664)

https://github.com/py-pdf/pypdfStefanFeb 27, 2026via ghsa
3 files changed · +25 2
  • docs/user/security.md+1 0 modified
    @@ -13,6 +13,7 @@ aware of the possible side effects, you can modify the following constants which
     
     * `pypdf.filters.ZLIB_MAX_OUTPUT_LENGTH` for the *FlateDecode* filter (zlib compression)
     * `pypdf.filters.LZW_MAX_OUTPUT_LENGTH` for the *LZWDecode* filter (LZW compression)
    +* `pypdf.filters.RUN_LENGTH_MAX_OUTPUT_LENGTH` for the *RunLengthDecode* filter (run-length compression)
     
     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.
    
  • pypdf/filters.py+7 2 modified
    @@ -73,6 +73,7 @@
     
     JBIG2_MAX_OUTPUT_LENGTH = 75_000_000
     LZW_MAX_OUTPUT_LENGTH = 75_000_000
    +RUN_LENGTH_MAX_OUTPUT_LENGTH = 75_000_000
     ZLIB_MAX_OUTPUT_LENGTH = 75_000_000
     ZLIB_MAX_RECOVERY_INPUT_LENGTH = 5_000_000
     
    @@ -408,16 +409,17 @@ def decode(
             """
             lst = []
             index = 0
    +        data_length = len(data)
    +        total_length = 0
             while True:
    -            if index >= len(data):
    +            if index >= data_length:
                     logger_warning(
                         "missing EOD in RunLengthDecode, check if output is OK", __name__
                     )
                     break  # Reached end of string without an EOD
                 length = data[index]
                 index += 1
                 if length == 128:
    -                data_length = len(data)
                     if index < data_length:
                         # We should first check, if we have an inner stream from a multi-encoded
                         # stream with a faulty trailing newline that we can decode properly.
    @@ -442,6 +444,9 @@ def decode(
                     length = 257 - length
                     lst.append(bytes((data[index],)) * length)
                     index += 1
    +            total_length += length
    +            if total_length > RUN_LENGTH_MAX_OUTPUT_LENGTH:
    +                raise LimitReachedError("Limit reached while decompressing.")
             return b"".join(lst)
     
     
    
  • tests/test_filters.py+17 0 modified
    @@ -1026,3 +1026,20 @@ def test_flatedecode__columns_is_zero():
     
         with pytest.raises(expected_exception=PdfReadError, match=r"^Expected positive number for /Columns, got 0!$"):
             codec.decode(codec.encode(data), parameters)
    +
    +
    +def test_runlengthdecode__decode_limit():
    +    uncompressed_size = 76 * 1024 * 1024  # 76 MB target
    +    runs = uncompressed_size // 128
    +    encoded = (b"\x81A" * runs) + b"\x80"
    +
    +    with pytest.raises(expected_exception=LimitReachedError, match=r"^Limit reached while decompressing\.$"):
    +        RunLengthDecode.decode(encoded)
    +
    +    uncompressed_size = 5 * 1024
    +    runs = uncompressed_size // 128
    +    encoded = (b"\x81A" * runs) + b"\x80"
    +
    +    # Use a very low limit for this exact comparison, otherwise *pytest* takes ages to render a failure diff.
    +    with mock.patch("pypdf.filters.RUN_LENGTH_MAX_OUTPUT_LENGTH", uncompressed_size):
    +        assert RunLengthDecode.decode(encoded) == b"A" * uncompressed_size
    

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.