Skip to content

Commit 80dd66a

Browse files
authored
[3.7] bpo-38216, bpo-36274: Allow subclasses to separately override validation and encoding behavior (GH-16448) (GH-16461)
* bpo-38216: Allow bypassing input validation * bpo-36274: Also allow the URL encoding to be overridden. * bpo-38216, bpo-36274: Add tests demonstrating a hook for overriding validation, test demonstrating override encoding, and a test to capture expectation of the interface for the URL. * Call with skip_host to avoid tripping on the host checking in the URL. * Remove obsolete comment. * Make _prepare_path_encoding its own attr. This makes overriding just that simpler. Also, don't use the := operator to make backporting easier. * Add a news entry. * _prepare_path_encoding -> _encode_prepared_path() * Once again separate the path validation and request encoding, drastically simplifying the behavior. Drop the guarantee that all processing happens in _prepare_path.. (cherry picked from commit 7774d78) Co-authored-by: Jason R. Coombs <[email protected]>
1 parent 6112b91 commit 80dd66a

File tree

3 files changed

+51
-10
lines changed

3 files changed

+51
-10
lines changed

Lib/http/client.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1107,19 +1107,15 @@ def putrequest(self, method, url, skip_host=False,
11071107
else:
11081108
raise CannotSendRequest(self.__state)
11091109

1110-
# Save the method we use, we need it later in the response phase
1110+
# Save the method for use later in the response phase
11111111
self._method = method
1112-
if not url:
1113-
url = '/'
1114-
# Prevent CVE-2019-9740.
1115-
match = _contains_disallowed_url_pchar_re.search(url)
1116-
if match:
1117-
raise InvalidURL(f"URL can't contain control characters. {url!r} "
1118-
f"(found at least {match.group()!r})")
1112+
1113+
url = url or '/'
1114+
self._validate_path(url)
1115+
11191116
request = '%s %s %s' % (method, url, self._http_vsn_str)
11201117

1121-
# Non-ASCII characters should have been eliminated earlier
1122-
self._output(request.encode('ascii'))
1118+
self._output(self._encode_request(request))
11231119

11241120
if self._http_vsn == 11:
11251121
# Issue some standard headers for better HTTP/1.1 compliance
@@ -1197,6 +1193,18 @@ def putrequest(self, method, url, skip_host=False,
11971193
# For HTTP/1.0, the server will assume "not chunked"
11981194
pass
11991195

1196+
def _encode_request(self, request):
1197+
# ASCII also helps prevent CVE-2019-9740.
1198+
return request.encode('ascii')
1199+
1200+
def _validate_path(self, url):
1201+
"""Validate a url for putrequest."""
1202+
# Prevent CVE-2019-9740.
1203+
match = _contains_disallowed_url_pchar_re.search(url)
1204+
if match:
1205+
raise InvalidURL(f"URL can't contain control characters. {url!r} "
1206+
f"(found at least {match.group()!r})")
1207+
12001208
def putheader(self, header, *values):
12011209
"""Send a request header line to the server.
12021210

Lib/test/test_httplib.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1157,6 +1157,34 @@ def run_server():
11571157
thread.join()
11581158
self.assertEqual(result, b"proxied data\n")
11591159

1160+
def test_putrequest_override_validation(self):
1161+
"""
1162+
It should be possible to override the default validation
1163+
behavior in putrequest (bpo-38216).
1164+
"""
1165+
class UnsafeHTTPConnection(client.HTTPConnection):
1166+
def _validate_path(self, url):
1167+
pass
1168+
1169+
conn = UnsafeHTTPConnection('example.com')
1170+
conn.sock = FakeSocket('')
1171+
conn.putrequest('GET', '/\x00')
1172+
1173+
def test_putrequest_override_encoding(self):
1174+
"""
1175+
It should be possible to override the default encoding
1176+
to transmit bytes in another encoding even if invalid
1177+
(bpo-36274).
1178+
"""
1179+
class UnsafeHTTPConnection(client.HTTPConnection):
1180+
def _encode_request(self, str_url):
1181+
return str_url.encode('utf-8')
1182+
1183+
conn = UnsafeHTTPConnection('example.com')
1184+
conn.sock = FakeSocket('')
1185+
conn.putrequest('GET', '/☃')
1186+
1187+
11601188
class ExtendedReadTest(TestCase):
11611189
"""
11621190
Test peek(), read1(), readline()
@@ -1281,6 +1309,7 @@ def test_peek_0(self):
12811309
p = self.resp.peek(0)
12821310
self.assertLessEqual(0, len(p))
12831311

1312+
12841313
class ExtendedReadTestChunked(ExtendedReadTest):
12851314
"""
12861315
Test peek(), read1(), readline() in chunked mode
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Allow the rare code that wants to send invalid http requests from the
2+
`http.client` library a way to do so. The fixes for bpo-30458 led to
3+
breakage for some projects that were relying on this ability to test their
4+
own behavior in the face of bad requests.

0 commit comments

Comments
 (0)