From 5a050bf5c71716b6191266939654ff617a67387c Mon Sep 17 00:00:00 2001 From: arshiya tabasum Date: Mon, 24 Aug 2026 12:35:48 +0530 Subject: [PATCH 1/7] reject reserved close code 1006 in websocket reader --- CHANGES/13536.bugfix.rst | 7 +++++++ aiohttp/_websocket/reader_py.py | 8 +++++++- tests/test_websocket_parser.py | 13 +++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) 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..1b0acbe4c8a --- /dev/null +++ b/CHANGES/13536.bugfix.rst @@ -0,0 +1,7 @@ +Fixed the WebSocket reader accepting close code ``1006`` in a Close frame +received from the peer -- by :user:`arshsmith1`. + +``1006`` (``ABNORMAL_CLOSURE``), like ``1005`` and ``1015``, is reserved by +:rfc:`6455#section-7.4.1` and must not be sent as a status code on the wire; +a Close frame carrying it is now rejected as a protocol error instead of being +delivered as a valid close code. diff --git a/aiohttp/_websocket/reader_py.py b/aiohttp/_websocket/reader_py.py index a295db0a4a7..d80cc08aea6 100644 --- a/aiohttp/_websocket/reader_py.py +++ b/aiohttp/_websocket/reader_py.py @@ -24,7 +24,13 @@ WSMsgType, ) -ALLOWED_CLOSE_CODES: set[int] = {int(i) for i in WSCloseCode} +# WSCloseCode.ABNORMAL_CLOSURE (1006) is reported locally when a connection +# drops without a Close frame; RFC 6455 7.4.1 reserves it (along with 1005 and +# 1015) and forbids it on the wire, so a Close frame that carries it must be +# rejected rather than accepted as a valid code. +ALLOWED_CLOSE_CODES: set[int] = {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/test_websocket_parser.py b/tests/test_websocket_parser.py index 2cd383cdb77..57874e65b1a 100644 --- a/tests/test_websocket_parser.py +++ b/tests/test_websocket_parser.py @@ -325,6 +325,19 @@ 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: + # RFC 6455 7.4.1 reserves 1004, 1005, 1006 and 1015 and forbids them as a + # status code in a Close frame on the wire. 1006 is a WSCloseCode member + # (aiohttp reports 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") From f2e87e3ccafceb6b6ab6c9af13388e2947b0bd2b Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Fri, 28 Aug 2026 17:27:50 +0100 Subject: [PATCH 2/7] Apply batched suggestions from code review Co-authored-by: Sam Bull --- CHANGES/13536.bugfix.rst | 5 ----- aiohttp/_websocket/reader_py.py | 10 +++------- tests/test_websocket_parser.py | 5 +++-- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/CHANGES/13536.bugfix.rst b/CHANGES/13536.bugfix.rst index 1b0acbe4c8a..13ffdc75661 100644 --- a/CHANGES/13536.bugfix.rst +++ b/CHANGES/13536.bugfix.rst @@ -1,7 +1,2 @@ Fixed the WebSocket reader accepting close code ``1006`` in a Close frame received from the peer -- by :user:`arshsmith1`. - -``1006`` (``ABNORMAL_CLOSURE``), like ``1005`` and ``1015``, is reserved by -:rfc:`6455#section-7.4.1` and must not be sent as a status code on the wire; -a Close frame carrying it is now rejected as a protocol error instead of being -delivered as a valid close code. diff --git a/aiohttp/_websocket/reader_py.py b/aiohttp/_websocket/reader_py.py index d80cc08aea6..c374023017b 100644 --- a/aiohttp/_websocket/reader_py.py +++ b/aiohttp/_websocket/reader_py.py @@ -24,13 +24,9 @@ WSMsgType, ) -# WSCloseCode.ABNORMAL_CLOSURE (1006) is reported locally when a connection -# drops without a Close frame; RFC 6455 7.4.1 reserves it (along with 1005 and -# 1015) and forbids it on the wire, so a Close frame that carries it must be -# rejected rather than accepted as a valid code. -ALLOWED_CLOSE_CODES: set[int] = {int(i) for i in WSCloseCode} - { - int(WSCloseCode.ABNORMAL_CLOSURE) -} +# 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/test_websocket_parser.py b/tests/test_websocket_parser.py index 57874e65b1a..11a13d96133 100644 --- a/tests/test_websocket_parser.py +++ b/tests/test_websocket_parser.py @@ -327,9 +327,10 @@ def test_close_frame_invalid_code_above_range( @pytest.mark.parametrize("code", (1004, 1005, 1006, 1015)) def test_close_frame_reserved_code(parser: PatchableWebSocketReader, code: int) -> None: - # RFC 6455 7.4.1 reserves 1004, 1005, 1006 and 1015 and forbids them as a + # 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 reports it locally), so it must still be rejected on receipt. + # (aiohttp uses it locally), so it must still be rejected on receipt. data = build_close_frame(code=code) with pytest.raises(WebSocketError) as ctx: From d0de9ff895d9b26c3e98690cdf5bccc43175d957 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:28:31 +0000 Subject: [PATCH 3/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- aiohttp/_websocket/reader_py.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aiohttp/_websocket/reader_py.py b/aiohttp/_websocket/reader_py.py index 26ee08e02c8..569be86f3fe 100644 --- a/aiohttp/_websocket/reader_py.py +++ b/aiohttp/_websocket/reader_py.py @@ -28,7 +28,9 @@ # 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)} +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 From 38a6079c507f236535c4b26007f8d8df5312d22d Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Fri, 28 Aug 2026 17:29:54 +0100 Subject: [PATCH 4/7] Apply batched suggestions from code review Co-authored-by: Sam Bull --- aiohttp/_websocket/reader_py.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aiohttp/_websocket/reader_py.py b/aiohttp/_websocket/reader_py.py index 569be86f3fe..ddb7b8d1a6d 100644 --- a/aiohttp/_websocket/reader_py.py +++ b/aiohttp/_websocket/reader_py.py @@ -2,6 +2,7 @@ import asyncio import builtins +from typing import Final import sys import weakref from collections import deque @@ -28,7 +29,7 @@ # 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} - { +ALLOWED_CLOSE_CODES: Final = {int(i) for i in WSCloseCode} - { int(WSCloseCode.ABNORMAL_CLOSURE) } From b0d16d480dda8b2a5c79837824dc3512ec2d57e5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:30:36 +0000 Subject: [PATCH 5/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- aiohttp/_websocket/reader_py.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiohttp/_websocket/reader_py.py b/aiohttp/_websocket/reader_py.py index ddb7b8d1a6d..5336cc659b4 100644 --- a/aiohttp/_websocket/reader_py.py +++ b/aiohttp/_websocket/reader_py.py @@ -2,10 +2,10 @@ import asyncio import builtins -from typing import Final import sys import weakref from collections import deque +from typing import Final from ..base_protocol import BaseProtocol from ..compression_utils import TooManyMembersError, ZLibDecompressor From c9f9ad1c4430d91d6a81def32b419385c63ab176 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Fri, 28 Aug 2026 22:56:01 +0100 Subject: [PATCH 6/7] Apply batched suggestions from code review Co-authored-by: Sam Bull --- aiohttp/_websocket/reader_py.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/aiohttp/_websocket/reader_py.py b/aiohttp/_websocket/reader_py.py index 5336cc659b4..569be86f3fe 100644 --- a/aiohttp/_websocket/reader_py.py +++ b/aiohttp/_websocket/reader_py.py @@ -5,7 +5,6 @@ import sys import weakref from collections import deque -from typing import Final from ..base_protocol import BaseProtocol from ..compression_utils import TooManyMembersError, ZLibDecompressor @@ -29,7 +28,7 @@ # 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: Final = {int(i) for i in WSCloseCode} - { +ALLOWED_CLOSE_CODES = {int(i) for i in WSCloseCode} - { int(WSCloseCode.ABNORMAL_CLOSURE) } From 2213687c53f77eebcc735664484a6ed309cd82c3 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Fri, 28 Aug 2026 23:36:42 +0100 Subject: [PATCH 7/7] Update test_autobahn.py --- tests/autobahn/test_autobahn.py | 2 -- 1 file changed, 2 deletions(-) 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.",