Reject empty header names on the outbound path - #1325
Conversation
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.
| Raises a ProtocolError if we try to send a header block with an empty | ||
| header name. | ||
| """ | ||
| return _validate_nonempty_header_names(headers, "Sent header name with zero length.") |
There was a problem hiding this comment.
IMO there is no need to separate just on the error message here. Please unify this again and follow similar message styles as already used in the other cases, e.g., "header name with zero length present".
There was a problem hiding this comment.
Merged back into a single generator, used on both paths.
Message is now Header name with zero length present. I dropped the Received prefix rather than keeping your sentence verbatim, because in this file Received ... marks the inbound-only checks, and this one now runs in both directions. The ... present. form follows _reject_connection_header, whose message is Connection-specific header field present: ... — the closest neighbour, being the other both-directions check with a direction-neutral message.
test_inbound_header_name_length_full_frame_decode matched the old string verbatim, so it is updated to the new one in the same commit.
Use a single generator for both directions, with a direction-neutral message matching the style of the other checks that run on both paths. Update the inbound test to the new wording.
Sending a header block with an empty header name raises
IndexErrorinstead ofProtocolError.The inbound pipeline runs
_reject_empty_header_names(utilities.py:201) before_reject_pseudo_header_fields(utilities.py:210), so an empty name is caught before this lookup:validate_outbound_headershas no such guard, soheader[0][0]indexes into emptybytes:Precedent
#1257 reported the same class of failure on the inbound path —
IndexErrorfrom an empty header name — and the response there was:#1257
The fix at the time added
_reject_empty_header_names, but only tovalidate_headers. The outbound path was left as it was, so the sameIndexErroris still reachable, now from_reject_pseudo_header_fieldsrather than_reject_surrounding_whitespace.The change
Adds the guard to
validate_outbound_headers, in the same position relative to_reject_pseudo_header_fieldsas inbound.The message needed to differ by direction — "Received header name with zero length." is wrong for a block we are sending, and an existing test matches on that exact string. Rather than parameterise the generator, I shared the body and kept two thin wrappers, which is the shape this module already uses for direction-specific checks:
_check_host_authority_headerand_check_sent_host_authority_headerboth delegate to_validate_host_authority_headerand differ only in name and docstring. So_reject_empty_header_namesand_reject_sent_empty_header_namesnow both delegate to_validate_nonempty_header_names(headers, msg).Tests
Added to
tests/test_invalid_headers.py: empty name outbound raisesProtocolErroracross the validation-flag combinations, the same viasend_headerswith the message asserted, and a valid name still accepted in both directions.Against unmodified
utilities.pythe new tests fail with exactly the reported error:With the fix:
Full suite and the lint commands from
[tool.tox.env.lint]:Separate question, not addressed here
While tracing this I noticed the two pipelines have diverged more broadly. Four character classes that inbound rejects are accepted outbound:
ProtocolErrorProtocolErrorProtocolErrorProtocolErrorVerified through
send_headers; the bytes reach the wire, e.g.[(b"x", b"a\r\nInjected: evil")]. Uppercase names and surrounding whitespace are handled outbound, but bynormalize_outbound_headersrewriting them rather than rejecting.RFC 9113 § 8.2.1 lists these among the conditions where "A request or response that contains a field that violates any of these conditions MUST be treated as malformed", and
config.py:109-114documents the flag as:#289 established the principle when it first added outbound validation: "We already have the code to police these rules, so apply it to outbound as well as inbound headers."
I have deliberately left that out of this PR. It is a larger change than a wrong exception type, and it could break callers who send such headers today — that seems like your call rather than something to fold into a bugfix. Happy to open a follow-up if you want it, in whatever shape you prefer.