Skip to content

Commit defaa2b

Browse files
gh-87389: Fix an open redirection vulnerability in http.server. (GH-93879) (GH-94093)
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) Co-authored-by: Gregory P. Smith <greg@krypto.org>
1 parent 893adbf commit defaa2b

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
@@ -330,6 +330,13 @@ def parse_request(self):
330330
return False
331331
self.command, self.path = command, path
332332

333+
# gh-87389: The purpose of replacing '//' with '/' is to protect
334+
# against open redirect attacks possibly triggered if the path starts
335+
# with '//' because http clients treat //path as an absolute URI
336+
# without scheme (similar to http://path) rather than a path.
337+
if self.path.startswith('//'):
338+
self.path = '/' + self.path.lstrip('/') # Reduce to a single /
339+
333340
# Examine the headers and look for a Connection directive.
334341
try:
335342
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
@@ -331,7 +331,7 @@ class request_handler(NoLogRequestHandler, SimpleHTTPRequestHandler):
331331
pass
332332

333333
def setUp(self):
334-
BaseTestCase.setUp(self)
334+
super().setUp()
335335
self.cwd = os.getcwd()
336336
basetempdir = tempfile.gettempdir()
337337
os.chdir(basetempdir)
@@ -359,7 +359,7 @@ def tearDown(self):
359359
except:
360360
pass
361361
finally:
362-
BaseTestCase.tearDown(self)
362+
super().tearDown()
363363

364364
def check_status_and_reason(self, response, status, data=None):
365365
def close_conn():
@@ -415,6 +415,55 @@ def test_undecodable_filename(self):
415415
self.check_status_and_reason(response, HTTPStatus.OK,
416416
data=support.TESTFN_UNDECODABLE)
417417

418+
def test_get_dir_redirect_location_domain_injection_bug(self):
419+
"""Ensure //evil.co/..%2f../../X does not put //evil.co/ in Location.
420+
421+
//netloc/ in a Location header is a redirect to a new host.
422+
https://github.com/python/cpython/issues/87389
423+
424+
This checks that a path resolving to a directory on our server cannot
425+
resolve into a redirect to another server.
426+
"""
427+
os.mkdir(os.path.join(self.tempdir, 'existing_directory'))
428+
url = f'/python.org/..%2f..%2f..%2f..%2f..%2f../%0a%0d/../{self.tempdir_name}/existing_directory'
429+
expected_location = f'{url}/' # /python.org.../ single slash single prefix, trailing slash
430+
# Canonicalizes to /tmp/tempdir_name/existing_directory which does
431+
# exist and is a dir, triggering the 301 redirect logic.
432+
response = self.request(url)
433+
self.check_status_and_reason(response, HTTPStatus.MOVED_PERMANENTLY)
434+
location = response.getheader('Location')
435+
self.assertEqual(location, expected_location, msg='non-attack failed!')
436+
437+
# //python.org... multi-slash prefix, no trailing slash
438+
attack_url = f'/{url}'
439+
response = self.request(attack_url)
440+
self.check_status_and_reason(response, HTTPStatus.MOVED_PERMANENTLY)
441+
location = response.getheader('Location')
442+
self.assertFalse(location.startswith('//'), msg=location)
443+
self.assertEqual(location, expected_location,
444+
msg='Expected Location header to start with a single / and '
445+
'end with a / as this is a directory redirect.')
446+
447+
# ///python.org... triple-slash prefix, no trailing slash
448+
attack3_url = f'//{url}'
449+
response = self.request(attack3_url)
450+
self.check_status_and_reason(response, HTTPStatus.MOVED_PERMANENTLY)
451+
self.assertEqual(response.getheader('Location'), expected_location)
452+
453+
# If the second word in the http request (Request-URI for the http
454+
# method) is a full URI, we don't worry about it, as that'll be parsed
455+
# and reassembled as a full URI within BaseHTTPRequestHandler.send_head
456+
# so no errant scheme-less //netloc//evil.co/ domain mixup can happen.
457+
attack_scheme_netloc_2slash_url = f'https://pypi.org/{url}'
458+
expected_scheme_netloc_location = f'{attack_scheme_netloc_2slash_url}/'
459+
response = self.request(attack_scheme_netloc_2slash_url)
460+
self.check_status_and_reason(response, HTTPStatus.MOVED_PERMANENTLY)
461+
location = response.getheader('Location')
462+
# We're just ensuring that the scheme and domain make it through, if
463+
# there are or aren't multiple slashes at the start of the path that
464+
# follows that isn't important in this Location: header.
465+
self.assertTrue(location.startswith('https://pypi.org/'), msg=location)
466+
418467
def test_get(self):
419468
#constructs the path relative to the root directory of the HTTPServer
420469
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