Repository: python/cpython
Pull request: gh-103204: `http.server` - Enforce that HTTP version numbers must consist only of digits
# gh-103204: Enforce that HTTP version numbers must consist only of digits

Currently, `http.server` accepts requests with version numbers preceded with `'+'` or `'-'`, as well as those with `'_'` between digits. I am not aware of any other HTTP server that accepts such requests. This PR makes it reject those requests.

Fixes #103204.

Problem statement:
`http.server` parses HTTP version numbers too permissively.
# `http.server` parses HTTP version numbers too permissively.
`http.server` accepts request lines with HTTP version numbers that have `'_'`, `'+'`,  and `'-'`.

# Reproduction steps:
(Requires netcat)
```bash
python3 -m http.server --bind 127.0.0.1
printf 'GET / HTTP/-9_9_9.+9_9_9\r\n\r\n' | nc 127.0.0.1 8000
```

# Justification
Here are the `HTTP-version` definitions from each of the three HTTP RFCs:
- RFC 2616:
```
HTTP-Version   = "HTTP" "/" 1*DIGIT "." 1*DIGIT
```
- RFC 7230:
```
HTTP-version  = HTTP-name "/" DIGIT "." DIGIT
HTTP-name     = %x48.54.54.50 ; "HTTP", case-sensitive
```
- RFC 9112:
```
HTTP-version  = HTTP-name "/" DIGIT "." DIGIT
HTTP-name     = %s"HTTP"
```
I understand allowing multiple digits for backwards-compatibility with RFC 2616, but I don't think it makes sense to let the specifics of `int` leak out into the world. We should at least ensure that only digits are permitted in HTTP version numbers.

# My environment
- CPython 3.12.0a6+
- Operating system and architecture: Arch Linux on x86_64


<!-- gh-linked-prs -->
### Linked PRs
* gh-103205
* gh-104438
<!-- /gh-linked-prs -->

Hints from issue discussion:

Commit message: Merge branch 'main' into restrict-http-version-number-parsing

Patch to review:
diff --git a/Lib/http/server.py b/Lib/http/server.py
index a245ffb307860a..c018b5042183db 100644
--- a/Lib/http/server.py
+++ b/Lib/http/server.py
@@ -300,6 +300,9 @@ def parse_request(self):
                 #   - Leading zeros MUST be ignored by recipients.
                 if len(version_number) != 2:
                     raise ValueError
+                for component in version_number:
+                    if not component.isdigit():
+                        raise ValueError
                 version_number = int(version_number[0]), int(version_number[1])
             except (ValueError, IndexError):
                 self.send_error(
diff --git a/Lib/test/test_httpservers.py b/Lib/test/test_httpservers.py
index 0382b5ec448d57..8a09c878e3d4c5 100644
--- a/Lib/test/test_httpservers.py
+++ b/Lib/test/test_httpservers.py
@@ -164,6 +164,13 @@ def test_version_digits(self):
         res = self.con.getresponse()
         self.assertEqual(res.status, HTTPStatus.BAD_REQUEST)
 
+    def test_version_signs_and_underscores(self):
+        self.con._http_vsn_str = 'HTTP/-9_9_9.+9_9_9'
+        self.con.putrequest('GET', '/')
+        self.con.endheaders()
+        res = self.con.getresponse()
+        self.assertEqual(res.status, HTTPStatus.BAD_REQUEST)
+
     def test_version_none_get(self):
         self.con._http_vsn_str = ''
         self.con.putrequest('GET', '/')
diff --git a/Misc/NEWS.d/next/Library/2023-04-02-23-05-22.gh-issue-103204.bbDmu0.rst b/Misc/NEWS.d/next/Library/2023-04-02-23-05-22.gh-issue-103204.bbDmu0.rst
new file mode 100644
index 00000000000000..728c4bb967f4d9
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2023-04-02-23-05-22.gh-issue-103204.bbDmu0.rst
@@ -0,0 +1,2 @@
+Fixes http.server accepting HTTP requests with HTTP version numbers
+preceded by '+', or '-', and/or with digit-separating '_'.


Code review comments:
diff_hunk: @@ -300,6 +300,9 @@ def parse_request(self):
                 #   - Leading zeros MUST be ignored by recipients.
                 if len(version_number) != 2:
                     raise ValueError
+                for component in version_number:
+                    if not component.isdigit():
+                        raise ValueError
path: Lib/http/server.py
text: @user1:
Will this work? I have no Python machine on hands to check myself.

```suggestion
                if any(not component.isdigit() for component in version_number):
                    raise ValueError
```

@author:
lgtm