Skip to content

Commit bfd5b6c

Browse files
icanhasmathclaude
andcommitted
Fix CVE-2026-33123: bound array-based ContentStream
A content stream supplied as an array of many stream objects (or whose elements concatenate to an enormous size) made ContentStream.__init__ build the combined buffer with an unbounded `data += ...` loop, causing CPU/memory exhaustion (CWE-400/CWE-407). Cap the number of array elements (CONTENT_STREAM_ARRAY_MAX_LENGTH = 10000) and the total concatenated size (MAX_ARRAY_BASED_STREAM_OUTPUT_LENGTH = 75 MB), raising PdfReadError when exceeded, and join via a list to avoid the quadratic concatenation. Mirrors upstream pypdf 6.9.1 (PR py-pdf#3686). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 690c679 commit bfd5b6c

2 files changed

Lines changed: 66 additions & 4 deletions

File tree

PyPDF2/generic.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@
6868
# stream /Length. Set to 0 to disable (only safe for fully trusted input).
6969
MAX_DECLARED_STREAM_LENGTH = 75000000 # 75 MB
7070

71+
# CVE-2026-33123: bound array-based content streams. Set to 0 to disable.
72+
CONTENT_STREAM_ARRAY_MAX_LENGTH = 10000 # max number of array elements
73+
MAX_ARRAY_BASED_STREAM_OUTPUT_LENGTH = 75000000 # 75 MB total concatenated
74+
7175
ObjectPrefix = b_("/<[tf(n%")
7276
NumberSigns = b_("+-")
7377
IndirectPattern = re.compile(b_(r"[+-]?(\d+)\s+(\d+)\s+R[^a-zA-Z]"))
@@ -1220,10 +1224,32 @@ def __init__(self, stream, pdf):
12201224
# multiple StreamObjects to be cat'd together.
12211225
stream = stream.get_object()
12221226
if isinstance(stream, ArrayObject):
1223-
data = b_("")
1227+
# CVE-2026-33123: bound both the number of array elements and the
1228+
# total concatenated size so a crafted array-based content stream
1229+
# cannot exhaust CPU/memory.
1230+
if (
1231+
CONTENT_STREAM_ARRAY_MAX_LENGTH
1232+
and len(stream) > CONTENT_STREAM_ARRAY_MAX_LENGTH
1233+
):
1234+
raise PdfReadError(
1235+
"Content stream array has %d elements, exceeding the "
1236+
"maximum of %d." % (len(stream), CONTENT_STREAM_ARRAY_MAX_LENGTH)
1237+
)
1238+
parts = []
1239+
total = 0
12241240
for s in stream:
1225-
data += b_(s.get_object().get_data())
1226-
stream = BytesIO(b_(data))
1241+
new_data = b_(s.get_object().get_data())
1242+
total += len(new_data)
1243+
if (
1244+
MAX_ARRAY_BASED_STREAM_OUTPUT_LENGTH
1245+
and total > MAX_ARRAY_BASED_STREAM_OUTPUT_LENGTH
1246+
):
1247+
raise PdfReadError(
1248+
"Content stream array output exceeds the maximum of "
1249+
"%d bytes." % MAX_ARRAY_BASED_STREAM_OUTPUT_LENGTH
1250+
)
1251+
parts.append(new_data)
1252+
stream = BytesIO(b_("").join(parts))
12271253
else:
12281254
stream = BytesIO(b_(stream.get_data()))
12291255
self.__parseContentStream(stream)

Tests/test_security_generic.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,20 @@
1919

2020
from PyPDF2 import generic
2121
from PyPDF2.errors import PdfReadError
22-
from PyPDF2.generic import DictionaryObject, NameObject, TreeObject
22+
from PyPDF2.generic import (
23+
ArrayObject,
24+
ContentStream,
25+
DecodedStreamObject,
26+
DictionaryObject,
27+
NameObject,
28+
TreeObject,
29+
)
30+
31+
32+
def _stream(data):
33+
so = DecodedStreamObject()
34+
so._data = data
35+
return so
2336

2437

2538
def _node(**kv):
@@ -67,3 +80,26 @@ def test_small_stream_length_ok():
6780
data = b"<< /Length 3 >>\nstream\nabc\nendstream"
6881
obj = generic.DictionaryObject.read_from_stream(BytesIO(data), pdf)
6982
assert obj.get_data() in (b"abc", "abc")
83+
84+
85+
# --- CVE-2026-33123: array-based ContentStream caps -----------------------
86+
87+
def test_content_stream_array_element_cap(monkeypatch):
88+
monkeypatch.setattr(generic, "CONTENT_STREAM_ARRAY_MAX_LENGTH", 2)
89+
arr = ArrayObject([_stream(b"q\n"), _stream(b"Q\n"), _stream(b"q\n")])
90+
with pytest.raises(PdfReadError):
91+
ContentStream(arr, Mock())
92+
93+
94+
def test_content_stream_array_output_cap(monkeypatch):
95+
monkeypatch.setattr(generic, "MAX_ARRAY_BASED_STREAM_OUTPUT_LENGTH", 3)
96+
arr = ArrayObject([_stream(b"q\n"), _stream(b"Q\n")]) # 4 bytes > 3
97+
with pytest.raises(PdfReadError):
98+
ContentStream(arr, Mock())
99+
100+
101+
def test_content_stream_array_ok():
102+
arr = ArrayObject([_stream(b"q\n"), _stream(b"Q\n")])
103+
cs = ContentStream(arr, Mock())
104+
assert len(cs.operations) == 2
105+

0 commit comments

Comments
 (0)