Skip to content

Commit 4aa3d90

Browse files
committed
Reject empty header names on the outbound path
The inbound pipeline runs _reject_empty_header_names before _reject_pseudo_header_fields, so an empty name is caught before the `header[0][0]` lookup. The outbound pipeline has no such guard, so sending a header block with an empty name raises IndexError from utilities.py:335 instead of ProtocolError. Add the guard to validate_outbound_headers in the same position. The message differs by direction, so the body is shared by _validate_nonempty_header_names, mirroring how _check_host_authority_header and _check_sent_host_authority_header already share _validate_host_authority_header.
1 parent bc239af commit 4aa3d90

2 files changed

Lines changed: 56 additions & 7 deletions

File tree

‎src/h2/utilities.py‎

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -265,21 +265,36 @@ def _reject_illegal_characters(headers: Iterable[Header],
265265
yield header
266266

267267

268-
def _reject_empty_header_names(headers: Iterable[Header],
269-
hdr_validation_flags: HeaderValidationFlags) -> Generator[Header, None, None]:
268+
def _validate_nonempty_header_names(headers: Iterable[Header], msg: str) -> Generator[Header, None, None]:
270269
"""
271-
Raises a ProtocolError if any header names are empty (length 0).
272-
While hpack decodes such headers without errors, they are semantically
273-
forbidden in HTTP, see RFC 7230, stating that they must be at least one
274-
character long.
270+
Raises a ProtocolError with the given message if any header name is empty
271+
(length 0). While hpack decodes such headers without errors, they are
272+
semantically forbidden in HTTP, see RFC 7230, stating that they must be at
273+
least one character long.
275274
"""
276275
for header in headers:
277276
if len(header[0]) == 0:
278-
msg = "Received header name with zero length."
279277
raise ProtocolError(msg)
280278
yield header
281279

282280

281+
def _reject_empty_header_names(headers: Iterable[Header],
282+
hdr_validation_flags: HeaderValidationFlags) -> Generator[Header, None, None]:
283+
"""
284+
Raises a ProtocolError if a header block arrives with an empty header name.
285+
"""
286+
return _validate_nonempty_header_names(headers, "Received header name with zero length.")
287+
288+
289+
def _reject_sent_empty_header_names(headers: Iterable[Header],
290+
hdr_validation_flags: HeaderValidationFlags) -> Generator[Header, None, None]:
291+
"""
292+
Raises a ProtocolError if we try to send a header block with an empty
293+
header name.
294+
"""
295+
return _validate_nonempty_header_names(headers, "Sent header name with zero length.")
296+
297+
283298
def _reject_te(headers: Iterable[Header], hdr_validation_flags: HeaderValidationFlags) -> Generator[Header, None, None]:
284299
"""
285300
Raises a ProtocolError if the TE header is present in a header block and
@@ -690,6 +705,9 @@ def validate_outbound_headers(headers: Iterable[Header],
690705
:param headers: The HTTP header set.
691706
:param hdr_validation_flags: An instance of HeaderValidationFlags.
692707
"""
708+
headers = _reject_sent_empty_header_names(
709+
headers, hdr_validation_flags,
710+
)
693711
headers = _reject_te(
694712
headers, hdr_validation_flags,
695713
)

‎tests/test_invalid_headers.py‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,37 @@ def test_inbound_header_name_length(self, hdr_validation_flags) -> None:
672672
with pytest.raises(h2.exceptions.ProtocolError):
673673
list(h2.utilities.validate_headers([(b"", b"foobar")], hdr_validation_flags))
674674

675+
@pytest.mark.parametrize("hdr_validation_flags", hdr_validation_combos)
676+
def test_outbound_header_name_length(self, hdr_validation_flags) -> None:
677+
# An empty outbound header name must raise ProtocolError, not IndexError
678+
# from the `header[0][0]` lookup in _reject_pseudo_header_fields.
679+
with pytest.raises(h2.exceptions.ProtocolError):
680+
list(h2.utilities.validate_outbound_headers([(b"", b"foobar")], hdr_validation_flags))
681+
682+
def test_outbound_header_name_length_send_headers(self, frame_factory) -> None:
683+
c = h2.connection.H2Connection()
684+
c.initiate_connection()
685+
c.clear_outbound_data_buffer()
686+
687+
headers = [
688+
(b":authority", b"example.com"),
689+
(b":path", b"/"),
690+
(b":scheme", b"https"),
691+
(b":method", b"GET"),
692+
(b"", b"foobar"),
693+
]
694+
with pytest.raises(h2.exceptions.ProtocolError, match=r"Sent header name with zero length\."):
695+
c.send_headers(1, headers)
696+
697+
@pytest.mark.parametrize("hdr_validation_flags", [
698+
flags for flags in hdr_validation_combos
699+
if flags.is_trailer and not flags.is_response_header
700+
])
701+
def test_valid_header_name_accepted_both_directions(self, hdr_validation_flags) -> None:
702+
headers = [(b"x-custom-header", b"foobar")]
703+
assert list(h2.utilities.validate_headers(list(headers), hdr_validation_flags))
704+
assert list(h2.utilities.validate_outbound_headers(list(headers), hdr_validation_flags))
705+
675706
def test_inbound_header_name_length_full_frame_decode(self, frame_factory) -> None:
676707
f = frame_factory.build_headers_frame([])
677708
f.data = b"\x00\x00\x01\x04"

0 commit comments

Comments
 (0)