Skip to content

Commit e2e8847

Browse files
gh-87389: Fix an open redirection vulnerability in http.server. (GH-93879)
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 a1565a8 commit e2e8847

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

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

336336
def setUp(self):
337-
BaseTestCase.setUp(self)
337+
super().setUp()
338338
self.cwd = os.getcwd()
339339
basetempdir = tempfile.gettempdir()
340340
os.chdir(basetempdir)
@@ -362,7 +362,7 @@ def tearDown(self):
362362
except:
363363
pass
364364
finally:
365-
BaseTestCase.tearDown(self)
365+
super().tearDown()
366366

367367
def check_status_and_reason(self, response, status, data=None):
368368
def close_conn():
@@ -418,6 +418,55 @@ def test_undecodable_filename(self):
418418
self.check_status_and_reason(response, HTTPStatus.OK,
419419
data=os_helper.TESTFN_UNDECODABLE)
420420

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