Skip to content

Commit 5fac30a

Browse files
authored
Reject trailing Brotli data to prevent infinite drain loops (#232)
1 parent 26d7e3f commit 5fac30a

3 files changed

Lines changed: 54 additions & 0 deletions

File tree

‎HISTORY.rst‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
Changelog
22
=========
33

4+
Unreleased
5+
----------
6+
7+
- Changed one-shot ``decompress()`` and streaming ``Decompressor.process()`` /
8+
``Decompressor.decompress()`` to raise ``brotlicffi.error`` for trailing bytes
9+
after a complete Brotli stream, including calls without an output limit.
10+
Previously tolerated input is now rejected, and any output produced during
11+
the failing call is discarded. This prevents infinite drain loops in
12+
streaming consumers.
13+
414
1.2.0.2 (2026-08-21)
515
--------------------
616

‎src/brotlicffi/_api.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,11 @@ def _decompress(self, data, output_buffer_limit):
492492
b"Decompression error: %s" % ffi.string(error_message)
493493
)
494494

495+
# Reject bytes remaining after the Brotli stream finishes.
496+
if rc == lib.BROTLI_DECODER_RESULT_SUCCESS and available_in[0]:
497+
self._unconsumed_data = b''
498+
raise error("Decompression error: trailing data after stream.")
499+
495500
# Next, copy the result out.
496501
chunk = ffi.buffer(out_buffer, buffer_size - available_out[0])[:]
497502
chunks.append(chunk)

‎test/test_simple_decompression.py‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,45 @@ def test_high_expansion_prefix_without_output_buffer_limit():
108108
assert result == uncompressed
109109

110110

111+
def test_decompression_rejects_trailing_data():
112+
data = brotlicffi.compress(b'A' * 100) + b'x'
113+
with pytest.raises(brotlicffi.error, match='trailing data'):
114+
brotlicffi.decompress(data)
115+
116+
117+
@pytest.mark.parametrize('method', ['decompress', 'process'])
118+
@pytest.mark.parametrize('output_buffer_limit', [None, 50])
119+
def test_decompressobj_rejects_trailing_data(method, output_buffer_limit):
120+
o = brotlicffi.Decompressor()
121+
decompress = getattr(o, method)
122+
# Without a limit, decoding needs multiple internal buffer iterations.
123+
data = brotlicffi.compress(b'A' * 100) + b'tail'
124+
if output_buffer_limit is not None:
125+
assert decompress(data, output_buffer_limit=50) == b'A' * 50
126+
assert not o.can_accept_more_data()
127+
data = b''
128+
129+
with pytest.raises(brotlicffi.error, match='trailing data'):
130+
decompress(data, output_buffer_limit=output_buffer_limit)
131+
132+
assert o._unconsumed_data == b''
133+
with pytest.raises(brotlicffi.error, match='trailing data'):
134+
decompress(b'x', output_buffer_limit=output_buffer_limit)
135+
136+
137+
@pytest.mark.parametrize('output_buffer_limit', [None, 100])
138+
def test_decompressobj_rejects_data_after_finished(output_buffer_limit):
139+
o = brotlicffi.Decompressor()
140+
data = brotlicffi.compress(b'A' * 100)
141+
assert (
142+
o.process(data, output_buffer_limit=output_buffer_limit) == b'A' * 100
143+
)
144+
assert o.is_finished()
145+
146+
with pytest.raises(brotlicffi.error, match='trailing data'):
147+
o.process(b'x', output_buffer_limit=output_buffer_limit)
148+
149+
111150
def test_drip_feed(simple_compressed_file):
112151
"""
113152
Sending in the data one byte at a time still works.

0 commit comments

Comments
 (0)