diff --git a/CHANGES/12895.bugfix.rst b/CHANGES/12895.bugfix.rst new file mode 100644 index 00000000000..af206e7f443 --- /dev/null +++ b/CHANGES/12895.bugfix.rst @@ -0,0 +1,4 @@ +Rejected WebSocket ``CLOSE`` frames carrying a status code above the +valid range (greater than ``4999``) with a protocol error, matching the +existing handling of out-of-range low codes, per :rfc:`6455#section-7.4.1` +-- by :user:`dxbjavid`. diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 88875d58397..66015395713 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -123,6 +123,7 @@ Dmitry Trofimov Dmytro Bohomiakov Dmytro Kuznetsov Dustin J. Mitchell +dxbjavid Earle Lowe Eduard Iskandarov Eli Ribble diff --git a/aiohttp/_websocket/reader_py.py b/aiohttp/_websocket/reader_py.py index 82b5b65af60..9fd1e334896 100644 --- a/aiohttp/_websocket/reader_py.py +++ b/aiohttp/_websocket/reader_py.py @@ -293,7 +293,10 @@ def _handle_frame( payload_len = len(payload) if payload_len >= 2: close_code = UNPACK_CLOSE_CODE(payload[:2])[0] - if close_code < 3000 and close_code not in ALLOWED_CLOSE_CODES: + # https://datatracker.ietf.org/doc/html/rfc6455#section-7.4.2 + if close_code > 4999 or ( + close_code < 3000 and close_code not in ALLOWED_CLOSE_CODES + ): raise WebSocketError( WSCloseCode.PROTOCOL_ERROR, f"Invalid close code: {close_code}", diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 2dedb6e2f42..c9fee14512e 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -45,7 +45,7 @@ blockbuster==1.5.26 # -r requirements/test-common.in brotli==1.2.0 ; platform_python_implementation == "CPython" and sys_platform != "android" and sys_platform != "ios" # via -r requirements/runtime-deps.in -build==1.5.0 +build==1.5.1 # via pip-tools certifi==2026.6.17 # via requests @@ -118,7 +118,7 @@ jinja2==3.1.6 # sphinx # sphinxcontrib-mermaid # towncrier -librt==0.12.0 +librt==0.13.0 # via mypy markdown-it-py==3.0.0 # via @@ -233,7 +233,7 @@ pytest-xdist==3.8.0 # via -r requirements/test-common.in python-dateutil==2.9.0.post0 # via freezegun -python-discovery==1.4.3 +python-discovery==1.4.4 # via virtualenv python-on-whales==0.81.0 # via diff --git a/requirements/dev.txt b/requirements/dev.txt index ca38b7bab11..49dd89869b4 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -45,7 +45,7 @@ blockbuster==1.5.26 # -r requirements/test-common.in brotli==1.2.0 ; platform_python_implementation == "CPython" and sys_platform != "android" and sys_platform != "ios" # via -r requirements/runtime-deps.in -build==1.5.0 +build==1.5.1 # via pip-tools certifi==2026.6.17 # via requests @@ -116,7 +116,7 @@ jinja2==3.1.6 # sphinx # sphinxcontrib-mermaid # towncrier -librt==0.12.0 +librt==0.13.0 # via mypy markdown-it-py==3.0.0 # via @@ -228,7 +228,7 @@ pytest-xdist==3.8.0 # via -r requirements/test-common.in python-dateutil==2.9.0.post0 # via freezegun -python-discovery==1.4.3 +python-discovery==1.4.4 # via virtualenv python-on-whales==0.81.0 # via diff --git a/requirements/lint.txt b/requirements/lint.txt index 5efdfbf7c37..63507975fb1 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -60,7 +60,7 @@ iniconfig==2.3.0 # via pytest isal==1.8.0 # via -r requirements/lint.in -librt==0.12.0 +librt==0.13.0 # via mypy markdown-it-py==4.2.0 # via rich @@ -123,7 +123,7 @@ pytest-mock==3.15.1 # via -r requirements/lint.in python-dateutil==2.9.0.post0 # via freezegun -python-discovery==1.4.3 +python-discovery==1.4.4 # via virtualenv python-on-whales==0.81.0 # via -r requirements/lint.in diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 9bf331379d8..d90f13720e2 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -50,7 +50,7 @@ iniconfig==2.3.0 # via pytest isal==1.8.0 ; python_version < "3.14" and implementation_name == "cpython" # via -r requirements/test-common.in -librt==0.12.0 +librt==0.13.0 # via mypy markdown-it-py==4.2.0 # via rich diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 280b9266273..e820e63109d 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -67,7 +67,7 @@ iniconfig==2.3.0 # via pytest isal==1.8.0 ; python_version < "3.14" and implementation_name == "cpython" # via -r requirements/test-common.in -librt==0.12.0 +librt==0.13.0 # via mypy markdown-it-py==4.2.0 # via rich diff --git a/requirements/test.txt b/requirements/test.txt index 30722b73010..e1c3cce7aca 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -67,7 +67,7 @@ iniconfig==2.3.0 # via pytest isal==1.8.0 ; python_version < "3.14" and implementation_name == "cpython" # via -r requirements/test-common.in -librt==0.12.0 +librt==0.13.0 # via mypy markdown-it-py==4.2.0 # via rich diff --git a/tests/test_websocket_parser.py b/tests/test_websocket_parser.py index b703232fd71..a89a2210f31 100644 --- a/tests/test_websocket_parser.py +++ b/tests/test_websocket_parser.py @@ -287,9 +287,9 @@ def test_close_frame(out: WebSocketDataQueue, parser: PatchableWebSocketReader) def test_close_frame_info( out: WebSocketDataQueue, parser: PatchableWebSocketReader ) -> None: - parser._handle_frame(True, WSMsgType.CLOSE, b"0112345", 0) + parser._handle_frame(True, WSMsgType.CLOSE, b"\x03\xe912345", 0) res = out._buffer[0] - assert res == WSMessageClose(data=12337, size=7, extra="12345") + assert res == WSMessageClose(data=1001, size=7, extra="12345") def test_close_frame_invalid( @@ -311,6 +311,18 @@ def test_close_frame_invalid_2( assert ctx.value.code == WSCloseCode.PROTOCOL_ERROR +@pytest.mark.parametrize("code", (5000, 9999, 65535)) +def test_close_frame_invalid_code_above_range( + parser: PatchableWebSocketReader, code: int +) -> None: + 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")