From 2b486a7fce4ce6ca3275cd0a9fa8aa3900aec945 Mon Sep 17 00:00:00 2001 From: connabyss Date: Sun, 1 Nov 2026 03:39:04 +0000 Subject: [PATCH 1/2] Test duplicate Host headers that the mismatch check misses 292a408 rejects a request header block with more than one Host header (GHSA-6hr6-w5qg-qmwg), and 92b925e added a test case for it. That case puts the mismatching Host last, so the older :authority/Host mismatch check already rejects it: the test still passes with the new check taken out. Add two cases only the duplicate check catches: the matching Host header last, and two Host headers with no :authority. Both are accepted without the fix. Assisted-by: Claude Code --- tests/test_invalid_headers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_invalid_headers.py b/tests/test_invalid_headers.py index 2a68c6fc6..87358cdd1 100644 --- a/tests/test_invalid_headers.py +++ b/tests/test_invalid_headers.py @@ -44,6 +44,9 @@ class TestInvalidFrameSequences: [*base_request_headers, ("upgrade", "super-protocol/1.1")], [*base_request_headers, ("te", "chunked")], [*base_request_headers, ("host", "example.com"), ("host", "evil.example.com")], + [*base_request_headers, ("host", "evil.example.com"), ("host", "example.com")], + [*[header for header in base_request_headers if header[0] != ":authority"], + ("host", "example.com"), ("host", "evil.example.com")], [*base_request_headers, ("host", "notexample.com")], [*base_request_headers, (" name", "name with leading space")], [*base_request_headers, ("name ", "name with trailing space")], From b7b20fae2bfebe75f1f756e40dc2a15df22a4af9 Mon Sep 17 00:00:00 2001 From: connabyss Date: Sun, 1 Nov 2026 21:57:28 +0000 Subject: [PATCH 2/2] Assert the duplicate Host error message in the test Move the duplicate Host header cases into their own list and add a test that checks the ProtocolError says "Request header block has multiple Host headers", so it fails if any other header check rejects these blocks instead. The cases stay in the general invalid header list too. Assisted-by: Claude Code --- tests/test_invalid_headers.py | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/tests/test_invalid_headers.py b/tests/test_invalid_headers.py index 87358cdd1..3433e90a2 100644 --- a/tests/test_invalid_headers.py +++ b/tests/test_invalid_headers.py @@ -33,6 +33,12 @@ class TestInvalidFrameSequences: (":method", "GET"), ("user-agent", "someua/0.0.1"), ] + duplicate_host_header_blocks = [ + [*base_request_headers, ("host", "example.com"), ("host", "evil.example.com")], + [*base_request_headers, ("host", "evil.example.com"), ("host", "example.com")], + [*[header for header in base_request_headers if header[0] != ":authority"], + ("host", "example.com"), ("host", "evil.example.com")], + ] base_invalid_header_blocks = [ [*base_request_headers, ("Uppercase", "name")], [*base_request_headers, (":late", "pseudo-header")], @@ -43,10 +49,7 @@ class TestInvalidFrameSequences: [*base_request_headers, ("transfer-encoding", "gzip")], [*base_request_headers, ("upgrade", "super-protocol/1.1")], [*base_request_headers, ("te", "chunked")], - [*base_request_headers, ("host", "example.com"), ("host", "evil.example.com")], - [*base_request_headers, ("host", "evil.example.com"), ("host", "example.com")], - [*[header for header in base_request_headers if header[0] != ":authority"], - ("host", "example.com"), ("host", "evil.example.com")], + *duplicate_host_header_blocks, [*base_request_headers, ("host", "notexample.com")], [*base_request_headers, (" name", "name with leading space")], [*base_request_headers, ("name ", "name with trailing space")], @@ -91,6 +94,29 @@ def test_headers_event(self, frame_factory, headers) -> None: ) assert c.data_to_send() == expected_frame.serialize() + @pytest.mark.parametrize("headers", duplicate_host_header_blocks + [ + h2.utilities.utf8_encode_headers(headers) for headers in duplicate_host_header_blocks + ]) + def test_duplicate_host_headers_rejected(self, frame_factory, headers) -> None: + """ + A request header block with more than one Host header is rejected + by the duplicate Host check, not by any other header check. + """ + c = h2.connection.H2Connection(config=self.server_config) + c.receive_data(frame_factory.preamble()) + c.clear_outbound_data_buffer() + + f = frame_factory.build_headers_frame(headers) + data = f.serialize() + + with pytest.raises(h2.exceptions.ProtocolError, match="Request header block has multiple Host headers"): + c.receive_data(data) + + expected_frame = frame_factory.build_goaway_frame( + last_stream_id=1, error_code=h2.errors.ErrorCodes.PROTOCOL_ERROR, + ) + assert c.data_to_send() == expected_frame.serialize() + @pytest.mark.parametrize("headers", invalid_header_blocks) def test_push_promise_event(self, frame_factory, headers) -> None: """