diff --git a/CHANGES/13415.bugfix.rst b/CHANGES/13415.bugfix.rst new file mode 100644 index 00000000000..f31b9a63e65 --- /dev/null +++ b/CHANGES/13415.bugfix.rst @@ -0,0 +1 @@ +Fixed an integer overflow on too large messages -- by :user:`Dreamsorcerer`. diff --git a/aiohttp/_websocket/reader_c.pxd b/aiohttp/_websocket/reader_c.pxd index 94c8f18ef2d..33587831d0c 100644 --- a/aiohttp/_websocket/reader_c.pxd +++ b/aiohttp/_websocket/reader_c.pxd @@ -114,6 +114,7 @@ cdef class WebSocketReader: has_mask=bint, fin=bint, had_fragments=Py_ssize_t, + partial_len=Py_ssize_t, payload_bytearray=bytearray, ) cpdef void _feed_data(self, bytes data) except * diff --git a/aiohttp/_websocket/reader_py.py b/aiohttp/_websocket/reader_py.py index 6160206a137..40dda00d039 100644 --- a/aiohttp/_websocket/reader_py.py +++ b/aiohttp/_websocket/reader_py.py @@ -466,11 +466,14 @@ def _feed_data(self, data: bytes) -> None: OP_CODE_BINARY, OP_CODE_CONTINUATION, }: - projected_size = self._payload_bytes_to_read + len(self._partial) - if projected_size >= self._max_msg_size: + # partial_len declared in reader_c.pxd to keep it in C. + partial_len = len(self._partial) + # payload_bytes_to_read is a signed 64-bit C value, + # use subtraction here to avoid an integer overflow. + if self._payload_bytes_to_read >= self._max_msg_size - partial_len: raise WebSocketError( WSCloseCode.MESSAGE_TOO_BIG, - f"Message size {projected_size} " + f"Message size {int(self._payload_bytes_to_read) + partial_len} " f"exceeds limit {self._max_msg_size}", ) diff --git a/tests/test_websocket_parser.py b/tests/test_websocket_parser.py index 9e3d08a1649..a2438d461e2 100644 --- a/tests/test_websocket_parser.py +++ b/tests/test_websocket_parser.py @@ -828,6 +828,33 @@ def test_msg_too_large_text_after_non_fin_text(out: WebSocketDataQueue) -> None: assert ctx.value.code == WSCloseCode.MESSAGE_TOO_BIG +def test_msg_too_large_declared_length_near_ssize_t_max( + out: WebSocketDataQueue, +) -> None: + # Regression test: the header-time size check compares + # `payload_bytes_to_read + len(partial)` against max_msg_size. In the + # compiled Cython reader payload_bytes_to_read is a signed 64-bit C + # value, and RFC 6455 allows a declared length up to 2**63-1, so a + # naive addition can wrap around to a negative number and bypass the + # limit entirely once anything is already buffered in `partial`. + max_msg_size = 4 * 1024 * 1024 + parser = WebSocketReader(out, max_msg_size, compress=False, decode_text=True) + + # Buffer one byte in `partial` via a non-fin fragment. + first = build_frame(b"a", WSMsgType.TEXT, is_fin=False) + parser._feed_data(first) + + # Continuation header alone declares the maximum length a signed + # 64-bit payload_bytes_to_read can hold: 2**63-1. + header = PACK_LEN3(WSMsgType.CONTINUATION, 127, 2**63 - 1) + with pytest.raises( + WebSocketError, + match=rf"^Message size {2**63} exceeds limit {max_msg_size}$", + ) as ctx: + parser._feed_data(header) + assert ctx.value.code == WSCloseCode.MESSAGE_TOO_BIG + + @pytest.mark.parametrize( "opcode", (0x3, 0x4, 0x5, 0x6, 0x7, 0xB, 0xC, 0xD, 0xE, 0xF),