Skip to content

Commit 38a1582

Browse files
miss-islingtongpshead
authored andcommitted
Fix an open redirection vulnerability in the `http.server` module when an URI path starts with `//` that could produce a 301 Location header with a misleading target. Vulnerability discovered, and logic fix proposed, by Hamza Avvan (@hamzaavvan). Test and comments authored by Gregory P. Smith [Google]. (cherry picked from commit 4abab6b) Upstream: python#93879 Tracking bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=2120642 Co-authored-by: Gregory P. Smith <greg@krypto.org>
1 parent 0449071 commit 38a1582

File tree

3 files changed

+61
-2
lines changed

3 files changed

+61
-2
lines changed

Lib/http/server.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,13 @@ def parse_request(self):
323323
return False
324324
self.command, self.path, self.request_version = command, path, version
325325

326+
# gh-87389: The purpose of replacing '//' with '/' is to protect
327+
# against open redirect attacks possibly triggered if the path starts
328+
# with '//' because http clients treat //path as an absolute URI
329+
# without scheme (similar to http://path) rather than a path.
330+
if self.path.startswith('//'):
331+
self.path = '/' + self.path.lstrip('/') # Reduce to a single /
332+
326333
# Examine the headers and look for a Connection directive.
327334
try:
328335
self.headers = http.client.parse_headers(self.rfile,

Lib/test/test_httpservers.py

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ class request_handler(NoLogRequestHandler, SimpleHTTPRequestHandler):
324324
pass
325325

326326
def setUp(self):
327-
BaseTestCase.setUp(self)
327+
super().setUp()
328328
self.cwd = os.getcwd()
329329
basetempdir = tempfile.gettempdir()
330330
os.chdir(basetempdir)
@@ -343,7 +343,7 @@ def tearDown(self):
343343
except:
344344
pass
345345
finally:
346-
BaseTestCase.tearDown(self)
346+
super().tearDown()
347347

348348
def check_status_and_reason(self, response, status, data=None):
349349
def close_conn():
@@ -399,6 +399,55 @@ def test_undecodable_filename(self):
399399
self.check_status_and_reason(response, HTTPStatus.OK,
400400
data=support.TESTFN_UNDECODABLE)
401401

402+
def test_get_dir_redirect_location_domain_injection_bug(self):
403+
"""Ensure //evil.co/..%2f../../X does not put //evil.co/ in Location.
404+
405+
//netloc/ in a Location header is a redirect to a new host.
406+
https://github.com/python/cpython/issues/87389
407+
408+
This checks that a path resolving to a directory on our server cannot
409+
resolve into a redirect to another server.
410+
"""
411+
os.mkdir(os.path.join(self.tempdir, 'existing_directory'))
412+
url = f'/python.org/..%2f..%2f..%2f..%2f..%2f../%0a%0d/../{self.tempdir_name}/existing_directory'
413+
expected_location = f'{url}/' # /python.org.../ single slash single prefix, trailing slash
414+
# Canonicalizes to /tmp/tempdir_name/existing_directory which does
415+
# exist and is a dir, triggering the 301 redirect logic.
416+
response = self.request(url)
417+
self.check_status_and_reason(response, HTTPStatus.MOVED_PERMANENTLY)
418+
location = response.getheader('Location')
419+
self.assertEqual(location, expected_location, msg='non-attack failed!')
420+
421+
# //python.org... multi-slash prefix, no trailing slash
422+
attack_url = f'/{url}'
423+
response = self.request(attack_url)
424+
self.check_status_and_reason(response, HTTPStatus.MOVED_PERMANENTLY)
425+
location = response.getheader('Location')
426+
self.assertFalse(location.startswith('//'), msg=location)
427+
self.assertEqual(location, expected_location,
428+
msg='Expected Location header to start with a single / and '
429+
'end with a / as this is a directory redirect.')
430+
431+
# ///python.org... triple-slash prefix, no trailing slash
432+
attack3_url = f'//{url}'
433+
response = self.request(attack3_url)
434+
self.check_status_and_reason(response, HTTPStatus.MOVED_PERMANENTLY)
435+
self.assertEqual(response.getheader('Location'), expected_location)
436+
437+
# If the second word in the http request (Request-URI for the http
438+
# method) is a full URI, we don't worry about it, as that'll be parsed
439+
# and reassembled as a full URI within BaseHTTPRequestHandler.send_head
440+
# so no errant scheme-less //netloc//evil.co/ domain mixup can happen.
441+
attack_scheme_netloc_2slash_url = f'https://pypi.org/{url}'
442+
expected_scheme_netloc_location = f'{attack_scheme_netloc_2slash_url}/'
443+
response = self.request(attack_scheme_netloc_2slash_url)
444+
self.check_status_and_reason(response, HTTPStatus.MOVED_PERMANENTLY)
445+
location = response.getheader('Location')
446+
# We're just ensuring that the scheme and domain make it through, if
447+
# there are or aren't multiple slashes at the start of the path that
448+
# follows that isn't important in this Location: header.
449+
self.assertTrue(location.startswith('https://pypi.org/'), msg=location)
450+
402451
def test_get(self):
403452
#constructs the path relative to the root directory of the HTTPServer
404453
response = self.request(self.base_url + '/test')
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:mod:`http.server`: Fix an open redirection vulnerability in the HTTP server
2+
when an URI path starts with ``//``. Vulnerability discovered, and initial
3+
fix proposed, by Hamza Avvan.

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy