Skip to content

Commit 883ed37

Browse files
committed
reject header names and values containing unpermitted characters \r, \n, or \0x00
1 parent 0583911 commit 883ed37

File tree

3 files changed

+33
-2
lines changed

3 files changed

+33
-2
lines changed

CHANGELOG.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ dev
66

77
**API Changes (Backward Incompatible)**
88

9-
-
9+
- Reject header names and values containing unpermitted characters `\r`, `\n`, or `\0x00`.
1010

1111
**API Changes (Backward Compatible)**
1212

src/h2/utilities.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@
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+
2733

2834
# A set of headers that are hop-by-hop or connection-specific and thus
2935
# forbidden in HTTP/2. This list comes from RFC 7540 § 8.1.2.2.
@@ -201,6 +207,9 @@ def validate_headers(headers: Iterable[Header], hdr_validation_flags: HeaderVali
201207
# For example, we avoid tuple unpacking in loops because it represents a
202208
# fixed cost that we don't want to spend, instead indexing into the header
203209
# tuples.
210+
headers = _reject_unpermitted_characters(
211+
headers, hdr_validation_flags,
212+
)
204213
headers = _reject_empty_header_names(
205214
headers, hdr_validation_flags,
206215
)
@@ -225,6 +234,22 @@ def validate_headers(headers: Iterable[Header], hdr_validation_flags: HeaderVali
225234
return _check_path_header(headers, hdr_validation_flags)
226235

227236

237+
def _reject_unpermitted_characters(headers: Iterable[Header],
238+
hdr_validation_flags: HeaderValidationFlags) -> Generator[Header, None, None]:
239+
"""
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.
242+
"""
243+
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}"
247+
raise ProtocolError(msg)
248+
if c in header[1]:
249+
msg = f"Unpermitted character '{c}' in header value: {header[1]!r}"
250+
raise ProtocolError(msg)
251+
yield header
252+
228253

229254
def _reject_empty_header_names(headers: Iterable[Header],
230255
hdr_validation_flags: HeaderValidationFlags) -> Generator[Header, None, None]:

tests/test_invalid_headers.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ 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")],
5157
[header for header in base_request_headers
5258
if header[0] != ":authority"],
5359
[(":protocol", "websocket"), *base_request_headers],
@@ -665,7 +671,7 @@ def test_inbound_header_name_length(self, hdr_validation_flags) -> None:
665671

666672
def test_inbound_header_name_length_full_frame_decode(self, frame_factory) -> None:
667673
f = frame_factory.build_headers_frame([])
668-
f.data = b"\x00\x00\x05\x00\x00\x00\x00\x04"
674+
f.data = b"\x00\x00\x01\x04"
669675
data = f.serialize()
670676

671677
c = h2.connection.H2Connection(config=h2.config.H2Configuration(client_side=False))

0 commit comments

Comments
 (0)