Skip to content

Commit 8a34afd

Browse files
gh-87389: Fix an open redirection vulnerability in http.server. (GH-93879) (GH-94095)
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 9b13df4 commit 8a34afd

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

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

331331
def setUp(self):
332-
BaseTestCase.setUp(self)
332+
super().setUp()
333333
self.cwd = os.getcwd()
334334
basetempdir = tempfile.gettempdir()
335335
os.chdir(basetempdir)
@@ -357,7 +357,7 @@ def tearDown(self):
357357
except:
358358
pass
359359
finally:
360-
BaseTestCase.tearDown(self)
360+
super().tearDown()
361361

362362
def check_status_and_reason(self, response, status, data=None):
363363
def close_conn():
@@ -413,6 +413,55 @@ def test_undecodable_filename(self):
413413
self.check_status_and_reason(response, HTTPStatus.OK,
414414
data=support.TESTFN_UNDECODABLE)
415415

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