diff --git a/CHANGES/10355.bugfix.rst b/CHANGES/10355.bugfix.rst new file mode 100644 index 00000000000..99db3382cd4 --- /dev/null +++ b/CHANGES/10355.bugfix.rst @@ -0,0 +1,5 @@ +Fixed missing CRLF in a malformed chunked encoded message body -- by +:user:`xdegaye`. + +A missing CRLF in a chunked encoded message now raises an exception instead of +silently discarding the following part of the message. diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py index d44f4cb12f1..ddb03a15ef7 100644 --- a/aiohttp/http_parser.py +++ b/aiohttp/http_parser.py @@ -880,6 +880,19 @@ def feed_data( chunk = chunk[len(SEP) :] self._chunk = ChunkState.PARSE_CHUNKED_SIZE else: + length = len(chunk) + if ( + self._lax + and length + or ( + not self._lax + and (length and chunk[:1] != SEP[:1] or length > 1) + ) + ): + exc = TransferEncodingError("Missing CRLF at chunk end") + set_exception(self.payload, exc) + raise exc + # Get the CRLF or the missing LF in the next chunk. self._chunk_tail = chunk return False, b"" diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py index f7489bc7656..39308f9996b 100644 --- a/tests/test_http_parser.py +++ b/tests/test_http_parser.py @@ -1761,6 +1761,69 @@ async def test_parse_chunked_payload_split_end_trailers4( assert out.is_eof() assert b"asdf" == b"".join(out._buffer) + async def test_parse_chunked_payload_split_CRLF( + self, protocol: BaseProtocol + ) -> None: + loop = asyncio.get_running_loop() + for lax, SEP in ((False, b"\r\n"), (True, b"\n")): + out = aiohttp.StreamReader(protocol, 2**16, loop=loop) + p = HttpPayloadParser(out, chunked=True, lax=lax) + p.feed_data(b"4\r\nasdf", SEP=SEP) # type: ignore[arg-type] + p.feed_data(b"\r\n0\r\n\r\n", SEP=SEP) # type: ignore[arg-type] + + assert out.is_eof() + assert b"asdf" == b"".join(out._buffer) + + async def test_parse_chunked_payload_split_CRLF2( + self, protocol: BaseProtocol + ) -> None: + loop = asyncio.get_running_loop() + for lax, SEP in ((False, b"\r\n"), (True, b"\n")): + out = aiohttp.StreamReader(protocol, 2**16, loop=loop) + p = HttpPayloadParser(out, chunked=True, lax=lax) + p.feed_data(b"4\r\nasdf\r", SEP=SEP) # type: ignore[arg-type] + p.feed_data(b"\n0\r\n\r\n", SEP=SEP) # type: ignore[arg-type] + + assert out.is_eof() + assert b"asdf" == b"".join(out._buffer) + + async def test_parse_chunked_payload_split_CRLF3( + self, protocol: BaseProtocol + ) -> None: + loop = asyncio.get_running_loop() + for lax, SEP in ((False, b"\r\n"), (True, b"\n")): + out = aiohttp.StreamReader(protocol, 2**16, loop=loop) + p = HttpPayloadParser(out, chunked=True, lax=lax) + with pytest.raises(http_exceptions.TransferEncodingError): + p.feed_data(b"4\r\nasdf", SEP=SEP) # type: ignore[arg-type] + p.feed_data(b"X\n0\r\n\r\n", SEP=SEP) # type: ignore[arg-type] + exc = out.exception() + assert isinstance(exc, http_exceptions.TransferEncodingError) + assert "Missing CRLF" in exc.args[0] + + async def test_parse_chunked_payload_split_CRLF4( + self, protocol: BaseProtocol + ) -> None: + out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop()) + p = HttpPayloadParser(out, chunked=True) + with pytest.raises(http_exceptions.TransferEncodingError): + p.feed_data(b"4\r\nasdfX") + p.feed_data(b"\n0\r\n\r\n") + exc = out.exception() + assert isinstance(exc, http_exceptions.TransferEncodingError) + assert "Missing CRLF" in exc.args[0] + + async def test_parse_chunked_payload_missing_CRLF( + self, protocol: BaseProtocol + ) -> None: + out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop()) + p = HttpPayloadParser(out, chunked=True) + with pytest.raises(http_exceptions.TransferEncodingError): + p.feed_data(b"4\r\nasdf\rX0\r\n\r\n") + exc = out.exception() + assert isinstance(exc, http_exceptions.TransferEncodingError) + assert "Missing CRLF" in exc.args[0] + async def test_http_payload_parser_length(self, protocol: BaseProtocol) -> None: out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop()) p = HttpPayloadParser(out, length=2)