Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions cheroot/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,13 @@
QUOTED_SLASH = b'%2F'
QUOTED_SLASH_REGEX = re.compile(b''.join((b'(?i)', QUOTED_SLASH)))

# ``Content-Length`` is defined as ``1*DIGIT`` by
# https://datatracker.ietf.org/doc/html/rfc9110#section-8.6. ``int()``
# additionally tolerates surrounding whitespace, a leading sign and
# digit-separating underscores (e.g. ``1_0`` -> ``10``), none of which are
# valid, so the raw value has to be validated before it is parsed.
NUMERIC_REGEX = re.compile(rb'[0-9]+')


_STOPPING_FOR_INTERRUPT = Exception() # sentinel used during shutdown

Expand Down Expand Up @@ -1008,14 +1015,17 @@ def read_request_headers(self): # noqa: C901 # FIXME

mrbs = self.server.max_request_body_size

try:
cl = int(self.inheaders.get(b'Content-Length', 0))
except ValueError:
raw_content_length = self.inheaders.get(b'Content-Length', b'0')
# ``int()`` silently accepts values that are not valid ``1*DIGIT``
# strings (e.g. ``b'1_0'``, ``b'+10'`` or surrounding whitespace), so
# the raw value has to be checked against the grammar first.
if NUMERIC_REGEX.fullmatch(raw_content_length) is None:
self.simple_response(
'400 Bad Request',
'Malformed Content-Length Header.',
)
return False
cl = int(raw_content_length)

if mrbs and cl > mrbs:
self.simple_response(
Expand Down
17 changes: 14 additions & 3 deletions cheroot/test/test_conn.py
Original file line number Diff line number Diff line change
Expand Up @@ -1437,13 +1437,24 @@ def test_Content_Length_in(test_client):
conn.close()


def test_Content_Length_not_int(test_client):
"""Test that malicious Content-Length header returns 400."""
@pytest.mark.parametrize(
'content_length_value',
(
'not-an-integer',
# `int()` accepts these, but they are not valid `1*DIGIT` values
# per :rfc:`9110#section-8.6`, so they must be rejected (#738).
'1_0', # a digit-separating underscore
'+10', # a leading sign
'0x10', # a hexadecimal literal
),
)
def test_Content_Length_not_int(test_client, content_length_value):
"""Test that a malformed Content-Length header returns 400."""
status_line, _actual_headers, actual_resp_body = test_client.post(
'/upload',
headers=[
('Content-Type', 'text/plain'),
('Content-Length', 'not-an-integer'),
('Content-Length', content_length_value),
],
)
actual_status = int(status_line[:3])
Expand Down
6 changes: 6 additions & 0 deletions docs/changelog-fragments.d/738.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Started rejecting ``Content-Length`` request header values that are not
valid ``1*DIGIT`` strings with a ``400 Bad Request`` response. Previously,
values such as ``1_0`` (a digit-separating underscore) or ``+10`` (a leading
sign) were silently accepted by :py:func:`int` and parsed as ``10``.

-- by :user:`apoorvdarshan`
Loading