Skip to content

Commit aba9ad6

Browse files
authored
fix: reject conflicting content-length headers (#1317)
1 parent 1cd9ce0 commit aba9ad6

3 files changed

Lines changed: 92 additions & 4 deletions

File tree

CHANGELOG.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ dev
99
- Support for Python 3.9 has been removed.
1010
- Support for PyPy 3.9 has been removed.
1111
- `Stream.end_stream()` now raises `NoSuchStreamError` or `StreamClosedError` exceptions, instead of a generic `KeyError`.
12+
- Duplicate ``content-length`` headers with different values now raise ``ProtocolError``.
13+
Previously, the first ``content-length`` header was accepted and later conflicting values were ignored.
1214
- **backfill from v4.3.0** Convert emitted events into Python `dataclass`, which introduces new constructors with required arguments.
1315
Instantiating these events without arguments, as previously commonly used API pattern, will no longer work.
1416

src/h2/stream.py

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

1367+
content_length = None
1368+
13671369
for n, v in headers:
13681370
if n == b"content-length":
13691371
try:
1370-
self._expected_content_length = int(v, 10)
1372+
parsed_content_length = 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 content_length is None:
1378+
content_length = parsed_content_length
1379+
elif parsed_content_length != content_length:
1380+
msg = f"Conflicting content-length headers: {content_length} and {parsed_content_length}"
1381+
raise ProtocolError(msg)
1382+
1383+
self._expected_content_length = content_length
13761384

13771385
def _track_content_length(self, length: int, end_stream: bool) -> None:
13781386
"""

tests/test_invalid_content_lengths.py

Lines changed: 80 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,78 @@ 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(
109+
h2.exceptions.ProtocolError,
110+
match="Conflicting content-length headers: 15 and 16",
111+
):
112+
c.receive_data(headers.serialize())
113+
114+
expected_frame = frame_factory.build_goaway_frame(
115+
last_stream_id=1,
116+
error_code=h2.errors.ErrorCodes.PROTOCOL_ERROR,
117+
)
118+
assert c.data_to_send() == expected_frame.serialize()
119+
42120
@pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes])
43121
def test_too_much_data(self, frame_factory, request_headers) -> None:
44122
"""

0 commit comments

Comments
 (0)