From 25ffb323efc56c6ea805feb1441cb0505c790fb5 Mon Sep 17 00:00:00 2001 From: JinWang An Date: Mon, 26 Apr 2021 17:00:45 +0900 Subject: [PATCH] [CVE-2019-18348]Disallow control characters in hostnames in http.client An issue was discovered in urllib2 in Python 2.x through 2.7.17 and urllib in Python 3.x through 3.8.0. CRLF injection is possible if the attacker controls a url parameter, as demonstrated by the first argument to urllib.request.urlopen with \r\n (specifically in the host component of a URL) followed by an HTTP header. This is similar to the CVE-2019-9740 query string issue and the CVE-2019-9947 path string issue. (This is not exploitable when glibc has CVE-2016-10739 fixed.). This is fixed in: v2.7.18, v2.7.18rc1; v3.5.10, v3.5.10rc1; v3.6.11, v3.6.11rc1, v3.6.12; v3.7.8, v3.7.8rc1, v3.7.9; v3.8.3, v3.8.3rc1, v3.8.4, v3.8.4rc1, v3.8.5, v3.8.6, v3.8.6rc1. Change-Id: I9f29bdc023cd909dd7cc5250f1faba0940d591db Signed-off-by: JinWang An --- Lib/http/client.py | 11 +++++++++++ Lib/test/test_httplib.py | 11 +++++++++++ Lib/test/test_urllib.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/Lib/http/client.py b/Lib/http/client.py index 09ffa751..9ba54942 100644 --- a/Lib/http/client.py +++ b/Lib/http/client.py @@ -844,6 +844,8 @@ 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 @@ -1189,6 +1191,15 @@ class HTTPConnection: # For HTTP/1.0, the server will assume "not chunked" pass + 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: + raise InvalidURL(f"URL can't contain control characters. {host!r} " + f"(found at least {match.group()!r})") + + 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 c731ddb7..cff0ae0f 100644 --- a/Lib/test/test_httplib.py +++ b/Lib/test/test_httplib.py @@ -1176,6 +1176,17 @@ class BasicTest(TestCase): thread.join() self.assertEqual(result, b"proxied data\n") + 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) + class ExtendedReadTest(TestCase): """ Test peek(), read1(), readline() diff --git a/Lib/test/test_urllib.py b/Lib/test/test_urllib.py index 2ac73b58..6d5b022c 100644 --- a/Lib/test/test_urllib.py +++ b/Lib/test/test_urllib.py @@ -465,6 +465,38 @@ Connection: close 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 = f"//localhost{char}/test/" + 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, f"contain control.*{escaped_char_repr}"): + urlopen(f"http:{schemeless_url}") + with self.assertRaisesRegex(InvalidURL, f"contain control.*{escaped_char_repr}"): + urlopen(f"https:{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(f"http:{schemeless_url}") + with self.assertRaisesRegex(InvalidURL, r"contain control.*\\n"): + urlopen(f"https:{schemeless_url}") + finally: + self.unfakehttp() + def test_URLopener_deprecation(self): with support.check_warnings(('',DeprecationWarning)): urllib.request.URLopener() -- 2.34.1