From d126a7654af03df4ec26ab1be4614279d6c1fec5 Mon Sep 17 00:00:00 2001
From: Seth Michael Larson <sethmichaellarson@gmail.com>
Date: Wed, 12 Feb 2020 20:03:54 -0600
Subject: [PATCH 1/4] Raise ValueError if method contains control characters

---
 urllib3/connection.py                    | 13 +++++++++++++
 test/with_dummyserver/test_connectionpool.py |  6 ++++++
 test/with_dummyserver/test_poolmanager.py    |  6 ++++++
 3 files changed, 25 insertions(+)

diff --git a/src/urllib3/connection.py b/src/urllib3/connection.py
index 71e6790b1..c315078b4 100644
--- a/urllib3/connection.py
+++ b/urllib3/connection.py
@@ -1,4 +1,5 @@
 from __future__ import absolute_import
+import re
 import datetime
 import logging
 import os
@@ -61,6 +62,8 @@ class ConnectionError(Exception):
 # less than 6 months ago.
 RECENT_DATE = datetime.date(2019, 1, 1)
 
+_CONTAINS_CONTROL_CHAR_RE = re.compile("[\x00-\x20\x7f]")
+
 
 class DummyConnection(object):
     """Used to detect a failed ConnectionCls import."""
@@ -166,6 +169,16 @@ def connect(self):
         conn = self._new_conn()
         self._prepare_conn(conn)
 
+    def putrequest(self, method, url, *args, **kwargs):
+        match = _CONTAINS_CONTROL_CHAR_RE.search(method)
+        if match:
+            raise ValueError(
+                "Method cannot contain control characters. %r (found at least %r)"
+                % (method, match.group())
+            )
+
+        return _HTTPConnection.putrequest(self, method, url, *args, **kwargs)
+
     def request_chunked(self, method, url, body=None, headers=None):
         """
         Alternative to the common request method, which sends the
diff --git a/test/with_dummyserver/test_connectionpool.py b/test/with_dummyserver/test_connectionpool.py
index 57f0dbd2f..79cbd2718 100644
--- a/test/with_dummyserver/test_connectionpool.py
+++ b/test/with_dummyserver/test_connectionpool.py
@@ -656,6 +656,11 @@ def test_dns_error(self):
         pool = HTTPConnectionPool('thishostdoesnotexist.invalid', self.port, timeout=0.001)
         self.assertRaises(MaxRetryError, pool.request, 'GET', '/test', retries=2)
 
+    def test_invalid_method_not_allowed(self):
+        for char in [" ", "\r", "\n", "\x00"]:
+            pool = HTTPConnectionPool(self.host, self.port)
+            self.assertRaises(ValueError, pool.request, 'GET' + char, '/')
+
     def test_source_address(self):
         for addr, is_ipv6 in VALID_SOURCE_ADDRESSES:
             if is_ipv6 and not HAS_IPV6_AND_DNS:

From 842d80008087045257cbf5df7e12c23d104159c4 Mon Sep 17 00:00:00 2001
From: Seth Michael Larson <sethmichaellarson@gmail.com>
Date: Thu, 13 Feb 2020 14:14:11 -0600
Subject: [PATCH 3/4] Restrict all non-tokens

---
 src/urllib3/connection.py                 | 4 ++--
 2 files changed, 2 insertions(+), 8 deletions(-)

diff --git a/urllib3/connection.py b/urllib3/connection.py
index c315078b4..019f6e437 100644
--- a/urllib3/connection.py
+++ b/urllib3/connection.py
@@ -62,7 +62,7 @@ class ConnectionError(Exception):
 # less than 6 months ago.
 RECENT_DATE = datetime.date(2019, 1, 1)
 
-_CONTAINS_CONTROL_CHAR_RE = re.compile("[\x00-\x20\x7f]")
+_CONTAINS_CONTROL_CHAR_RE = re.compile(r"[^-!#$%&'*+.^_`|~0-9a-zA-Z]")
 
 
 class DummyConnection(object):
@@ -173,7 +173,7 @@ def putrequest(self, method, url, *args, **kwargs):
         match = _CONTAINS_CONTROL_CHAR_RE.search(method)
         if match:
             raise ValueError(
-                "Method cannot contain control characters. %r (found at least %r)"
+                "Method cannot contain non-token characters %r (found at least %r)"
                 % (method, match.group())
             )
 

From 695f832bb1aec4c5453530b6c25e57a3bb0c05a9 Mon Sep 17 00:00:00 2001
From: Seth Michael Larson <sethmichaellarson@gmail.com>
Date: Sun, 16 Feb 2020 21:31:40 -0600
Subject: [PATCH 4/4] Add a simple docstring to putrequest()

---
 src/urllib3/connection.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/urllib3/connection.py b/urllib3/connection.py
index 019f6e437..f7b176093 100644
--- a/urllib3/connection.py
+++ b/urllib3/connection.py
@@ -170,6 +170,7 @@ def connect(self):
         self._prepare_conn(conn)
 
     def putrequest(self, method, url, *args, **kwargs):
+        """Send a request to the server"""
         match = _CONTAINS_CONTROL_CHAR_RE.search(method)
         if match:
             raise ValueError(
