Skip to content

Commit 035e989

Browse files
mhilsKriechi
authored andcommitted
be stricter about which characters to accept for headers
This now adheres to the minimal requirements laid out in RFC 9113. We could consider putting additional restrictions on the header value, but in order to keep breakage at a minimum let's do that in a later release if at all.
1 parent 883ed37 commit 035e989

File tree

2 files changed

+33
-22
lines changed

2 files changed

+33
-22
lines changed

src/h2/utilities.py

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,6 @@
2424
SIGIL = ord(b":")
2525
INFORMATIONAL_START = ord(b"1")
2626

27-
HEADER_UNPERMITTED_CHARACTERS = frozenset([
28-
b"\r",
29-
b"\n",
30-
b"\x00",
31-
])
32-
3327

3428
# A set of headers that are hop-by-hop or connection-specific and thus
3529
# forbidden in HTTP/2. This list comes from RFC 7540 § 8.1.2.2.
@@ -207,7 +201,7 @@ def validate_headers(headers: Iterable[Header], hdr_validation_flags: HeaderVali
207201
# For example, we avoid tuple unpacking in loops because it represents a
208202
# fixed cost that we don't want to spend, instead indexing into the header
209203
# tuples.
210-
headers = _reject_unpermitted_characters(
204+
headers = _reject_illegal_characters(
211205
headers, hdr_validation_flags,
212206
)
213207
headers = _reject_empty_header_names(
@@ -234,20 +228,35 @@ def validate_headers(headers: Iterable[Header], hdr_validation_flags: HeaderVali
234228
return _check_path_header(headers, hdr_validation_flags)
235229

236230

237-
def _reject_unpermitted_characters(headers: Iterable[Header],
238-
hdr_validation_flags: HeaderValidationFlags) -> Generator[Header, None, None]:
231+
def _reject_illegal_characters(headers: Iterable[Header],
232+
hdr_validation_flags: HeaderValidationFlags) -> Generator[Header, None, None]:
239233
"""
240-
Raises a ProtocolError if any header names or values contain unpermitted characters.
241-
See RFC 7540, section 10.3 and 8.1.2.6.
234+
Raises a ProtocolError if any header names or values contain illegal characters.
235+
See RFC 9113, section 8.2.1.
242236
"""
243237
for header in headers:
244-
for c in HEADER_UNPERMITTED_CHARACTERS:
245-
if c in header[0]:
246-
msg = f"Unpermitted character '{c}' in header name: {header[0]!r}"
238+
# > A field name MUST NOT contain characters in the ranges 0x00-0x20, 0x41-0x5a,
239+
# > or 0x7f-0xff (all ranges inclusive).
240+
for c in header[0]:
241+
if c <= 0x20 or 0x41 <= c <= 0x5a or 0x7f <= c:
242+
msg = f"Illegal character '{chr(c)}' in header name: {header[0]!r}"
247243
raise ProtocolError(msg)
248-
if c in header[1]:
249-
msg = f"Unpermitted character '{c}' in header value: {header[1]!r}"
244+
245+
# > With the exception of pseudo-header fields (Section 8.3), which have a name
246+
# > that starts with a single colon, field names MUST NOT include a colon (ASCII
247+
# > COLON, 0x3a).
248+
if header[0].find(b":", 1) != -1:
249+
msg = f"Illegal character ':' in header name: {header[0]!r}"
250+
raise ProtocolError(msg)
251+
252+
# > A field value MUST NOT contain the zero value (ASCII NUL, 0x00), line feed
253+
# > (ASCII LF, 0x0a), or carriage return (ASCII CR, 0x0d) at any position.
254+
for c in header[1]:
255+
if c == 0 or c == 0x0a or c == 0x0d:
256+
msg = f"Illegal character '{chr(c)}' in header value: {header[1]!r}"
250257
raise ProtocolError(msg)
258+
259+
# Surrounding whitespace is enforced in `_reject_surrounding_whitespace`.
251260
yield header
252261

253262

tests/test_invalid_headers.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,14 @@ class TestInvalidFrameSequences:
4848
[*base_request_headers, ("name ", "name with trailing space")],
4949
[*base_request_headers, ("name", " value with leading space")],
5050
[*base_request_headers, ("name", "value with trailing space ")],
51-
[*base_request_headers, ("unpermitted-\r-characters", "value")],
52-
[*base_request_headers, ("unpermitted-\n-characters", "value")],
53-
[*base_request_headers, ("unpermitted-\x00-characters", "value")],
54-
[*base_request_headers, ("unpermitted-characters", "some \r value")],
55-
[*base_request_headers, ("unpermitted-characters", "some \n value")],
56-
[*base_request_headers, ("unpermitted-characters", "some \x00 value")],
51+
[*base_request_headers, ("illegal:characters", "value")],
52+
[*base_request_headers, ("illegal-\r-characters", "value")],
53+
[*base_request_headers, ("illegal-\n-characters", "value")],
54+
[*base_request_headers, ("illegal-\x00-characters", "value")],
55+
[*base_request_headers, ("illegal-\x01-characters", "value")],
56+
[*base_request_headers, ("illegal-characters", "some \r value")],
57+
[*base_request_headers, ("illegal-characters", "some \n value")],
58+
[*base_request_headers, ("illegal-characters", "some \x00 value")],
5759
[header for header in base_request_headers
5860
if header[0] != ":authority"],
5961
[(":protocol", "websocket"), *base_request_headers],

0 commit comments

Comments
 (0)