Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGES/10596.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fixed server hanging indefinitely when chunked transfer encoding chunk-size
does not match actual data length. The server now raises
``TransferEncodingError`` instead of waiting forever for data that will
never arrive -- by :user:`Fridayai700`.
2 changes: 1 addition & 1 deletion aiohttp/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@
TOKEN = CHAR ^ CTL ^ SEPARATORS


json_re = re.compile(r"(?:application/|[\w.-]+/[\w.+-]+?\+)json$", re.IGNORECASE)
json_re = re.compile(r"^(?:application/|[\w.-]+/[\w.+-]+?\+)json$", re.IGNORECASE)


class BasicAuth(namedtuple("BasicAuth", ["login", "password", "encoding"])):
Expand Down
6 changes: 6 additions & 0 deletions aiohttp/http_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -889,6 +889,12 @@ def feed_data(
if chunk[: len(SEP)] == SEP:
chunk = chunk[len(SEP) :]
self._chunk = ChunkState.PARSE_CHUNKED_SIZE
elif len(chunk) >= len(SEP) or chunk != SEP[: len(chunk)]:
exc = TransferEncodingError(
"Chunk size mismatch: expected CRLF after chunk data"
)
set_exception(self.payload, exc)
raise exc
else:
self._chunk_tail = chunk
return False, b""
Expand Down
4 changes: 4 additions & 0 deletions aiohttp/web_urldispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,10 @@ def __iter__(self) -> Iterator[AbstractRoute]:

async def _handle(self, request: Request) -> StreamResponse:
filename = request.match_info["filename"]
if Path(filename).is_absolute():
# filename is an absolute path e.g. //network/share or D:\path
# which could be a UNC path leading to NTLM credential theft
raise HTTPNotFound()
unresolved_path = self._directory.joinpath(filename)
loop = asyncio.get_running_loop()
return await loop.run_in_executor(
Expand Down
30 changes: 30 additions & 0 deletions tests/test_http_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -1747,6 +1747,36 @@ async def test_parse_chunked_payload_size_error(
p.feed_data(b"blah\r\n")
assert isinstance(out.exception(), http_exceptions.TransferEncodingError)

async def test_parse_chunked_payload_size_data_mismatch(
self, protocol: BaseProtocol
) -> None:
"""Chunk-size does not match actual data: should raise, not hang.

Regression test for #10596.
"""
out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
p = HttpPayloadParser(out, chunked=True, headers_parser=HeadersParser())
# Declared chunk-size is 4 but actual data is "Hello" (5 bytes).
# After consuming 4 bytes, remaining starts with "o" not "\r\n".
with pytest.raises(http_exceptions.TransferEncodingError):
p.feed_data(b"4\r\nHello\r\n0\r\n\r\n")
assert isinstance(out.exception(), http_exceptions.TransferEncodingError)

async def test_parse_chunked_payload_size_data_mismatch_too_short(
self, protocol: BaseProtocol
) -> None:
"""Chunk-size larger than data: declared 6 but only 5 bytes before CRLF.

Regression test for #10596.
"""
out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
p = HttpPayloadParser(out, chunked=True, headers_parser=HeadersParser())
# Declared chunk-size is 6 but actual data before CRLF is "Hello" (5 bytes).
# Parser reads 6 bytes: "Hello\r", then expects \r\n but sees "\n0\r\n..."
with pytest.raises(http_exceptions.TransferEncodingError):
p.feed_data(b"6\r\nHello\r\n0\r\n\r\n")
assert isinstance(out.exception(), http_exceptions.TransferEncodingError)

async def test_parse_chunked_payload_split_end(
self, protocol: BaseProtocol
) -> None:
Expand Down
Loading