From 20acdf440b0a39c00054d4a70ea59376b793c83d Mon Sep 17 00:00:00 2001 From: arshsmith Date: Sat, 29 Aug 2026 04:28:28 +0530 Subject: [PATCH] reject reserved close code 1006 in websocket reader (#13536) --- CHANGES/13536.bugfix.rst | 2 ++ aiohttp/_websocket/reader_py.py | 6 +++++- tests/autobahn/test_autobahn.py | 2 -- tests/test_websocket_parser.py | 14 ++++++++++++++ 4 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 CHANGES/13536.bugfix.rst diff --git a/CHANGES/13536.bugfix.rst b/CHANGES/13536.bugfix.rst new file mode 100644 index 00000000000..13ffdc75661 --- /dev/null +++ b/CHANGES/13536.bugfix.rst @@ -0,0 +1,2 @@ +Fixed the WebSocket reader accepting close code ``1006`` in a Close frame +received from the peer -- by :user:`arshsmith1`. diff --git a/aiohttp/_websocket/reader_py.py b/aiohttp/_websocket/reader_py.py index 12cd1b8616f..569be86f3fe 100644 --- a/aiohttp/_websocket/reader_py.py +++ b/aiohttp/_websocket/reader_py.py @@ -26,7 +26,11 @@ WSMsgType, ) -ALLOWED_CLOSE_CODES: set[int] = {int(i) for i in WSCloseCode} +# ABNORMAL_CLOSURE is used internally, should never be accepted from a client. +# https://datatracker.ietf.org/doc/html/rfc6455#section-7.4.1 +ALLOWED_CLOSE_CODES = {int(i) for i in WSCloseCode} - { + int(WSCloseCode.ABNORMAL_CLOSURE) +} # States for the reader, used to parse the WebSocket frame # integer values are used so they can be cythonized diff --git a/tests/autobahn/test_autobahn.py b/tests/autobahn/test_autobahn.py index deb35341b48..4488282117f 100644 --- a/tests/autobahn/test_autobahn.py +++ b/tests/autobahn/test_autobahn.py @@ -124,7 +124,6 @@ def test_client(report_dir: Path, request: pytest.FixtureRequest) -> None: results = get_test_results(report_dir / "clients", "aiohttp") xfail = { - "7.9.5": "The close code should have been 1002 or empty", "9.1.4": "Did not receive message within 100 seconds.", "9.1.5": "Did not receive message within 100 seconds.", "9.1.6": "Did not receive message within 100 seconds.", @@ -194,7 +193,6 @@ def test_server(report_dir: Path, request: pytest.FixtureRequest) -> None: results = get_test_results(report_dir / "servers", "AutobahnServer") xfail = { - "7.9.5": "The close code should have been 1002 or empty", "9.1.4": "Did not receive message within 100 seconds.", "9.1.5": "Did not receive message within 100 seconds.", "9.1.6": "Did not receive message within 100 seconds.", diff --git a/tests/test_websocket_parser.py b/tests/test_websocket_parser.py index 4b3684ba864..a4942bc0460 100644 --- a/tests/test_websocket_parser.py +++ b/tests/test_websocket_parser.py @@ -330,6 +330,20 @@ def test_close_frame_invalid_code_above_range( assert ctx.value.code == WSCloseCode.PROTOCOL_ERROR +@pytest.mark.parametrize("code", (1004, 1005, 1006, 1015)) +def test_close_frame_reserved_code(parser: PatchableWebSocketReader, code: int) -> None: + # https://datatracker.ietf.org/doc/html/rfc6455#section-7.4.1 + # 1004, 1005, 1006 and 1015 are resreved and forbidden as a + # status code in a Close frame on the wire. 1006 is a WSCloseCode member + # (aiohttp uses it locally), so it must still be rejected on receipt. + data = build_close_frame(code=code) + + with pytest.raises(WebSocketError) as ctx: + parser._feed_data(data) + + assert ctx.value.code == WSCloseCode.PROTOCOL_ERROR + + def test_close_frame_unicode_err(parser: PatchableWebSocketReader) -> None: data = build_close_frame(code=1000, message=b"\xf4\x90\x80\x80")