Skip to content

Commit 5a33a46

Browse files
SEC: Avoid infinite loops for incomplete ASCII85 and ASCIIHex inline images (#3892)
1 parent 1ee4e58 commit 5a33a46

5 files changed

Lines changed: 46 additions & 22 deletions

File tree

pypdf/generic/_data_structures.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1396,7 +1396,7 @@ def _read_inline_image(self, stream: StreamType) -> dict[str, Any]:
13961396
while True:
13971397
tok = read_non_whitespace(stream)
13981398
if not tok:
1399-
raise PdfStreamError("Unexpected end of stream.")
1399+
raise PdfReadError("Unexpected end of stream.")
14001400
stream.seek(-1, 1)
14011401
if tok == b"I":
14021402
# "ID" - begin of image data

pypdf/generic/_image_inline.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,9 @@ def extract_inline__ascii_hex_decode(stream: StreamType) -> bytes:
6161
data_out = bytearray()
6262
# Read data until delimiter > and EI as backup.
6363
while True:
64-
data_buffered = read_non_whitespace(stream) + stream.read(BUFFER_SIZE)
65-
if not data_buffered:
66-
raise PdfReadError("Unexpected end of stream")
64+
data_buffered = read_non_whitespace(stream) + (read_bytes := stream.read(BUFFER_SIZE))
65+
if not data_buffered or not read_bytes:
66+
raise PdfReadError("Unexpected end of stream.")
6767
pos_tok = data_buffered.find(b">")
6868
if pos_tok >= 0: # found >
6969
data_out += data_buffered[: pos_tok + 1]
@@ -81,13 +81,13 @@ def extract_inline__ascii_hex_decode(stream: StreamType) -> bytes:
8181
break
8282
if len(data_buffered) == 2:
8383
data_out += data_buffered
84-
raise PdfReadError("Unexpected end of stream")
84+
raise PdfReadError("Unexpected end of stream.")
8585
# Neither > nor EI found
8686
data_out += data_buffered[:-2]
8787
stream.seek(-2, 1)
8888

8989
if not _check_end_image_marker(stream):
90-
raise PdfReadError("EI stream not found")
90+
raise PdfReadError("EI stream not found.")
9191
return bytes(data_out)
9292

9393

@@ -99,24 +99,24 @@ def extract_inline__ascii85_decode(stream: StreamType) -> bytes:
9999
data_out = bytearray()
100100
# Read data until delimiter ~>
101101
while True:
102-
data_buffered = read_non_whitespace(stream) + stream.read(BUFFER_SIZE)
103-
if not data_buffered:
104-
raise PdfReadError("Unexpected end of stream")
102+
data_buffered = read_non_whitespace(stream) + (read_bytes := stream.read(BUFFER_SIZE))
103+
if not data_buffered or not read_bytes:
104+
raise PdfReadError("Unexpected end of stream.")
105105
pos_tok = data_buffered.find(b"~>")
106106
if pos_tok >= 0: # found!
107107
data_out += data_buffered[: pos_tok + 2]
108108
stream.seek(-len(data_buffered) + pos_tok + 2, 1)
109109
break
110110
if len(data_buffered) == 2: # end of buffer
111111
data_out += data_buffered
112-
raise PdfReadError("Unexpected end of stream")
112+
raise PdfReadError("Unexpected end of stream.")
113113
data_out += data_buffered[
114114
:-2
115115
] # back by one char in case of in the middle of ~>
116116
stream.seek(-2, 1)
117117

118118
if not _check_end_image_marker(stream):
119-
raise PdfReadError("EI stream not found")
119+
raise PdfReadError("EI stream not found.")
120120
return bytes(data_out)
121121

122122

@@ -130,7 +130,7 @@ def extract_inline__run_length_decode(stream: StreamType) -> bytes:
130130
while True:
131131
data_buffered = stream.read(BUFFER_SIZE)
132132
if not data_buffered:
133-
raise PdfReadError("Unexpected end of stream")
133+
raise PdfReadError("Unexpected end of stream.")
134134
pos_tok = data_buffered.find(b"\x80")
135135
if pos_tok >= 0: # found
136136
# Ideally, we could just use plain run-length decoding here, where 80_16 = 128_10
@@ -155,7 +155,7 @@ def extract_inline__run_length_decode(stream: StreamType) -> bytes:
155155
data_out += data_buffered
156156

157157
if not _check_end_image_marker(stream):
158-
raise PdfReadError("EI stream not found")
158+
raise PdfReadError("EI stream not found.")
159159
return bytes(data_out)
160160

161161

@@ -169,7 +169,7 @@ def read(length: int) -> bytes:
169169
# If the object is in non-blocking mode and no bytes are available, `None` is returned.
170170
_result = stream.read(length)
171171
if _result is None or len(_result) != length:
172-
raise PdfReadError("Unexpected end of stream")
172+
raise PdfReadError("Unexpected end of stream.")
173173
return _result
174174

175175
data_out = bytearray()
@@ -202,7 +202,7 @@ def read(length: int) -> bytes:
202202
data_out += read(sz - 2)
203203

204204
if not _check_end_image_marker(stream):
205-
raise PdfReadError("EI stream not found")
205+
raise PdfReadError("EI stream not found.")
206206
return bytes(data_out)
207207

208208

@@ -213,7 +213,7 @@ def extract_inline_default(stream: StreamType) -> bytes:
213213
while True:
214214
data_buffered = stream.read(BUFFER_SIZE)
215215
if not data_buffered:
216-
raise PdfReadError("Unexpected end of stream")
216+
raise PdfReadError("Unexpected end of stream.")
217217
pos_ei = data_buffered.find(
218218
b"E"
219219
) # We can not look straight for "EI" because it may not have been loaded in the buffer

tests/generic/test_data_structures.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import pytest
1111

1212
from pypdf import PdfReader, PdfWriter
13-
from pypdf.errors import LimitReachedError, PdfReadError, PdfStreamError
13+
from pypdf.errors import LimitReachedError, PdfReadError
1414
from pypdf.generic import (
1515
ArrayObject,
1616
ByteStringObject,
@@ -460,12 +460,13 @@ def test_content_stream__parse_content_stream__limits() -> None:
460460
content_stream._parse_content_stream(stream)
461461

462462

463+
@pytest.mark.timeout(5)
463464
def test_content_stream__read_inline_image__end_of_stream() -> None:
464465
# Broken content stream, for example due to filter errors.
465466
# Specific example:
466467
# Error -3 while decompressing data: invalid distance too far back
467468
# b'q 0.1 0 0 0.1 0 0 cm\n/R7 gs\n0 g\nq 4.8 0 0 -135.6 2155.08 7150.48 cm\nBI\n/IM true\n/W001'
468469
content_stream = ContentStream(stream=None, pdf=None)
469470

470-
with pytest.raises(expected_exception=PdfStreamError, match=r"^Unexpected end of stream\.$"):
471+
with pytest.raises(expected_exception=PdfReadError, match=r"^Unexpected end of stream\.$"):
471472
content_stream._read_inline_image(BytesIO(b"\n/IM true\n/W001"))

tests/generic/test_image_inline.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55

66
from pypdf import PdfReader
77
from pypdf.errors import PdfReadError
8-
from pypdf.generic._image_inline import is_followed_by_binary_data
8+
from pypdf.generic._image_inline import (
9+
extract_inline__ascii85_decode,
10+
extract_inline__ascii_hex_decode,
11+
is_followed_by_binary_data,
12+
)
913
from tests import get_data_from_url
1014

1115

@@ -74,7 +78,7 @@ def test_extract_inline_dct__early_end_of_file() -> None:
7478
page = reader.pages[0]
7579

7680
with pytest.raises(
77-
expected_exception=PdfReadError, match=r"^Unexpected end of stream$"
81+
expected_exception=PdfReadError, match=r"^Unexpected end of stream\.$"
7882
):
7983
image = page.images[0].image
8084
assert image is not None
@@ -91,3 +95,23 @@ def test_extract_inline_dct__multiple_eod() -> None:
9195
for image in page.images:
9296
assert image.image is not None
9397
_ = image.image.load()
98+
99+
100+
@pytest.mark.timeout(5)
101+
def test_extract_inline__ascii_hex_decode__early_end_of_file() -> None:
102+
stream = BytesIO(b"ABCDE\nF G")
103+
104+
with pytest.raises(expected_exception=PdfReadError, match=r"^Unexpected end of stream\.$"):
105+
extract_inline__ascii_hex_decode(stream)
106+
107+
108+
@pytest.mark.timeout(5)
109+
def test_extract_inline__ascii85_decode__early_end_of_file() -> None:
110+
# Broken content stream, for example due to filter errors.
111+
# Specific example:
112+
# Error -3 while decompressing data: invalid distance too far back
113+
# b'[...] \nBI\n/W 16 /H 16 /BPC 8 /CS /RGB /F [/A85 /Fl]\nID\nGar8O(o6*i%*56~\ne L\ne L9/ LL9/ L'
114+
stream = BytesIO(b"Gar8O(o6*i%*56~\ne L\ne L9/ LL9/ L")
115+
116+
with pytest.raises(expected_exception=PdfReadError, match=r"^Unexpected end of stream\.$"):
117+
extract_inline__ascii85_decode(stream)

tests/test_workflows.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -943,9 +943,8 @@ def test_extra_test_iss1541():
943943
reader = PdfReader(
944944
BytesIO(bytes(b.getbuffer()).replace(b"EI \n", b"E! \n")), strict=False
945945
)
946-
with pytest.raises(PdfReadError) as exc:
946+
with pytest.raises(expected_exception=PdfReadError, match=r"^Unexpected end of stream\.$"):
947947
reader.pages[0].extract_text()
948-
assert exc.value.args[0] == "Unexpected end of stream"
949948

950949

951950
@pytest.mark.enable_socket

0 commit comments

Comments
 (0)