Skip to content

Validate header names on the outbound path - #1326

Open
avalyset wants to merge 1 commit into
python-hyper:masterfrom
avalyset:fix/outbound-illegal-header-names
Open

avalyset wants to merge 1 commit into
python-hyper:masterfrom
avalyset:fix/outbound-illegal-header-names

Conversation

@avalyset

Copy link
Copy Markdown

Follow-up to #1325, as discussed there in #1325 (comment).

Cause

_reject_illegal_characters is called from validate_headers but not from validate_outbound_headers. Once #1325 lands it is the only check in the inbound chain with no outbound counterpart.

It could not simply be added to the outbound chain, because it carries two value rules in addition to the four name classes of RFC 9113 section 8.2.1.

Change

_reject_illegal_characters is split into _reject_illegal_name_characters (uppercase 0x41-0x5a, <= 0x20, >= 0x7f, colon after position 0) and _reject_illegal_value_characters (NUL/LF/CR, surrounding SP/HTAB). validate_headers calls both in the previous order, so inbound behaviour is unchanged. validate_outbound_headers calls the name half only, first in the chain, mirroring the inbound order.

The value rules deliberately stay inbound-only: _strip_surrounding_whitespace already normalises the whitespace case before outbound validation, so enabling that half would only change behaviour for normalize_outbound_headers=False.

The uppercase message is made direction-neutral (Uppercase header name present: ...) because the check now runs both ways; this also changes the inbound message. Received header value surrounded by whitespace is left alone, as that rule is still inbound-only.

Measured

Through send_headers, decoding what reaches the wire:

name before, normalize=True before, normalize=False after, normalize=True after, normalize=False
X-Foo (class 1) sent as x-foo sent as X-Foo sent as x-foo ProtocolError
foo bar (class 2) sent sent ProtocolError ProtocolError
foo\x7f (class 3) sent sent ProtocolError ProtocolError
foo:bar (class 4) sent sent ProtocolError ProtocolError

src/h2/utilities.py is +21/-5. Seven of the eleven added tests fail against the unmodified file; the other four are regression guards that pass either way. Full suite 1673 passed, ruff check src/ and mypy --strict-bytes clean.

Note

This touches validate_outbound_headers in utilities.py, the same function as #1325. Whichever lands second, I will rebase it.

🤖 Generated with Claude Code

Split _reject_illegal_characters into a name half and a value half, and
run the name half from validate_outbound_headers as well.

The four name classes from RFC 9113 section 8.2.1 were checked on the
inbound path only. Classes 2, 3 and 4 went out on the wire untouched;
class 1 was normalised away by _lowercase_header_names with defaults,
but not with normalize_outbound_headers=False.

The value rules (NUL/LF/CR and surrounding whitespace) stay inbound-only:
_strip_surrounding_whitespace already normalises the whitespace case on
the outbound path.

"Received uppercase header name" is now direction-neutral, since the
check runs both ways.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant