Skip to content

Commit bc17da4

Browse files
committed
Parse integers strictly in the HTTP protocol.
This is a follow-up to 0437fbc, covering all similar cases (except in the legacy implementation) and simplifying checks for invalid values.
1 parent 7f56717 commit bc17da4

2 files changed

Lines changed: 19 additions & 18 deletions

File tree

‎src/websockets/http11.py‎

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,18 @@ def d(value: bytes | bytearray) -> str:
8080

8181
_value_re = re.compile(rb"[\x09\x20-\x7e\x80-\xff]*")
8282

83-
# Content-Length = 1*DIGIT, applied after decoding the header value.
83+
# Regexes for validating status codes and numeric header values.
84+
# Set a lower limit than default_max_str_digits; 1 EB is plenty.
8485

86+
# Content-Length = 1*DIGIT
8587
# See https://datatracker.ietf.org/doc/html/rfc9110#section-8.6
8688

87-
_content_length_re = re.compile(r"[0-9]+")
89+
_digits_re = re.compile(rb"[0-9]{1,18}")
90+
91+
# chunk-size = 1*HEXDIG
92+
# See https://datatracker.ietf.org/doc/html/rfc9112#section-7.1
93+
94+
_hexdigits_re = re.compile(rb"[0-9a-fA-F]{1,15}")
8895

8996

9097
@dataclasses.dataclass
@@ -304,12 +311,11 @@ def parse(
304311
raise ValueError(
305312
f"unsupported protocol; expected HTTP/1.1: {d(status_line)}"
306313
)
307-
try:
308-
status_code = int(raw_status_code)
309-
except ValueError: # invalid literal for int() with base 10
314+
if not _digits_re.fullmatch(raw_status_code):
310315
raise ValueError(
311316
f"invalid status code; expected integer; got {d(raw_status_code)}"
312-
) from None
317+
)
318+
status_code = int(raw_status_code)
313319
if not 100 <= status_code < 600:
314320
raise ValueError(
315321
f"invalid status code; expected 100–599; got {d(raw_status_code)}"
@@ -468,10 +474,8 @@ def read_body(
468474
while True:
469475
chunk_size_line = yield from parse_line(read_line, SecurityError)
470476
raw_chunk_size = chunk_size_line.split(b";", 1)[0]
471-
# Set a lower limit than default_max_str_digits; 1 EB is plenty.
472-
if len(raw_chunk_size) > 15:
473-
str_chunk_size = raw_chunk_size.decode(errors="backslashreplace")
474-
raise SecurityError(f"chunk too large: 0x{str_chunk_size} bytes")
477+
if not _hexdigits_re.fullmatch(raw_chunk_size):
478+
raise SecurityError(f"invalid chunk size: {d(raw_chunk_size)}")
475479
chunk_size = int(raw_chunk_size, 16)
476480
if chunk_size == 0:
477481
break
@@ -487,11 +491,8 @@ def read_body(
487491
return body
488492

489493
elif (raw_content_length := headers.get("Content-Length")) is not None:
490-
if not _content_length_re.fullmatch(raw_content_length):
491-
raise ValueError(f"invalid content length: {raw_content_length}")
492-
# Set a lower limit than default_max_str_digits; 1 EiB is plenty.
493-
if len(raw_content_length) > 18:
494-
raise SecurityError(f"body too large: {raw_content_length} bytes")
494+
if not _digits_re.fullmatch(raw_content_length.encode("iso-8859-1")):
495+
raise SecurityError(f"invalid content length: {raw_content_length}")
495496
content_length = int(raw_content_length)
496497
if content_length > MAX_BODY_SIZE:
497498
raise SecurityError(f"body too large: {content_length} bytes")

‎tests/test_http11.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -311,12 +311,12 @@ def test_parse_body_with_content_length_and_body_way_too_large(self):
311311
next(self.parse())
312312
self.assertEqual(
313313
str(raised.exception),
314-
"body too large: 1234567890123456789 bytes",
314+
"invalid content length: 1234567890123456789",
315315
)
316316

317317
def test_parse_body_with_negative_content_length(self):
318318
self.reader.feed_data(b"HTTP/1.1 200 OK\r\nContent-Length: -1\r\n\r\n")
319-
with self.assertRaises(ValueError) as raised:
319+
with self.assertRaises(SecurityError) as raised:
320320
next(self.parse())
321321
self.assertEqual(
322322
str(raised.exception),
@@ -364,7 +364,7 @@ def test_parse_body_with_chunked_transfer_encoding_and_chunk_way_too_large(self)
364364
next(self.parse())
365365
self.assertEqual(
366366
str(raised.exception),
367-
"chunk too large: 0x1234567890ABCDEF bytes",
367+
"invalid chunk size: 1234567890ABCDEF",
368368
)
369369

370370
def test_parse_body_with_unsupported_transfer_encoding(self):

0 commit comments

Comments
 (0)