diff options
author | Tapas Kundu <39723251+tapakund@users.noreply.github.com> | 2020-06-20 06:43:50 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-06-20 06:43:50 (GMT) |
commit | 09d8172837b6985c4ad90ee025f6b5a554a9f0ac (patch) | |
tree | b5a619ae3cc0060123aba6b08f6b3aaad62bd5b9 | |
parent | 37fe316479e0b6906a74b0c0a5e495c55037fdfd (diff) | |
download | cpython-09d8172837b6985c4ad90ee025f6b5a554a9f0ac.zip cpython-09d8172837b6985c4ad90ee025f6b5a554a9f0ac.tar.gz cpython-09d8172837b6985c4ad90ee025f6b5a554a9f0ac.tar.bz2 |
[3.5] closes bpo-38576: Disallow control characters in hostnames in http.client. (#19300)
Add host validation for control characters for more
CVE-2019-18348 protection.
(cherry picked from commit 83fc70159b24)
-rw-r--r-- | Lib/http/client.py | 12 | ||||
-rw-r--r-- | Lib/test/test_httplib.py | 13 | ||||
-rw-r--r-- | Lib/test/test_urllib.py | 36 | ||||
-rw-r--r-- | Misc/NEWS.d/next/Security/2020-03-14-14-57-44.bpo-38576.OowwQn.rst | 1 |
4 files changed, 59 insertions, 3 deletions
diff --git a/Lib/http/client.py b/Lib/http/client.py index 85dc802..5d5d7a7 100644 --- a/Lib/http/client.py +++ b/Lib/http/client.py @@ -771,6 +771,7 @@ class HTTPConnection: (self.host, self.port) = self._get_hostport(host, port) + self._validate_host(self.host) # This is stored as an instance variable to allow unit # tests to replace it with a suitable mockup self._create_connection = socket.create_connection @@ -1085,6 +1086,17 @@ class HTTPConnection: ).format(matched=match.group(), **locals()) raise InvalidURL(msg) + def _validate_host(self, host): + """Validate a host so it doesn't contain control characters.""" + # Prevent CVE-2019-18348. + match = _contains_disallowed_url_pchar_re.search(host) + if match: + msg = ( + "URL can't contain control characters. {host!r} " + "(found at least {matched!r})" + ).format(matched=match.group(), host=host) + raise InvalidURL(msg) + def putheader(self, header, *values): """Send a request header line to the server. diff --git a/Lib/test/test_httplib.py b/Lib/test/test_httplib.py index c12a429..a8e7a30 100644 --- a/Lib/test/test_httplib.py +++ b/Lib/test/test_httplib.py @@ -986,7 +986,7 @@ class BasicTest(TestCase): thread.join() self.assertEqual(result, b"proxied data\n") - def test_putrequest_override_validation(self): + def test_putrequest_override_domain_validation(self): """ It should be possible to override the default validation behavior in putrequest (bpo-38216). @@ -999,6 +999,17 @@ class BasicTest(TestCase): conn.sock = FakeSocket('') conn.putrequest('GET', '/\x00') + def test_putrequest_override_host_validation(self): + class UnsafeHTTPConnection(client.HTTPConnection): + def _validate_host(self, url): + pass + + conn = UnsafeHTTPConnection('example.com\r\n') + conn.sock = FakeSocket('') + # set skip_host so a ValueError is not raised upon adding the + # invalid URL as the value of the "Host:" header + conn.putrequest('GET', '/', skip_host=1) + def test_putrequest_override_encoding(self): """ It should be possible to override the default encoding diff --git a/Lib/test/test_urllib.py b/Lib/test/test_urllib.py index 1e2c622..d1074ad 100644 --- a/Lib/test/test_urllib.py +++ b/Lib/test/test_urllib.py @@ -331,7 +331,7 @@ class urlopen_HttpTests(unittest.TestCase, FakeHTTPMixin, FakeFTPMixin): self.unfakehttp() @unittest.skipUnless(ssl, "ssl module required") - def test_url_with_control_char_rejected(self): + def test_url_path_with_control_char_rejected(self): for char_no in list(range(0, 0x21)) + [0x7f]: char = chr(char_no) schemeless_url = "//localhost:7777/test{}/".format(char) @@ -360,7 +360,7 @@ class urlopen_HttpTests(unittest.TestCase, FakeHTTPMixin, FakeFTPMixin): self.unfakehttp() @unittest.skipUnless(ssl, "ssl module required") - def test_url_with_newline_header_injection_rejected(self): + def test_url_path_with_newline_header_injection_rejected(self): self.fakehttp(b"HTTP/1.1 200 OK\r\n\r\nHello.") host = "localhost:7777?a=1 HTTP/1.1\r\nX-injected: header\r\nTEST: 123" schemeless_url = "//" + host + ":8080/test/?test=a" @@ -385,6 +385,38 @@ class urlopen_HttpTests(unittest.TestCase, FakeHTTPMixin, FakeFTPMixin): finally: self.unfakehttp() + @unittest.skipUnless(ssl, "ssl module required") + def test_url_host_with_control_char_rejected(self): + for char_no in list(range(0, 0x21)) + [0x7f]: + char = chr(char_no) + schemeless_url = "//localhost{}/test/".format(char) + self.fakehttp(b"HTTP/1.1 200 OK\r\n\r\nHello.") + try: + escaped_char_repr = repr(char).replace('\\', r'\\') + InvalidURL = http.client.InvalidURL + with self.assertRaisesRegex( + InvalidURL, r"contain control.*{}".format(escaped_char_repr)): + urlopen("http:{}".format(schemeless_url)) + with self.assertRaisesRegex(InvalidURL, r"contain control.*{}".format(escaped_char_repr)): + urlopen("http:{}".format(schemeless_url)) + finally: + self.unfakehttp() + + @unittest.skipUnless(ssl, "ssl module required") + def test_url_host_with_newline_header_injection_rejected(self): + self.fakehttp(b"HTTP/1.1 200 OK\r\n\r\nHello.") + host = "localhost\r\nX-injected: header\r\n" + schemeless_url = "//" + host + ":8080/test/?test=a" + try: + InvalidURL = http.client.InvalidURL + with self.assertRaisesRegex( + InvalidURL, r"contain control.*\\r"): + urlopen("http:{}".format(schemeless_url)) + with self.assertRaisesRegex(InvalidURL, r"contain control.*\\n"): + urlopen("http:{}".format(schemeless_url)) + finally: + self.unfakehttp() + def test_read_0_9(self): # "0.9" response accepted (but not "simple responses" without # a status line) diff --git a/Misc/NEWS.d/next/Security/2020-03-14-14-57-44.bpo-38576.OowwQn.rst b/Misc/NEWS.d/next/Security/2020-03-14-14-57-44.bpo-38576.OowwQn.rst new file mode 100644 index 0000000..1d03574 --- /dev/null +++ b/Misc/NEWS.d/next/Security/2020-03-14-14-57-44.bpo-38576.OowwQn.rst @@ -0,0 +1 @@ +Disallow control characters in hostnames in http.client, addressing CVE-2019-18348. Such potentially malicious header injection URLs now cause a InvalidURL to be raised. |