diff --git a/HISTORY.rst b/HISTORY.rst index ecad247..4eef9ab 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,12 @@ Changelog ========= +Unreleased +---------- + +- Fixed infinite loops in streaming consumers by rejecting trailing data + after a complete Brotli stream. + 1.2.0.2 (2026-08-21) -------------------- diff --git a/src/brotlicffi/_api.py b/src/brotlicffi/_api.py index 75a5182..afcbc41 100644 --- a/src/brotlicffi/_api.py +++ b/src/brotlicffi/_api.py @@ -492,6 +492,10 @@ def _decompress(self, data, output_buffer_limit): b"Decompression error: %s" % ffi.string(error_message) ) + # Reject bytes remaining after the Brotli stream finishes. + if rc == lib.BROTLI_DECODER_RESULT_SUCCESS and available_in[0]: + raise error("Decompression error: trailing data after stream.") + # Next, copy the result out. chunk = ffi.buffer(out_buffer, buffer_size - available_out[0])[:] chunks.append(chunk) diff --git a/test/test_simple_decompression.py b/test/test_simple_decompression.py index dea8c96..2d0cc52 100644 --- a/test/test_simple_decompression.py +++ b/test/test_simple_decompression.py @@ -108,6 +108,19 @@ def test_high_expansion_prefix_without_output_buffer_limit(): assert result == uncompressed +@pytest.mark.parametrize('output_buffer_limit', [None, 50]) +def test_decompressobj_rejects_trailing_data(output_buffer_limit): + o = brotlicffi.Decompressor() + data = brotlicffi.compress(b'A' * 100) + b'tail' + if output_buffer_limit is not None: + assert o.decompress(data, output_buffer_limit=50) == b'A' * 50 + assert not o.can_accept_more_data() + data = b'' + + with pytest.raises(brotlicffi.error, match='trailing data'): + o.decompress(data, output_buffer_limit=output_buffer_limit) + + def test_drip_feed(simple_compressed_file): """ Sending in the data one byte at a time still works.