Skip to content

Commit a49f8eb

Browse files
committed
fix: reject conflicting content-length headers
1 parent dc5da5c commit a49f8eb

2 files changed

Lines changed: 88 additions & 4 deletions

File tree

src/h2/stream.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,15 +1364,24 @@ def _initialize_content_length(self, headers: Iterable[Header]) -> None:
13641364
self._expected_content_length = 0
13651365
return
13661366

1367+
content_lengths = []
1368+
13671369
for n, v in headers:
13681370
if n == b"content-length":
13691371
try:
1370-
self._expected_content_length = int(v, 10)
1372+
content_lengths.append(int(v, 10))
13711373
except ValueError as err:
13721374
msg = f"Invalid content-length header: {v!r}"
13731375
raise ProtocolError(msg) from err
13741376

1375-
return
1377+
if not content_lengths:
1378+
return
1379+
1380+
if len(set(content_lengths)) != 1:
1381+
msg = "Conflicting content-length headers"
1382+
raise ProtocolError(msg)
1383+
1384+
self._expected_content_length = content_lengths[0]
13761385

13771386
def _track_content_length(self, length: int, end_stream: bool) -> None:
13781387
"""

tests/test_invalid_content_lengths.py

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,24 @@ class TestInvalidContentLengths:
1919
peer is not valid.
2020
"""
2121

22-
example_request_headers = [
22+
example_request_headers_without_content_length = [
2323
(":authority", "example.com"),
2424
(":path", "/"),
2525
(":scheme", "https"),
2626
(":method", "POST"),
27+
]
28+
example_request_headers = [
29+
*example_request_headers_without_content_length,
2730
("content-length", "15"),
2831
]
29-
example_request_headers_bytes = [
32+
example_request_headers_bytes_without_content_length = [
3033
(b":authority", b"example.com"),
3134
(b":path", b"/"),
3235
(b":scheme", b"https"),
3336
(b":method", b"POST"),
37+
]
38+
example_request_headers_bytes = [
39+
*example_request_headers_bytes_without_content_length,
3440
(b"content-length", b"15"),
3541
]
3642
example_response_headers = [
@@ -39,6 +45,75 @@ class TestInvalidContentLengths:
3945
]
4046
server_config = h2.config.H2Configuration(client_side=False)
4147

48+
@pytest.mark.parametrize(
49+
"request_headers",
50+
[
51+
example_request_headers_without_content_length,
52+
example_request_headers_bytes_without_content_length,
53+
],
54+
)
55+
def test_duplicate_matching_content_lengths(self, frame_factory, request_headers) -> None:
56+
"""
57+
Remote peers sending duplicate matching content-length fields are
58+
accepted.
59+
"""
60+
c = h2.connection.H2Connection(config=self.server_config)
61+
c.initiate_connection()
62+
c.receive_data(frame_factory.preamble())
63+
c.clear_outbound_data_buffer()
64+
65+
headers = frame_factory.build_headers_frame(
66+
headers=[
67+
*request_headers,
68+
("content-length", "15"),
69+
("content-length", "15"),
70+
],
71+
)
72+
data = frame_factory.build_data_frame(
73+
data=b"\x01"*15,
74+
flags=["END_STREAM"],
75+
)
76+
77+
events = c.receive_data(headers.serialize() + data.serialize())
78+
79+
assert isinstance(events[0], h2.events.RequestReceived)
80+
assert isinstance(events[1], h2.events.DataReceived)
81+
assert isinstance(events[2], h2.events.StreamEnded)
82+
assert c.data_to_send() == b""
83+
84+
@pytest.mark.parametrize(
85+
"request_headers",
86+
[
87+
example_request_headers_without_content_length,
88+
example_request_headers_bytes_without_content_length,
89+
],
90+
)
91+
def test_duplicate_conflicting_content_lengths(self, frame_factory, request_headers) -> None:
92+
"""
93+
Remote peers sending duplicate conflicting content-length fields cause
94+
Protocol Errors.
95+
"""
96+
c = h2.connection.H2Connection(config=self.server_config)
97+
c.initiate_connection()
98+
c.receive_data(frame_factory.preamble())
99+
c.clear_outbound_data_buffer()
100+
101+
headers = frame_factory.build_headers_frame(
102+
headers=[
103+
*request_headers,
104+
("content-length", "15"),
105+
("content-length", "16"),
106+
],
107+
)
108+
with pytest.raises(h2.exceptions.ProtocolError):
109+
c.receive_data(headers.serialize())
110+
111+
expected_frame = frame_factory.build_goaway_frame(
112+
last_stream_id=1,
113+
error_code=h2.errors.ErrorCodes.PROTOCOL_ERROR,
114+
)
115+
assert c.data_to_send() == expected_frame.serialize()
116+
42117
@pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes])
43118
def test_too_much_data(self, frame_factory, request_headers) -> None:
44119
"""

0 commit comments

Comments
 (0)