Repository: benoitc/gunicorn

Issue:
Improper parsing of Content-Length
## Description
gunicorn currently parses Content-Length using `int`. This causes problems because `int` accepts a lot more than should be acceptable in that content. The biggest problem characters are, '-', '+', and '_'.

# Examples
## HTTP requests in which gunicorn misinterprets content-length (assume all newlines are CRLF):

### Underscore between digits
```
GET / HTTP/1.1
Connection: close
Host: whatever
Content-Length: 0_1


```

### Plus sign prefix
```
GET / HTTP/1.1
Connection: close
Host: whatever
Content-Length: +1


```

### Minus sign prefix (only works for 0)
```
GET / HTTP/1.1
Connection: close
Host: whatever
Content-Length: -0


```

# Solution
The set of things you want to accept in a Content-Length header is not equal to the set of things that can get through `int` without error. During Content-Length parsing, check that the value is all ASCII digits.

# Versions
CPython version: 3.10.11
Gunicorn version: 20.1.0


Discussion:

Resolving patch:
diff --git a/gunicorn/http/message.py b/gunicorn/http/message.py
--- a/gunicorn/http/message.py
+++ b/gunicorn/http/message.py
@@ -139,7 +139,10 @@ def set_body_reader(self):
             self.body = Body(ChunkedReader(self, self.unreader))
         elif content_length is not None:
             try:
-                content_length = int(content_length)
+                if str(content_length).isnumeric():
+                    content_length = int(content_length)
+                else:
+                    raise InvalidHeader("CONTENT-LENGTH", req=self)
             except ValueError:
                 raise InvalidHeader("CONTENT-LENGTH", req=self)
 


Test patch:
diff --git a/tests/requests/invalid/022.http b/tests/requests/invalid/022.http
new file mode 100644
--- /dev/null
+++ b/tests/requests/invalid/022.http
@@ -0,0 +1,3 @@
+GET /first HTTP/1.0\r\n
+Content-Length: -0\r\n
+\r\n
\ No newline at end of file
diff --git a/tests/requests/invalid/022.py b/tests/requests/invalid/022.py
new file mode 100644
--- /dev/null
+++ b/tests/requests/invalid/022.py
@@ -0,0 +1,5 @@
+from gunicorn.config import Config
+from gunicorn.http.errors import InvalidHeader
+
+cfg = Config()
+request = InvalidHeader
diff --git a/tests/requests/invalid/023.http b/tests/requests/invalid/023.http
new file mode 100644
--- /dev/null
+++ b/tests/requests/invalid/023.http
@@ -0,0 +1,3 @@
+GET /first HTTP/1.0\r\n
+Content-Length: 0_1\r\n
+\r\n
\ No newline at end of file
diff --git a/tests/requests/invalid/023.py b/tests/requests/invalid/023.py
new file mode 100644
--- /dev/null
+++ b/tests/requests/invalid/023.py
@@ -0,0 +1,5 @@
+from gunicorn.config import Config
+from gunicorn.http.errors import InvalidHeader
+
+cfg = Config()
+request = InvalidHeader
diff --git a/tests/requests/invalid/024.http b/tests/requests/invalid/024.http
new file mode 100644
--- /dev/null
+++ b/tests/requests/invalid/024.http
@@ -0,0 +1,3 @@
+GET /first HTTP/1.0\r\n
+Content-Length: +1\r\n
+\r\n
\ No newline at end of file
diff --git a/tests/requests/invalid/024.py b/tests/requests/invalid/024.py
new file mode 100644
--- /dev/null
+++ b/tests/requests/invalid/024.py
@@ -0,0 +1,5 @@
+from gunicorn.config import Config
+from gunicorn.http.errors import InvalidHeader
+
+cfg = Config()
+request = InvalidHeader