diff --git a/sanic/http/http1.py b/sanic/http/http1.py index 49a3ca96e7..c6b4662b87 100644 --- a/sanic/http/http1.py +++ b/sanic/http/http1.py @@ -273,9 +273,12 @@ async def http1_request_header(self): # no cov pos -= 2 # One CRLF stays in buffer else: self.request_body = True - self.request_bytes_left = self.request_bytes = int( - headers["content-length"] - ) + try: + self.request_bytes_left = self.request_bytes = ( + self._safe_int(headers["content-length"]) + ) + except Exception: + raise BadRequest("Bad content-length") # Remove header and its trailing CRLF del buf[: pos + 4] @@ -514,7 +517,8 @@ async def read(self) -> Optional[bytes]: # no cov await self._receive_more() try: - size = int(buf[2:pos].split(b";", 1)[0].decode(), 16) + raw = buf[2:pos].split(b";", 1)[0].decode() + size = self._safe_int(raw, 16) except Exception: self.keep_alive = False raise BadRequest("Bad chunked encoding") @@ -600,3 +604,9 @@ def set_header_max_size(cls, *sizes: int): *sizes, cls.HEADER_CEILING, ) + + @staticmethod + def _safe_int(value: str, base: int = 10) -> int: + if "-" in value or "+" in value or "_" in value: + raise ValueError + return int(value, base) diff --git a/tests/test_cookies.py b/tests/test_cookies.py index 18c01a3a0a..63c8de8183 100644 --- a/tests/test_cookies.py +++ b/tests/test_cookies.py @@ -402,7 +402,8 @@ def test_cookie_jar_delete_existing_cookie(): encoded = [cookie.encode("ascii") for cookie in jar.cookies] # deletion cookie contains samesite=Strict as was in original cookie assert encoded == [ - b'foo=""; Path=/; Domain=example.com; Max-Age=0; SameSite=Strict; Secure', + b'foo=""; Path=/; Domain=example.com; Max-Age=0; ' + b"SameSite=Strict; Secure", ] diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index b945eadea0..8d931ba461 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -423,7 +423,7 @@ def test_request_middleware_exception_on_404(app: Sanic): @app.on_request def request_middleware(request): - value = next(counter) + next(counter) raise Exception @app.route("/") diff --git a/tests/test_http.py b/tests/test_http.py index 77e4f6fc3f..4db6f111a3 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -110,3 +110,58 @@ def test_url_encoding(client): assert b"400 Bad Request" in headers assert b"URL may only contain US-ASCII characters." in body + + +@pytest.mark.parametrize( + "content_length", + ( + b"-50", + b"+50", + b"5_0", + b"50.5", + ), +) +def test_invalid_content_length(content_length, client): + body = b"Hello" * 10 + client.send( + b"POST /upload HTTP/1.1\r\n" + + b"content-length: " + + content_length + + b"\r\n\r\n" + + body + + b"\r\n\r\n" + ) + + response = client.recv() + headers, body = response.rsplit(b"\r\n\r\n", 1) + + assert b"400 Bad Request" in headers + assert b"Bad content-length" in body + + +@pytest.mark.parametrize( + "chunk_length", + ( + b"-50", + b"+50", + b"5_0", + b"50.5", + ), +) +def test_invalid_chunk_length(chunk_length, client): + body = b"Hello" * 10 + client.send( + b"POST /upload HTTP/1.1\r\n" + + b"transfer-encoding: chunked\r\n\r\n" + + chunk_length + + b"\r\n" + + body + + b"\r\n" + + b"0\r\n\r\n" + ) + + response = client.recv() + headers, body = response.rsplit(b"\r\n\r\n", 1) + + assert b"400 Bad Request" in headers + assert b"Bad chunked encoding" in body